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
018package org.apache.commons.net.ftp;
019
020import java.io.BufferedReader;
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.InputStreamReader;
024import java.io.OutputStream;
025import java.io.UnsupportedEncodingException;
026import java.net.Inet6Address;
027import java.net.Socket;
028import java.net.SocketException;
029import java.util.ArrayList;
030import java.util.List;
031
032import 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 */
039public 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     */
078    @Override
079    protected Socket _openDataConnection_(String command, String arg) 
080    throws IOException {
081        //Force local passive mode, active mode not supported by through proxy
082        if (getDataConnectionMode() != PASSIVE_LOCAL_DATA_CONNECTION_MODE) {
083            throw new IllegalStateException("Only passive connection mode supported");
084        }
085
086        final boolean isInet6Address = getRemoteAddress() instanceof Inet6Address;
087        
088        boolean attemptEPSV = isUseEPSVwithIPv4() || isInet6Address;
089        if (attemptEPSV && epsv() == FTPReply.ENTERING_EPSV_MODE) {
090            _parseExtendedPassiveModeReply(_replyLines.get(0));
091        } else {
092            if (isInet6Address) {
093                return null; // Must use EPSV for IPV6
094            }
095            // If EPSV failed on IPV4, revert to PASV
096            if (pasv() != FTPReply.ENTERING_PASSIVE_MODE) {
097                return null;
098            }
099            _parsePassiveModeReply(_replyLines.get(0));
100        }
101
102        Socket socket = new Socket(proxyHost, proxyPort);
103        InputStream is = socket.getInputStream();
104        OutputStream os = socket.getOutputStream();
105        tunnelHandshake(this.getPassiveHost(), this.getPassivePort(), is, os);
106        if ((getRestartOffset() > 0) && !restart(getRestartOffset())) {
107            socket.close();
108            return null;
109        }
110
111        if (!FTPReply.isPositivePreliminary(sendCommand(command, arg))) {
112            socket.close();
113            return null;
114        }
115
116        return socket;
117    }
118
119    @Override
120    public void connect(String host, int port) throws SocketException, IOException {
121
122        _socket_ = new Socket(proxyHost, proxyPort);
123        _input_ = _socket_.getInputStream();
124        _output_ = _socket_.getOutputStream();
125        try {
126            tunnelHandshake(host, port, _input_, _output_);
127        }
128        catch (Exception e) {
129            IOException ioe = new IOException("Could not connect to " + host+ " using port " + port);
130            ioe.initCause(e);
131            throw ioe;
132        }
133        super._connectAction_();
134    }
135
136    private void tunnelHandshake(String host, int port, InputStream input, OutputStream output) throws IOException,
137    UnsupportedEncodingException {
138        final String connectString = "CONNECT "  + host + ":" + port  + " HTTP/1.1";
139        final String hostString = "Host: " + host + ":" + port;
140
141        output.write(connectString.getBytes("UTF-8")); // TODO what is the correct encoding?
142        output.write(CRLF);
143        output.write(hostString.getBytes("UTF-8"));
144        output.write(CRLF);
145
146        if (proxyUsername != null && proxyPassword != null) {
147            final String auth = proxyUsername + ":" + proxyPassword;
148            final String header = "Proxy-Authorization: Basic "
149                + base64.encodeToString(auth.getBytes("UTF-8"));
150            output.write(header.getBytes("UTF-8"));
151        }
152        output.write(CRLF);
153
154        List<String> response = new ArrayList<String>();
155        BufferedReader reader = new BufferedReader(
156                new InputStreamReader(input));
157
158        for (String line = reader.readLine(); line != null
159        && line.length() > 0; line = reader.readLine()) {
160            response.add(line);
161        }
162
163        int size = response.size();
164        if (size == 0) {
165            throw new IOException("No response from proxy");
166        }
167
168        String code = null;
169        String resp = response.get(0);
170        if (resp.startsWith("HTTP/") && resp.length() >= 12) {
171            code = resp.substring(9, 12);
172        } else {
173            throw new IOException("Invalid response from proxy: " + resp);
174        }
175
176        if (!"200".equals(code)) {
177            StringBuilder msg = new StringBuilder();
178            msg.append("HTTPTunnelConnector: connection failed\r\n");
179            msg.append("Response received from the proxy:\r\n");
180            for (String line : response) {
181                msg.append(line);
182                msg.append("\r\n");
183            }
184            throw new IOException(msg.toString());
185        }
186    }
187}
188
189