    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