View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.net.ftp;
19  
20  import java.io.BufferedReader;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.InputStreamReader;
24  import java.io.OutputStream;
25  import java.io.Reader;
26  import java.io.UnsupportedEncodingException;
27  import java.net.Inet6Address;
28  import java.net.Socket;
29  import java.net.SocketException;
30  import java.nio.charset.Charset;
31  import java.nio.charset.StandardCharsets;
32  import java.util.ArrayList;
33  import java.util.Base64;
34  import java.util.List;
35  
36  /**
37   * Experimental attempt at FTP client that tunnels over an HTTP proxy connection.
38   *
39   * @since 2.2
40   */
41  public class FTPHTTPClient extends FTPClient {
42  
43      private static final byte[] CRLF = { '\r', '\n' };
44      private final String proxyHost;
45      private final int proxyPort;
46      private final String proxyUserName;
47      private final String proxyPassword;
48      private final Charset charset;
49      private String tunnelHost; // Save the host when setting up a tunnel (needed for EPSV)
50  
51      /**
52       * Create an instance using the UTF-8 encoding, with no proxy credentials.
53       *
54       * @param proxyHost the hostname to use
55       * @param proxyPort the port to use
56       */
57      public FTPHTTPClient(final String proxyHost, final int proxyPort) {
58          this(proxyHost, proxyPort, null, null);
59      }
60  
61      /**
62       * Create an instance using the specified encoding, with no proxy credentials.
63       *
64       * @param proxyHost the hostname to use
65       * @param proxyPort the port to use
66       * @param encoding  the encoding to use
67       */
68      public FTPHTTPClient(final String proxyHost, final int proxyPort, final Charset encoding) {
69          this(proxyHost, proxyPort, null, null, encoding);
70      }
71  
72      /**
73       * Create an instance using the UTF-8 encoding
74       *
75       * @param proxyHost the hostname to use
76       * @param proxyPort the port to use
77       * @param proxyUser the user name for the proxy
78       * @param proxyPass the password for the proxy
79       */
80      public FTPHTTPClient(final String proxyHost, final int proxyPort, final String proxyUser, final String proxyPass) {
81          this(proxyHost, proxyPort, proxyUser, proxyPass, StandardCharsets.UTF_8);
82      }
83  
84      /**
85       * Create an instance with the specified encoding
86       *
87       * @param proxyHost the hostname to use
88       * @param proxyPort the port to use
89       * @param proxyUser the user name for the proxy
90       * @param proxyPass the password for the proxy
91       * @param encoding  the encoding to use
92       */
93      public FTPHTTPClient(final String proxyHost, final int proxyPort, final String proxyUser, final String proxyPass, final Charset encoding) {
94          this.proxyHost = proxyHost;
95          this.proxyPort = proxyPort;
96          this.proxyUserName = proxyUser;
97          this.proxyPassword = proxyPass;
98          this.charset = encoding;
99      }
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 }