001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.apache.commons.net.ftp; 019 020import java.io.BufferedReader; 021import java.io.IOException; 022import java.io.InputStream; 023import java.io.InputStreamReader; 024import java.io.OutputStream; 025import java.io.Reader; 026import java.io.UnsupportedEncodingException; 027import java.net.Inet6Address; 028import java.net.Socket; 029import java.net.SocketException; 030import java.util.ArrayList; 031import java.util.List; 032 033import org.apache.commons.net.util.Base64; 034 035/** 036 * Experimental attempt at FTP client that tunnels over an HTTP proxy connection. 037 * 038 * @since 2.2 039 */ 040public class FTPHTTPClient extends FTPClient { 041 private final String proxyHost; 042 private final int proxyPort; 043 private final String proxyUsername; 044 private final String proxyPassword; 045 046 private static final byte[] CRLF={'\r', '\n'}; 047 private final Base64 base64 = new Base64(); 048 049 private String tunnelHost; // Save the host when setting up a tunnel (needed for EPSV) 050 051 public FTPHTTPClient(String proxyHost, int proxyPort, String proxyUser, String proxyPass) { 052 this.proxyHost = proxyHost; 053 this.proxyPort = proxyPort; 054 this.proxyUsername = proxyUser; 055 this.proxyPassword = proxyPass; 056 this.tunnelHost = null; 057 } 058 059 public FTPHTTPClient(String proxyHost, int proxyPort) { 060 this(proxyHost, proxyPort, null, null); 061 } 062 063 064 /** 065 * {@inheritDoc} 066 * 067 * @throws IllegalStateException if connection mode is not passive 068 * @deprecated (3.3) Use {@link FTPClient#_openDataConnection_(FTPCmd, String)} instead 069 */ 070 // Kept to maintain binary compatibility 071 // Not strictly necessary, but Clirr complains even though there is a super-impl 072 @Override 073 @Deprecated 074 protected Socket _openDataConnection_(int command, String arg) 075 throws IOException { 076 return super._openDataConnection_(command, arg); 077 } 078 079 /** 080 * {@inheritDoc} 081 * 082 * @throws IllegalStateException if connection mode is not passive 083 * @since 3.1 084 */ 085 @Override 086 protected Socket _openDataConnection_(String command, String arg) 087 throws IOException { 088 //Force local passive mode, active mode not supported by through proxy 089 if (getDataConnectionMode() != PASSIVE_LOCAL_DATA_CONNECTION_MODE) { 090 throw new IllegalStateException("Only passive connection mode supported"); 091 } 092 093 final boolean isInet6Address = getRemoteAddress() instanceof Inet6Address; 094 String passiveHost = null; 095 096 boolean attemptEPSV = isUseEPSVwithIPv4() || isInet6Address; 097 if (attemptEPSV && epsv() == FTPReply.ENTERING_EPSV_MODE) { 098 _parseExtendedPassiveModeReply(_replyLines.get(0)); 099 passiveHost = this.tunnelHost; 100 } else { 101 if (isInet6Address) { 102 return null; // Must use EPSV for IPV6 103 } 104 // If EPSV failed on IPV4, revert to PASV 105 if (pasv() != FTPReply.ENTERING_PASSIVE_MODE) { 106 return null; 107 } 108 _parsePassiveModeReply(_replyLines.get(0)); 109 passiveHost = this.getPassiveHost(); 110 } 111 112 Socket socket = _socketFactory_.createSocket(proxyHost, proxyPort); 113 InputStream is = socket.getInputStream(); 114 OutputStream os = socket.getOutputStream(); 115 tunnelHandshake(passiveHost, this.getPassivePort(), is, os); 116 if ((getRestartOffset() > 0) && !restart(getRestartOffset())) { 117 socket.close(); 118 return null; 119 } 120 121 if (!FTPReply.isPositivePreliminary(sendCommand(command, arg))) { 122 socket.close(); 123 return null; 124 } 125 126 return socket; 127 } 128 129 @Override 130 public void connect(String host, int port) throws SocketException, IOException { 131 132 _socket_ = _socketFactory_.createSocket(proxyHost, proxyPort); 133 _input_ = _socket_.getInputStream(); 134 _output_ = _socket_.getOutputStream(); 135 Reader socketIsReader; 136 try { 137 socketIsReader = tunnelHandshake(host, port, _input_, _output_); 138 } 139 catch (Exception e) { 140 IOException ioe = new IOException("Could not connect to " + host+ " using port " + port); 141 ioe.initCause(e); 142 throw ioe; 143 } 144 super._connectAction_(socketIsReader); 145 } 146 147 private BufferedReader tunnelHandshake(String host, int port, InputStream input, OutputStream output) throws IOException, 148 UnsupportedEncodingException { 149 final String connectString = "CONNECT " + host + ":" + port + " HTTP/1.1"; 150 final String hostString = "Host: " + host + ":" + port; 151 152 this.tunnelHost = host; 153 output.write(connectString.getBytes("UTF-8")); // TODO what is the correct encoding? 154 output.write(CRLF); 155 output.write(hostString.getBytes("UTF-8")); 156 output.write(CRLF); 157 158 if (proxyUsername != null && proxyPassword != null) { 159 final String auth = proxyUsername + ":" + proxyPassword; 160 final String header = "Proxy-Authorization: Basic " 161 + base64.encodeToString(auth.getBytes("UTF-8")); 162 output.write(header.getBytes("UTF-8")); 163 } 164 output.write(CRLF); 165 166 List<String> response = new ArrayList<String>(); 167 BufferedReader reader = new BufferedReader( 168 new InputStreamReader(input, getCharset())); 169 170 for (String line = reader.readLine(); line != null 171 && line.length() > 0; line = reader.readLine()) { 172 response.add(line); 173 } 174 175 int size = response.size(); 176 if (size == 0) { 177 throw new IOException("No response from proxy"); 178 } 179 180 String code = null; 181 String resp = response.get(0); 182 if (resp.startsWith("HTTP/") && resp.length() >= 12) { 183 code = resp.substring(9, 12); 184 } else { 185 throw new IOException("Invalid response from proxy: " + resp); 186 } 187 188 if (!"200".equals(code)) { 189 StringBuilder msg = new StringBuilder(); 190 msg.append("HTTPTunnelConnector: connection failed\r\n"); 191 msg.append("Response received from the proxy:\r\n"); 192 for (String line : response) { 193 msg.append(line); 194 msg.append("\r\n"); 195 } 196 throw new IOException(msg.toString()); 197 } 198 return reader; 199 } 200} 201 202