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 EPSV failed on IPV4, revert to PASV
137             if (isInet6Address || pasv() != FTPReply.ENTERING_PASSIVE_MODE) {
138                 return null;
139             }
140             _parsePassiveModeReply(_replyLines.get(0));
141             passiveHost = getPassiveHost();
142         }
143 
144         final Socket socket = _socketFactory_.createSocket(proxyHost, proxyPort);
145         final InputStream is = socket.getInputStream();
146         final OutputStream os = socket.getOutputStream();
147         tunnelHandshake(passiveHost, getPassivePort(), is, os);
148         if (getRestartOffset() > 0 && !restart(getRestartOffset()) || !FTPReply.isPositivePreliminary(sendCommand(command, arg))) {
149             socket.close();
150             return null;
151         }
152 
153         return socket;
154     }
155 
156     @Override
157     public void connect(final String host, final int port) throws SocketException, IOException {
158 
159         _socket_ = _socketFactory_.createSocket(proxyHost, proxyPort);
160         _input_ = _socket_.getInputStream();
161         _output_ = _socket_.getOutputStream();
162         final Reader socketIsReader;
163         try {
164             socketIsReader = tunnelHandshake(host, port, _input_, _output_);
165         } catch (final Exception e) {
166             throw new IOException("Could not connect to " + host + " using port " + port, e);
167         }
168         super._connectAction_(socketIsReader);
169     }
170 
171     private BufferedReader tunnelHandshake(final String host, final int port, final InputStream input, final OutputStream output)
172             throws IOException, UnsupportedEncodingException {
173         final String connectString = "CONNECT " + host + ":" + port + " HTTP/1.1";
174         final String hostString = "Host: " + host + ":" + port;
175 
176         this.tunnelHost = host;
177         output.write(connectString.getBytes(charset));
178         output.write(CRLF);
179         output.write(hostString.getBytes(charset));
180         output.write(CRLF);
181 
182         if (proxyUserName != null && proxyPassword != null) {
183             final String auth = proxyUserName + ":" + proxyPassword;
184             final String header = "Proxy-Authorization: Basic " + Base64.getEncoder().encodeToString(auth.getBytes(charset));
185             output.write(header.getBytes(charset));
186             output.write(CRLF);
187         }
188         output.write(CRLF);
189 
190         final List<String> response = new ArrayList<>();
191         final BufferedReader reader = new BufferedReader(new InputStreamReader(input, getCharset()));
192 
193         for (String line = reader.readLine(); line != null && !line.isEmpty(); line = reader.readLine()) {
194             response.add(line);
195         }
196 
197         final int size = response.size();
198         if (size == 0) {
199             throw new IOException("No response from proxy");
200         }
201 
202         final String code;
203         final String resp = response.get(0);
204         if (!resp.startsWith("HTTP/") || resp.length() < 12) {
205             throw new IOException("Invalid response from proxy: " + resp);
206         }
207         code = resp.substring(9, 12);
208 
209         if (!"200".equals(code)) {
210             final StringBuilder msg = new StringBuilder(256);
211             msg.append("HTTPTunnelConnector: connection failed\r\n");
212             msg.append("Response received from the proxy:\r\n");
213             for (final String line : response) {
214                 msg.append(line);
215                 msg.append("\r\n");
216             }
217             throw new IOException(msg.toString());
218         }
219         return reader;
220     }
221 }