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.util.ArrayList;
31  import java.util.List;
32  
33  import org.apache.commons.net.util.Base64;
34  
35  /**
36   * Experimental attempt at FTP client that tunnels over an HTTP proxy connection.
37   *
38   * @since 2.2
39   */
40  public class FTPHTTPClient extends FTPClient {
41      private final String proxyHost;
42      private final int proxyPort;
43      private final String proxyUsername;
44      private final String proxyPassword;
45  
46      private static final byte[] CRLF={'\r', '\n'};
47      private final Base64 base64 = new Base64();
48  
49      private String tunnelHost; // Save the host when setting up a tunnel (needed for EPSV)
50  
51      public FTPHTTPClient(String proxyHost, int proxyPort, String proxyUser, String proxyPass) {
52          this.proxyHost = proxyHost;
53          this.proxyPort = proxyPort;
54          this.proxyUsername = proxyUser;
55          this.proxyPassword = proxyPass;
56          this.tunnelHost = null;
57      }
58  
59      public FTPHTTPClient(String proxyHost, int proxyPort) {
60          this(proxyHost, proxyPort, null, null);
61      }
62  
63  
64      /**
65       * {@inheritDoc}
66       *
67       * @throws IllegalStateException if connection mode is not passive
68       * @deprecated (3.3) Use {@link FTPClient#_openDataConnection_(FTPCmd, String)} instead
69       */
70      // Kept to maintain binary compatibility
71      // Not strictly necessary, but Clirr complains even though there is a super-impl
72      @Override
73      @Deprecated
74      protected Socket _openDataConnection_(int command, String arg)
75      throws IOException {
76          return super._openDataConnection_(command, arg);
77      }
78  
79      /**
80       * {@inheritDoc}
81       *
82       * @throws IllegalStateException if connection mode is not passive
83       * @since 3.1
84       */
85      @Override
86      protected Socket _openDataConnection_(String command, String arg)
87      throws IOException {
88          //Force local passive mode, active mode not supported by through proxy
89          if (getDataConnectionMode() != PASSIVE_LOCAL_DATA_CONNECTION_MODE) {
90              throw new IllegalStateException("Only passive connection mode supported");
91          }
92  
93          final boolean isInet6Address = getRemoteAddress() instanceof Inet6Address;
94          String passiveHost = null;
95  
96          boolean attemptEPSV = isUseEPSVwithIPv4() || isInet6Address;
97          if (attemptEPSV && epsv() == FTPReply.ENTERING_EPSV_MODE) {
98              _parseExtendedPassiveModeReply(_replyLines.get(0));
99              passiveHost = this.tunnelHost;
100         } else {
101             if (isInet6Address) {
102                 return null; // Must use EPSV for IPV6
103             }
104             // If EPSV failed on IPV4, revert to PASV
105             if (pasv() != FTPReply.ENTERING_PASSIVE_MODE) {
106                 return null;
107             }
108             _parsePassiveModeReply(_replyLines.get(0));
109             passiveHost = this.getPassiveHost();
110         }
111 
112         Socket socket = _socketFactory_.createSocket(proxyHost, proxyPort);
113         InputStream is = socket.getInputStream();
114         OutputStream os = socket.getOutputStream();
115         tunnelHandshake(passiveHost, this.getPassivePort(), is, os);
116         if ((getRestartOffset() > 0) && !restart(getRestartOffset())) {
117             socket.close();
118             return null;
119         }
120 
121         if (!FTPReply.isPositivePreliminary(sendCommand(command, arg))) {
122             socket.close();
123             return null;
124         }
125 
126         return socket;
127     }
128 
129     @Override
130     public void connect(String host, int port) throws SocketException, IOException {
131 
132         _socket_ = _socketFactory_.createSocket(proxyHost, proxyPort);
133         _input_ = _socket_.getInputStream();
134         _output_ = _socket_.getOutputStream();
135         Reader socketIsReader;
136         try {
137             socketIsReader = tunnelHandshake(host, port, _input_, _output_);
138         }
139         catch (Exception e) {
140             IOException ioe = new IOException("Could not connect to " + host+ " using port " + port);
141             ioe.initCause(e);
142             throw ioe;
143         }
144         super._connectAction_(socketIsReader);
145     }
146 
147     private BufferedReader tunnelHandshake(String host, int port, InputStream input, OutputStream output) throws IOException,
148     UnsupportedEncodingException {
149         final String connectString = "CONNECT "  + host + ":" + port  + " HTTP/1.1";
150         final String hostString = "Host: " + host + ":" + port;
151 
152         this.tunnelHost = host;
153         output.write(connectString.getBytes("UTF-8")); // TODO what is the correct encoding?
154         output.write(CRLF);
155         output.write(hostString.getBytes("UTF-8"));
156         output.write(CRLF);
157 
158         if (proxyUsername != null && proxyPassword != null) {
159             final String auth = proxyUsername + ":" + proxyPassword;
160             final String header = "Proxy-Authorization: Basic "
161                 + base64.encodeToString(auth.getBytes("UTF-8"));
162             output.write(header.getBytes("UTF-8"));
163         }
164         output.write(CRLF);
165 
166         List<String> response = new ArrayList<String>();
167         BufferedReader reader = new BufferedReader(
168                 new InputStreamReader(input, getCharset()));
169 
170         for (String line = reader.readLine(); line != null
171         && line.length() > 0; line = reader.readLine()) {
172             response.add(line);
173         }
174 
175         int size = response.size();
176         if (size == 0) {
177             throw new IOException("No response from proxy");
178         }
179 
180         String code = null;
181         String resp = response.get(0);
182         if (resp.startsWith("HTTP/") && resp.length() >= 12) {
183             code = resp.substring(9, 12);
184         } else {
185             throw new IOException("Invalid response from proxy: " + resp);
186         }
187 
188         if (!"200".equals(code)) {
189             StringBuilder msg = new StringBuilder();
190             msg.append("HTTPTunnelConnector: connection failed\r\n");
191             msg.append("Response received from the proxy:\r\n");
192             for (String line : response) {
193                 msg.append(line);
194                 msg.append("\r\n");
195             }
196             throw new IOException(msg.toString());
197         }
198         return reader;
199     }
200 }
201 
202