Shuyi Chen | d7955ce | 2013-05-22 14:51:55 -0700 | [diff] [blame] | 1 | /**
|
| 2 | * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
|
| 3 | * you may not use this file except in compliance with the License.
|
| 4 | * You may obtain a copy of the License at
|
| 5 | *
|
| 6 | * http://www.apache.org/licenses/LICENSE-2.0
|
| 7 | *
|
| 8 | * Unless required by applicable law or agreed to in writing, software
|
| 9 | * distributed under the License is distributed on an "AS IS" BASIS,
|
| 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 11 | * See the License for the specific language governing permissions and
|
| 12 | * limitations under the License.
|
| 13 | */
|
| 14 | package org.jivesoftware.smackx.bytestreams.socks5;
|
| 15 |
|
| 16 | import java.io.DataInputStream;
|
| 17 | import java.io.DataOutputStream;
|
| 18 | import java.io.IOException;
|
| 19 | import java.net.InetSocketAddress;
|
| 20 | import java.net.Socket;
|
| 21 | import java.net.SocketAddress;
|
| 22 | import java.util.Arrays;
|
| 23 | import java.util.concurrent.Callable;
|
| 24 | import java.util.concurrent.ExecutionException;
|
| 25 | import java.util.concurrent.FutureTask;
|
| 26 | import java.util.concurrent.TimeUnit;
|
| 27 | import java.util.concurrent.TimeoutException;
|
| 28 |
|
| 29 | import org.jivesoftware.smack.XMPPException;
|
| 30 | import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;
|
| 31 |
|
| 32 | /**
|
| 33 | * The SOCKS5 client class handles establishing a connection to a SOCKS5 proxy. Connecting to a
|
| 34 | * SOCKS5 proxy requires authentication. This implementation only supports the no-authentication
|
| 35 | * authentication method.
|
| 36 | *
|
| 37 | * @author Henning Staib
|
| 38 | */
|
| 39 | class Socks5Client {
|
| 40 |
|
| 41 | /* stream host containing network settings and name of the SOCKS5 proxy */
|
| 42 | protected StreamHost streamHost;
|
| 43 |
|
| 44 | /* SHA-1 digest identifying the SOCKS5 stream */
|
| 45 | protected String digest;
|
| 46 |
|
| 47 | /**
|
| 48 | * Constructor for a SOCKS5 client.
|
| 49 | *
|
| 50 | * @param streamHost containing network settings of the SOCKS5 proxy
|
| 51 | * @param digest identifying the SOCKS5 Bytestream
|
| 52 | */
|
| 53 | public Socks5Client(StreamHost streamHost, String digest) {
|
| 54 | this.streamHost = streamHost;
|
| 55 | this.digest = digest;
|
| 56 | }
|
| 57 |
|
| 58 | /**
|
| 59 | * Returns the initialized socket that can be used to transfer data between peers via the SOCKS5
|
| 60 | * proxy.
|
| 61 | *
|
| 62 | * @param timeout timeout to connect to SOCKS5 proxy in milliseconds
|
| 63 | * @return socket the initialized socket
|
| 64 | * @throws IOException if initializing the socket failed due to a network error
|
| 65 | * @throws XMPPException if establishing connection to SOCKS5 proxy failed
|
| 66 | * @throws TimeoutException if connecting to SOCKS5 proxy timed out
|
| 67 | * @throws InterruptedException if the current thread was interrupted while waiting
|
| 68 | */
|
| 69 | public Socket getSocket(int timeout) throws IOException, XMPPException, InterruptedException,
|
| 70 | TimeoutException {
|
| 71 |
|
| 72 | // wrap connecting in future for timeout
|
| 73 | FutureTask<Socket> futureTask = new FutureTask<Socket>(new Callable<Socket>() {
|
| 74 |
|
| 75 | public Socket call() throws Exception {
|
| 76 |
|
| 77 | // initialize socket
|
| 78 | Socket socket = new Socket();
|
| 79 | SocketAddress socketAddress = new InetSocketAddress(streamHost.getAddress(),
|
| 80 | streamHost.getPort());
|
| 81 | socket.connect(socketAddress);
|
| 82 |
|
| 83 | // initialize connection to SOCKS5 proxy
|
| 84 | if (!establish(socket)) {
|
| 85 |
|
| 86 | // initialization failed, close socket
|
| 87 | socket.close();
|
| 88 | throw new XMPPException("establishing connection to SOCKS5 proxy failed");
|
| 89 |
|
| 90 | }
|
| 91 |
|
| 92 | return socket;
|
| 93 | }
|
| 94 |
|
| 95 | });
|
| 96 | Thread executor = new Thread(futureTask);
|
| 97 | executor.start();
|
| 98 |
|
| 99 | // get connection to initiator with timeout
|
| 100 | try {
|
| 101 | return futureTask.get(timeout, TimeUnit.MILLISECONDS);
|
| 102 | }
|
| 103 | catch (ExecutionException e) {
|
| 104 | Throwable cause = e.getCause();
|
| 105 | if (cause != null) {
|
| 106 | // case exceptions to comply with method signature
|
| 107 | if (cause instanceof IOException) {
|
| 108 | throw (IOException) cause;
|
| 109 | }
|
| 110 | if (cause instanceof XMPPException) {
|
| 111 | throw (XMPPException) cause;
|
| 112 | }
|
| 113 | }
|
| 114 |
|
| 115 | // throw generic IO exception if unexpected exception was thrown
|
| 116 | throw new IOException("Error while connection to SOCKS5 proxy");
|
| 117 | }
|
| 118 |
|
| 119 | }
|
| 120 |
|
| 121 | /**
|
| 122 | * Initializes the connection to the SOCKS5 proxy by negotiating authentication method and
|
| 123 | * requesting a stream for the given digest. Currently only the no-authentication method is
|
| 124 | * supported by the Socks5Client.
|
| 125 | * <p>
|
| 126 | * Returns <code>true</code> if a stream could be established, otherwise <code>false</code>. If
|
| 127 | * <code>false</code> is returned the given Socket should be closed.
|
| 128 | *
|
| 129 | * @param socket connected to a SOCKS5 proxy
|
| 130 | * @return <code>true</code> if if a stream could be established, otherwise <code>false</code>.
|
| 131 | * If <code>false</code> is returned the given Socket should be closed.
|
| 132 | * @throws IOException if a network error occurred
|
| 133 | */
|
| 134 | protected boolean establish(Socket socket) throws IOException {
|
| 135 |
|
| 136 | /*
|
| 137 | * use DataInputStream/DataOutpuStream to assure read and write is completed in a single
|
| 138 | * statement
|
| 139 | */
|
| 140 | DataInputStream in = new DataInputStream(socket.getInputStream());
|
| 141 | DataOutputStream out = new DataOutputStream(socket.getOutputStream());
|
| 142 |
|
| 143 | // authentication negotiation
|
| 144 | byte[] cmd = new byte[3];
|
| 145 |
|
| 146 | cmd[0] = (byte) 0x05; // protocol version 5
|
| 147 | cmd[1] = (byte) 0x01; // number of authentication methods supported
|
| 148 | cmd[2] = (byte) 0x00; // authentication method: no-authentication required
|
| 149 |
|
| 150 | out.write(cmd);
|
| 151 | out.flush();
|
| 152 |
|
| 153 | byte[] response = new byte[2];
|
| 154 | in.readFully(response);
|
| 155 |
|
| 156 | // check if server responded with correct version and no-authentication method
|
| 157 | if (response[0] != (byte) 0x05 || response[1] != (byte) 0x00) {
|
| 158 | return false;
|
| 159 | }
|
| 160 |
|
| 161 | // request SOCKS5 connection with given address/digest
|
| 162 | byte[] connectionRequest = createSocks5ConnectRequest();
|
| 163 | out.write(connectionRequest);
|
| 164 | out.flush();
|
| 165 |
|
| 166 | // receive response
|
| 167 | byte[] connectionResponse;
|
| 168 | try {
|
| 169 | connectionResponse = Socks5Utils.receiveSocks5Message(in);
|
| 170 | }
|
| 171 | catch (XMPPException e) {
|
| 172 | return false; // server answered in an unsupported way
|
| 173 | }
|
| 174 |
|
| 175 | // verify response
|
| 176 | connectionRequest[1] = (byte) 0x00; // set expected return status to 0
|
| 177 | return Arrays.equals(connectionRequest, connectionResponse);
|
| 178 | }
|
| 179 |
|
| 180 | /**
|
| 181 | * Returns a SOCKS5 connection request message. It contains the command "connect", the address
|
| 182 | * type "domain" and the digest as address.
|
| 183 | * <p>
|
| 184 | * (see <a href="http://tools.ietf.org/html/rfc1928">RFC1928</a>)
|
| 185 | *
|
| 186 | * @return SOCKS5 connection request message
|
| 187 | */
|
| 188 | private byte[] createSocks5ConnectRequest() {
|
| 189 | byte addr[] = this.digest.getBytes();
|
| 190 |
|
| 191 | byte[] data = new byte[7 + addr.length];
|
| 192 | data[0] = (byte) 0x05; // version (SOCKS5)
|
| 193 | data[1] = (byte) 0x01; // command (1 - connect)
|
| 194 | data[2] = (byte) 0x00; // reserved byte (always 0)
|
| 195 | data[3] = (byte) 0x03; // address type (3 - domain name)
|
| 196 | data[4] = (byte) addr.length; // address length
|
| 197 | System.arraycopy(addr, 0, data, 5, addr.length); // address
|
| 198 | data[data.length - 2] = (byte) 0; // address port (2 bytes always 0)
|
| 199 | data[data.length - 1] = (byte) 0;
|
| 200 |
|
| 201 | return data;
|
| 202 | }
|
| 203 |
|
| 204 | }
|