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