001 /*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018 package org.apache.commons.net.ftp;
019
020 import java.io.BufferedReader;
021 import java.io.IOException;
022 import java.io.InputStream;
023 import java.io.InputStreamReader;
024 import java.io.OutputStream;
025 import java.io.UnsupportedEncodingException;
026 import java.net.Inet6Address;
027 import java.net.Socket;
028 import java.net.SocketException;
029 import java.util.ArrayList;
030 import java.util.List;
031
032 import org.apache.commons.net.util.Base64;
033
034 /**
035 * Experimental attempt at FTP client that tunnels over an HTTP proxy connection.
036 *
037 * @since 2.2
038 */
039 public class FTPHTTPClient extends FTPClient {
040 private final String proxyHost;
041 private final int proxyPort;
042 private final String proxyUsername;
043 private final String proxyPassword;
044
045 private static final byte[] CRLF={'\r', '\n'};
046 private final Base64 base64 = new Base64();
047
048 public FTPHTTPClient(String proxyHost, int proxyPort, String proxyUser, String proxyPass) {
049 this.proxyHost = proxyHost;
050 this.proxyPort = proxyPort;
051 this.proxyUsername = proxyUser;
052 this.proxyPassword = proxyPass;
053 }
054
055 public FTPHTTPClient(String proxyHost, int proxyPort) {
056 this(proxyHost, proxyPort, null, null);
057 }
058
059
060 /**
061 * {@inheritDoc}
062 *
063 * @throws IllegalStateException if connection mode is not passive
064 */
065 // Kept to maintain binary compatibility
066 // Not strictly necessary, but Clirr complains even though there is a super-impl
067 @Override
068 protected Socket _openDataConnection_(int command, String arg)
069 throws IOException {
070 return super._openDataConnection_(command, arg);
071 }
072
073 /**
074 * {@inheritDoc}
075 *
076 * @throws IllegalStateException if connection mode is not passive
077 * @since 3.1
078 */
079 @Override
080 protected Socket _openDataConnection_(String command, String arg)
081 throws IOException {
082 //Force local passive mode, active mode not supported by through proxy
083 if (getDataConnectionMode() != PASSIVE_LOCAL_DATA_CONNECTION_MODE) {
084 throw new IllegalStateException("Only passive connection mode supported");
085 }
086
087 final boolean isInet6Address = getRemoteAddress() instanceof Inet6Address;
088
089 boolean attemptEPSV = isUseEPSVwithIPv4() || isInet6Address;
090 if (attemptEPSV && epsv() == FTPReply.ENTERING_EPSV_MODE) {
091 _parseExtendedPassiveModeReply(_replyLines.get(0));
092 } else {
093 if (isInet6Address) {
094 return null; // Must use EPSV for IPV6
095 }
096 // If EPSV failed on IPV4, revert to PASV
097 if (pasv() != FTPReply.ENTERING_PASSIVE_MODE) {
098 return null;
099 }
100 _parsePassiveModeReply(_replyLines.get(0));
101 }
102
103 Socket socket = new Socket(proxyHost, proxyPort);
104 InputStream is = socket.getInputStream();
105 OutputStream os = socket.getOutputStream();
106 tunnelHandshake(this.getPassiveHost(), this.getPassivePort(), is, os);
107 if ((getRestartOffset() > 0) && !restart(getRestartOffset())) {
108 socket.close();
109 return null;
110 }
111
112 if (!FTPReply.isPositivePreliminary(sendCommand(command, arg))) {
113 socket.close();
114 return null;
115 }
116
117 return socket;
118 }
119
120 @Override
121 public void connect(String host, int port) throws SocketException, IOException {
122
123 _socket_ = new Socket(proxyHost, proxyPort);
124 _input_ = _socket_.getInputStream();
125 _output_ = _socket_.getOutputStream();
126 try {
127 tunnelHandshake(host, port, _input_, _output_);
128 }
129 catch (Exception e) {
130 IOException ioe = new IOException("Could not connect to " + host+ " using port " + port);
131 ioe.initCause(e);
132 throw ioe;
133 }
134 super._connectAction_();
135 }
136
137 private void tunnelHandshake(String host, int port, InputStream input, OutputStream output) throws IOException,
138 UnsupportedEncodingException {
139 final String connectString = "CONNECT " + host + ":" + port + " HTTP/1.1";
140 final String hostString = "Host: " + host + ":" + port;
141
142 output.write(connectString.getBytes("UTF-8")); // TODO what is the correct encoding?
143 output.write(CRLF);
144 output.write(hostString.getBytes("UTF-8"));
145 output.write(CRLF);
146
147 if (proxyUsername != null && proxyPassword != null) {
148 final String auth = proxyUsername + ":" + proxyPassword;
149 final String header = "Proxy-Authorization: Basic "
150 + base64.encodeToString(auth.getBytes("UTF-8"));
151 output.write(header.getBytes("UTF-8"));
152 }
153 output.write(CRLF);
154
155 List<String> response = new ArrayList<String>();
156 BufferedReader reader = new BufferedReader(
157 new InputStreamReader(input));
158
159 for (String line = reader.readLine(); line != null
160 && line.length() > 0; line = reader.readLine()) {
161 response.add(line);
162 }
163
164 int size = response.size();
165 if (size == 0) {
166 throw new IOException("No response from proxy");
167 }
168
169 String code = null;
170 String resp = response.get(0);
171 if (resp.startsWith("HTTP/") && resp.length() >= 12) {
172 code = resp.substring(9, 12);
173 } else {
174 throw new IOException("Invalid response from proxy: " + resp);
175 }
176
177 if (!"200".equals(code)) {
178 StringBuilder msg = new StringBuilder();
179 msg.append("HTTPTunnelConnector: connection failed\r\n");
180 msg.append("Response received from the proxy:\r\n");
181 for (String line : response) {
182 msg.append(line);
183 msg.append("\r\n");
184 }
185 throw new IOException(msg.toString());
186 }
187 }
188 }
189
190