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