| /** |
| * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.jivesoftware.smackx.bytestreams.socks5; |
| |
| import java.io.DataInputStream; |
| import java.io.DataOutputStream; |
| import java.io.IOException; |
| import java.net.InetSocketAddress; |
| import java.net.Socket; |
| import java.net.SocketAddress; |
| import java.util.Arrays; |
| import java.util.concurrent.Callable; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.FutureTask; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.TimeoutException; |
| |
| import org.jivesoftware.smack.XMPPException; |
| import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost; |
| |
| /** |
| * The SOCKS5 client class handles establishing a connection to a SOCKS5 proxy. Connecting to a |
| * SOCKS5 proxy requires authentication. This implementation only supports the no-authentication |
| * authentication method. |
| * |
| * @author Henning Staib |
| */ |
| class Socks5Client { |
| |
| /* stream host containing network settings and name of the SOCKS5 proxy */ |
| protected StreamHost streamHost; |
| |
| /* SHA-1 digest identifying the SOCKS5 stream */ |
| protected String digest; |
| |
| /** |
| * Constructor for a SOCKS5 client. |
| * |
| * @param streamHost containing network settings of the SOCKS5 proxy |
| * @param digest identifying the SOCKS5 Bytestream |
| */ |
| public Socks5Client(StreamHost streamHost, String digest) { |
| this.streamHost = streamHost; |
| this.digest = digest; |
| } |
| |
| /** |
| * Returns the initialized socket that can be used to transfer data between peers via the SOCKS5 |
| * proxy. |
| * |
| * @param timeout timeout to connect to SOCKS5 proxy in milliseconds |
| * @return socket the initialized socket |
| * @throws IOException if initializing the socket failed due to a network error |
| * @throws XMPPException if establishing connection to SOCKS5 proxy failed |
| * @throws TimeoutException if connecting to SOCKS5 proxy timed out |
| * @throws InterruptedException if the current thread was interrupted while waiting |
| */ |
| public Socket getSocket(int timeout) throws IOException, XMPPException, InterruptedException, |
| TimeoutException { |
| |
| // wrap connecting in future for timeout |
| FutureTask<Socket> futureTask = new FutureTask<Socket>(new Callable<Socket>() { |
| |
| public Socket call() throws Exception { |
| |
| // initialize socket |
| Socket socket = new Socket(); |
| SocketAddress socketAddress = new InetSocketAddress(streamHost.getAddress(), |
| streamHost.getPort()); |
| socket.connect(socketAddress); |
| |
| // initialize connection to SOCKS5 proxy |
| if (!establish(socket)) { |
| |
| // initialization failed, close socket |
| socket.close(); |
| throw new XMPPException("establishing connection to SOCKS5 proxy failed"); |
| |
| } |
| |
| return socket; |
| } |
| |
| }); |
| Thread executor = new Thread(futureTask); |
| executor.start(); |
| |
| // get connection to initiator with timeout |
| try { |
| return futureTask.get(timeout, TimeUnit.MILLISECONDS); |
| } |
| catch (ExecutionException e) { |
| Throwable cause = e.getCause(); |
| if (cause != null) { |
| // case exceptions to comply with method signature |
| if (cause instanceof IOException) { |
| throw (IOException) cause; |
| } |
| if (cause instanceof XMPPException) { |
| throw (XMPPException) cause; |
| } |
| } |
| |
| // throw generic IO exception if unexpected exception was thrown |
| throw new IOException("Error while connection to SOCKS5 proxy"); |
| } |
| |
| } |
| |
| /** |
| * Initializes the connection to the SOCKS5 proxy by negotiating authentication method and |
| * requesting a stream for the given digest. Currently only the no-authentication method is |
| * supported by the Socks5Client. |
| * <p> |
| * Returns <code>true</code> if a stream could be established, otherwise <code>false</code>. If |
| * <code>false</code> is returned the given Socket should be closed. |
| * |
| * @param socket connected to a SOCKS5 proxy |
| * @return <code>true</code> if if a stream could be established, otherwise <code>false</code>. |
| * If <code>false</code> is returned the given Socket should be closed. |
| * @throws IOException if a network error occurred |
| */ |
| protected boolean establish(Socket socket) throws IOException { |
| |
| /* |
| * use DataInputStream/DataOutpuStream to assure read and write is completed in a single |
| * statement |
| */ |
| DataInputStream in = new DataInputStream(socket.getInputStream()); |
| DataOutputStream out = new DataOutputStream(socket.getOutputStream()); |
| |
| // authentication negotiation |
| byte[] cmd = new byte[3]; |
| |
| cmd[0] = (byte) 0x05; // protocol version 5 |
| cmd[1] = (byte) 0x01; // number of authentication methods supported |
| cmd[2] = (byte) 0x00; // authentication method: no-authentication required |
| |
| out.write(cmd); |
| out.flush(); |
| |
| byte[] response = new byte[2]; |
| in.readFully(response); |
| |
| // check if server responded with correct version and no-authentication method |
| if (response[0] != (byte) 0x05 || response[1] != (byte) 0x00) { |
| return false; |
| } |
| |
| // request SOCKS5 connection with given address/digest |
| byte[] connectionRequest = createSocks5ConnectRequest(); |
| out.write(connectionRequest); |
| out.flush(); |
| |
| // receive response |
| byte[] connectionResponse; |
| try { |
| connectionResponse = Socks5Utils.receiveSocks5Message(in); |
| } |
| catch (XMPPException e) { |
| return false; // server answered in an unsupported way |
| } |
| |
| // verify response |
| connectionRequest[1] = (byte) 0x00; // set expected return status to 0 |
| return Arrays.equals(connectionRequest, connectionResponse); |
| } |
| |
| /** |
| * Returns a SOCKS5 connection request message. It contains the command "connect", the address |
| * type "domain" and the digest as address. |
| * <p> |
| * (see <a href="http://tools.ietf.org/html/rfc1928">RFC1928</a>) |
| * |
| * @return SOCKS5 connection request message |
| */ |
| private byte[] createSocks5ConnectRequest() { |
| byte addr[] = this.digest.getBytes(); |
| |
| byte[] data = new byte[7 + addr.length]; |
| data[0] = (byte) 0x05; // version (SOCKS5) |
| data[1] = (byte) 0x01; // command (1 - connect) |
| data[2] = (byte) 0x00; // reserved byte (always 0) |
| data[3] = (byte) 0x03; // address type (3 - domain name) |
| data[4] = (byte) addr.length; // address length |
| System.arraycopy(addr, 0, data, 5, addr.length); // address |
| data[data.length - 2] = (byte) 0; // address port (2 bytes always 0) |
| data[data.length - 1] = (byte) 0; |
| |
| return data; |
| } |
| |
| } |