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