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.nio.charset.Charset; 031import java.util.ArrayList; 032import java.util.List; 033 034import org.apache.commons.net.util.Base64; 035 036/** 037 * Experimental attempt at FTP client that tunnels over an HTTP proxy connection. 038 * 039 * @since 2.2 040 */ 041public class FTPHTTPClient extends FTPClient { 042 private final String proxyHost; 043 private final int proxyPort; 044 private final String proxyUsername; 045 private final String proxyPassword; 046 private final Charset charset; 047 048 private static final byte[] CRLF={'\r', '\n'}; 049 private final Base64 base64 = new Base64(); 050 051 private String tunnelHost; // Save the host when setting up a tunnel (needed for EPSV) 052 053 /** 054 * Create an instance with the specified encoding 055 * 056 * @param proxyHost the hostname to use 057 * @param proxyPort the port to use 058 * @param proxyUser the user name for the proxy 059 * @param proxyPass the password for the proxy 060 * @param encoding the encoding to use 061 */ 062 public FTPHTTPClient(String proxyHost, int proxyPort, String proxyUser, String proxyPass, Charset encoding) { 063 this.proxyHost = proxyHost; 064 this.proxyPort = proxyPort; 065 this.proxyUsername = proxyUser; 066 this.proxyPassword = proxyPass; 067 this.tunnelHost = null; 068 this.charset = encoding; 069 } 070 071 /** 072 * Create an instance using the UTF-8 encoding 073 * 074 * @param proxyHost the hostname to use 075 * @param proxyPort the port to use 076 * @param proxyUser the user name for the proxy 077 * @param proxyPass the password for the proxy 078 */ 079 public FTPHTTPClient(String proxyHost, int proxyPort, String proxyUser, String proxyPass) { 080 this(proxyHost, proxyPort, proxyUser, proxyPass, Charset.forName("UTF-8")); 081 } 082 083 /** 084 * Create an instance using the UTF-8 encoding, with no proxy credentials. 085 * 086 * @param proxyHost the hostname to use 087 * @param proxyPort the port to use 088 */ 089 public FTPHTTPClient(String proxyHost, int proxyPort) { 090 this(proxyHost, proxyPort, null, null); 091 } 092 093 094 /** 095 * Create an instance using the specified encoding, with no proxy credentials. 096 * 097 * @param proxyHost the hostname to use 098 * @param proxyPort the port to use 099 * @param encoding the encoding to use 100 */ 101 public FTPHTTPClient(String proxyHost, int proxyPort, Charset encoding) { 102 this(proxyHost, proxyPort, null, null, encoding); 103 } 104 105 106 /** 107 * {@inheritDoc} 108 * 109 * @throws IllegalStateException if connection mode is not passive 110 * @deprecated (3.3) Use {@link FTPClient#_openDataConnection_(FTPCmd, String)} instead 111 */ 112 // Kept to maintain binary compatibility 113 // Not strictly necessary, but Clirr complains even though there is a super-impl 114 @Override 115 @Deprecated 116 protected Socket _openDataConnection_(int command, String arg) 117 throws IOException { 118 return super._openDataConnection_(command, arg); 119 } 120 121 /** 122 * {@inheritDoc} 123 * 124 * @throws IllegalStateException if connection mode is not passive 125 * @since 3.1 126 */ 127 @Override 128 protected Socket _openDataConnection_(String command, String arg) 129 throws IOException { 130 //Force local passive mode, active mode not supported by through proxy 131 if (getDataConnectionMode() != PASSIVE_LOCAL_DATA_CONNECTION_MODE) { 132 throw new IllegalStateException("Only passive connection mode supported"); 133 } 134 135 final boolean isInet6Address = getRemoteAddress() instanceof Inet6Address; 136 String passiveHost = null; 137 138 boolean attemptEPSV = isUseEPSVwithIPv4() || isInet6Address; 139 if (attemptEPSV && epsv() == FTPReply.ENTERING_EPSV_MODE) { 140 _parseExtendedPassiveModeReply(_replyLines.get(0)); 141 passiveHost = this.tunnelHost; 142 } else { 143 if (isInet6Address) { 144 return null; // Must use EPSV for IPV6 145 } 146 // If EPSV failed on IPV4, revert to PASV 147 if (pasv() != FTPReply.ENTERING_PASSIVE_MODE) { 148 return null; 149 } 150 _parsePassiveModeReply(_replyLines.get(0)); 151 passiveHost = this.getPassiveHost(); 152 } 153 154 Socket socket = _socketFactory_.createSocket(proxyHost, proxyPort); 155 InputStream is = socket.getInputStream(); 156 OutputStream os = socket.getOutputStream(); 157 tunnelHandshake(passiveHost, this.getPassivePort(), is, os); 158 if ((getRestartOffset() > 0) && !restart(getRestartOffset())) { 159 socket.close(); 160 return null; 161 } 162 163 if (!FTPReply.isPositivePreliminary(sendCommand(command, arg))) { 164 socket.close(); 165 return null; 166 } 167 168 return socket; 169 } 170 171 @Override 172 public void connect(String host, int port) throws SocketException, IOException { 173 174 _socket_ = _socketFactory_.createSocket(proxyHost, proxyPort); 175 _input_ = _socket_.getInputStream(); 176 _output_ = _socket_.getOutputStream(); 177 Reader socketIsReader; 178 try { 179 socketIsReader = tunnelHandshake(host, port, _input_, _output_); 180 } 181 catch (Exception e) { 182 IOException ioe = new IOException("Could not connect to " + host+ " using port " + port); 183 ioe.initCause(e); 184 throw ioe; 185 } 186 super._connectAction_(socketIsReader); 187 } 188 189 private BufferedReader tunnelHandshake(String host, int port, InputStream input, OutputStream output) throws IOException, 190 UnsupportedEncodingException { 191 final String connectString = "CONNECT " + host + ":" + port + " HTTP/1.1"; 192 final String hostString = "Host: " + host + ":" + port; 193 194 this.tunnelHost = host; 195 output.write(connectString.getBytes(charset)); 196 output.write(CRLF); 197 output.write(hostString.getBytes(charset)); 198 output.write(CRLF); 199 200 if (proxyUsername != null && proxyPassword != null) { 201 final String auth = proxyUsername + ":" + proxyPassword; 202 final String header = "Proxy-Authorization: Basic " 203 + base64.encodeToString(auth.getBytes(charset)); 204 output.write(header.getBytes(charset)); 205 } 206 output.write(CRLF); 207 208 List<String> response = new ArrayList<String>(); 209 BufferedReader reader = new BufferedReader( 210 new InputStreamReader(input, getCharset())); 211 212 for (String line = reader.readLine(); line != null 213 && line.length() > 0; line = reader.readLine()) { 214 response.add(line); 215 } 216 217 int size = response.size(); 218 if (size == 0) { 219 throw new IOException("No response from proxy"); 220 } 221 222 String code = null; 223 String resp = response.get(0); 224 if (resp.startsWith("HTTP/") && resp.length() >= 12) { 225 code = resp.substring(9, 12); 226 } else { 227 throw new IOException("Invalid response from proxy: " + resp); 228 } 229 230 if (!"200".equals(code)) { 231 StringBuilder msg = new StringBuilder(); 232 msg.append("HTTPTunnelConnector: connection failed\r\n"); 233 msg.append("Response received from the proxy:\r\n"); 234 for (String line : response) { 235 msg.append(line); 236 msg.append("\r\n"); 237 } 238 throw new IOException(msg.toString()); 239 } 240 return reader; 241 } 242} 243 244