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