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