mirror of
https://codeberg.org/Mercury-IM/Smack
synced 2024-11-23 23:02:05 +01:00
205 lines
7.7 KiB
Java
205 lines
7.7 KiB
Java
|
/**
|
||
|
* 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;
|
||
|
}
|
||
|
|
||
|
}
|