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