001    /*
002     * ftp4j - A pure Java FTP client library
003     * 
004     * Copyright (C) 2008 Carlo Pelliccia (www.sauronsoftware.it)
005     * 
006     * This program is free software: you can redistribute it and/or modify
007     * it under the terms of the GNU General Public License as published by
008     * the Free Software Foundation, either version 3 of the License, or
009     * (at your option) any later version.
010     *
011     * This program is distributed in the hope that it will be useful,
012     * but WITHOUT ANY WARRANTY; without even the implied warranty of
013     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014     * GNU General Public License for more details.
015     *
016     * You should have received a copy of the GNU General Public License
017     * along with this program.  If not, see <http://www.gnu.org/licenses/>.
018     */
019    package it.sauronsoftware.ftp4j.connectors;
020    
021    import it.sauronsoftware.ftp4j.FTPConnection;
022    import it.sauronsoftware.ftp4j.FTPConnector;
023    
024    import java.io.IOException;
025    import java.io.InputStream;
026    import java.io.OutputStream;
027    import java.net.Socket;
028    
029    /**
030     * This one connects a remote ftp host through a SOCKS5 proxy server.
031     * 
032     * @author Carlo Pelliccia
033     */
034    public class SOCKS5Connector implements FTPConnector {
035    
036            /**
037             * The socks5 proxy host name.
038             */
039            private String socks5host;
040    
041            /**
042             * The socks5 proxy port.
043             */
044            private int socks5port;
045    
046            /**
047             * The socks5 proxy user (optional).
048             */
049            private String socks5user;
050    
051            /**
052             * The socks5 proxy password (optional).
053             */
054            private String socks5pass;
055    
056            /**
057             * It builds the connector.
058             * 
059             * @param socks5host
060             *            The socks5 proxy host name.
061             * @param socks5port
062             *            The socks5 proxy port.
063             * @param socks5user
064             *            The socks5 proxy user (optional, can be set to null).
065             * @param socks5pass
066             *            The socks5 proxy password (optional, can be set to null if
067             *            also socks5user is null).
068             */
069            public SOCKS5Connector(String socks5host, int socks5port,
070                            String socks5user, String socks5pass) {
071                    this.socks5host = socks5host;
072                    this.socks5port = socks5port;
073                    this.socks5user = socks5user;
074                    this.socks5pass = socks5pass;
075            }
076    
077            /**
078             * It builds the connector.
079             * 
080             * @param socks5host
081             *            The socks5 proxy host name.
082             * @param socks5port
083             *            The socks5 proxy port.
084             */
085            public SOCKS5Connector(String socks5host, int socks5port) {
086                    this(socks5host, socks5port, null, null);
087            }
088    
089            private FTPConnection connect(String host, int port) throws IOException {
090                    // Authentication flag
091                    boolean authentication = socks5user != null && socks5pass != null;
092                    // A connection status flag.
093                    boolean connected = false;
094                    // The socket for the connection with the proxy.
095                    Socket socket = null;
096                    InputStream in = null;
097                    OutputStream out = null;
098                    // FTPConnection routine.
099                    try {
100                            socket = new Socket(socks5host, socks5port);
101                            in = socket.getInputStream();
102                            out = socket.getOutputStream();
103                            int aux;
104                            // Version 5.
105                            out.write(0x05);
106                            // Authentication?
107                            if (authentication) {
108                                    // Authentication with username/password.
109                                    out.write(0x01);
110                                    out.write(0x02);
111                            } else {
112                                    // No authentication.
113                                    out.write(0x01);
114                                    out.write(0x00);
115                            }
116                            // Get the response.
117                            aux = read(in);
118                            if (aux != 0x05) {
119                                    throw new IOException("SOCKS5Connector: invalid proxy response");
120                            }
121                            aux = read(in);
122                            if (authentication) {
123                                    if (aux != 0x02) {
124                                            throw new IOException(
125                                                            "SOCKS5Connector: proxy doesn't support "
126                                                                            + "username/password authentication method");
127                                    }
128                                    // Authentication with username/password.
129                                    byte[] user = socks5user.getBytes("UTF-8");
130                                    byte[] pass = socks5pass.getBytes("UTF-8");
131                                    int userLength = user.length;
132                                    int passLength = pass.length;
133                                    // Check sizes.
134                                    if (userLength > 0xff) {
135                                            throw new IOException("SOCKS5Connector: username too long");
136                                    }
137                                    if (passLength > 0xff) {
138                                            throw new IOException("SOCKS5Connector: password too long");
139                                    }
140                                    // Version 1.
141                                    out.write(0x01);
142                                    // Username.
143                                    out.write(userLength);
144                                    out.write(user);
145                                    // Password.
146                                    out.write(passLength);
147                                    out.write(pass);
148                                    // Check the response.
149                                    aux = read(in);
150                                    if (aux != 0x01) {
151                                            throw new IOException(
152                                                            "SOCKS5Connector: invalid proxy response");
153                                    }
154                                    aux = read(in);
155                                    if (aux != 0x00) {
156                                            throw new IOException(
157                                                            "SOCKS5Connector: authentication failed");
158                                    }
159                            } else {
160                                    if (aux != 0x00) {
161                                            throw new IOException(
162                                                            "SOCKS5Connector: proxy requires authentication");
163                                    }
164                            }
165                            // FTPConnection request.
166                            // Version 5.
167                            out.write(0x05);
168                            // CONNECT method
169                            out.write(0x01);
170                            // Reserved.
171                            out.write(0x00);
172                            // Address type -> domain.
173                            out.write(0x03);
174                            // Domain.
175                            byte[] domain = host.getBytes("UTF-8");
176                            if (domain.length > 0xff) {
177                                    throw new IOException("SOCKS5Connector: domain name too long");
178                            }
179                            out.write(domain.length);
180                            out.write(domain);
181                            // Port number.
182                            out.write(port >> 8);
183                            out.write(port);
184    
185                            // FTPConnection response
186                            // Version?
187                            aux = read(in);
188                            if (aux != 0x05) {
189                                    throw new IOException("SOCKS5Connector: invalid proxy response");
190                            }
191                            // Status?
192                            aux = read(in);
193                            switch (aux) {
194                            case 0x00:
195                                    // Connected!
196                                    break;
197                            case 0x01:
198                                    throw new IOException("SOCKS5Connector: general failure");
199                            case 0x02:
200                                    throw new IOException(
201                                                    "SOCKS5Connector: connection not allowed by ruleset");
202                            case 0x03:
203                                    throw new IOException("SOCKS5Connector: network unreachable");
204                            case 0x04:
205                                    throw new IOException("SOCKS5Connector: host unreachable");
206                            case 0x05:
207                                    throw new IOException(
208                                                    "SOCKS5Connector: connection refused by destination host");
209                            case 0x06:
210                                    throw new IOException("SOCKS5Connector: TTL expired");
211                            case 0x07:
212                                    throw new IOException(
213                                                    "SOCKS5Connector: command not supported / protocol error");
214                            case 0x08:
215                                    throw new IOException(
216                                                    "SOCKS5Connector: address type not supported");
217                            default:
218                                    throw new IOException("SOCKS5Connector: invalid proxy response");
219                            }
220                            // Reserved.
221                            in.skip(1);
222                            // Address type.
223                            aux = read(in);
224                            if (aux == 0x01) {
225                                    // IPv4.
226                                    in.skip(4);
227                            } else if (aux == 0x03) {
228                                    // Domain name.
229                                    aux = read(in);
230                                    in.skip(aux);
231                            } else if (aux == 0x04) {
232                                    // IPv6.
233                                    in.skip(16);
234                            } else {
235                                    throw new IOException("SOCKS5Connector: invalid proxy response");
236                            }
237                            // Port number.
238                            in.skip(2);
239                            // Well done!
240                            connected = true;
241                    } catch (IOException e) {
242                            throw e;
243                    } finally {
244                            if (!connected) {
245                                    if (out != null) {
246                                            try {
247                                                    out.close();
248                                            } catch (Throwable t) {
249                                                    ;
250                                            }
251                                    }
252                                    if (in != null) {
253                                            try {
254                                                    in.close();
255                                            } catch (Throwable t) {
256                                                    ;
257                                            }
258                                    }
259                                    if (socket != null) {
260                                            try {
261                                                    socket.close();
262                                            } catch (Throwable t) {
263                                                    ;
264                                            }
265                                    }
266                            }
267                    }
268                    return new SocketConnection(socket, in, out);
269            }
270    
271            private int read(InputStream in) throws IOException {
272                    int aux = in.read();
273                    if (aux < 0) {
274                            throw new IOException(
275                                            "SOCKS5Connector: connection closed by the proxy");
276                    }
277                    return aux;
278            }
279    
280            public FTPConnection connectForCommunicationChannel(String host, int port)
281                            throws IOException {
282                    return connect(host, port);
283            }
284    
285            public FTPConnection connectForDataTransferChannel(String host, int port)
286                            throws IOException {
287                    return connect(host, port);
288            }
289    
290    }