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    *      http://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.tunnelHost = null;
99          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 = this.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, this.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         }
198         output.write(CRLF);
199 
200         final List<String> response = new ArrayList<>();
201         final BufferedReader reader = new BufferedReader(new InputStreamReader(input, getCharset()));
202 
203         for (String line = reader.readLine(); line != null && !line.isEmpty(); line = reader.readLine()) {
204             response.add(line);
205         }
206 
207         final int size = response.size();
208         if (size == 0) {
209             throw new IOException("No response from proxy");
210         }
211 
212         String code;
213         final String resp = response.get(0);
214         if (!resp.startsWith("HTTP/") || resp.length() < 12) {
215             throw new IOException("Invalid response from proxy: " + resp);
216         }
217         code = resp.substring(9, 12);
218 
219         if (!"200".equals(code)) {
220             final StringBuilder msg = new StringBuilder();
221             msg.append("HTTPTunnelConnector: connection failed\r\n");
222             msg.append("Response received from the proxy:\r\n");
223             for (final String line : response) {
224                 msg.append(line);
225                 msg.append("\r\n");
226             }
227             throw new IOException(msg.toString());
228         }
229         return reader;
230     }
231 }