aboutsummaryrefslogtreecommitdiff
path: root/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java')
-rw-r--r--src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java423
1 files changed, 423 insertions, 0 deletions
diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java
new file mode 100644
index 0000000..11ef7a9
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java
@@ -0,0 +1,423 @@
+/**
+ * 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.InetAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smack.XMPPException;
+
+/**
+ * The Socks5Proxy class represents a local SOCKS5 proxy server. It can be enabled/disabled by
+ * setting the <code>localSocks5ProxyEnabled</code> flag in the <code>smack-config.xml</code> or by
+ * invoking {@link SmackConfiguration#setLocalSocks5ProxyEnabled(boolean)}. The proxy is enabled by
+ * default.
+ * <p>
+ * The port of the local SOCKS5 proxy can be configured by setting <code>localSocks5ProxyPort</code>
+ * in the <code>smack-config.xml</code> or by invoking
+ * {@link SmackConfiguration#setLocalSocks5ProxyPort(int)}. Default port is 7777. If you set the
+ * port to a negative value Smack tries to the absolute value and all following until it finds an
+ * open port.
+ * <p>
+ * If your application is running on a machine with multiple network interfaces or if you want to
+ * provide your public address in case you are behind a NAT router, invoke
+ * {@link #addLocalAddress(String)} or {@link #replaceLocalAddresses(List)} to modify the list of
+ * local network addresses used for outgoing SOCKS5 Bytestream requests.
+ * <p>
+ * The local SOCKS5 proxy server refuses all connections except the ones that are explicitly allowed
+ * in the process of establishing a SOCKS5 Bytestream (
+ * {@link Socks5BytestreamManager#establishSession(String)}).
+ * <p>
+ * This Implementation has the following limitations:
+ * <ul>
+ * <li>only supports the no-authentication authentication method</li>
+ * <li>only supports the <code>connect</code> command and will not answer correctly to other
+ * commands</li>
+ * <li>only supports requests with the domain address type and will not correctly answer to requests
+ * with other address types</li>
+ * </ul>
+ * (see <a href="http://tools.ietf.org/html/rfc1928">RFC 1928</a>)
+ *
+ * @author Henning Staib
+ */
+public class Socks5Proxy {
+
+ /* SOCKS5 proxy singleton */
+ private static Socks5Proxy socks5Server;
+
+ /* reusable implementation of a SOCKS5 proxy server process */
+ private Socks5ServerProcess serverProcess;
+
+ /* thread running the SOCKS5 server process */
+ private Thread serverThread;
+
+ /* server socket to accept SOCKS5 connections */
+ private ServerSocket serverSocket;
+
+ /* assigns a connection to a digest */
+ private final Map<String, Socket> connectionMap = new ConcurrentHashMap<String, Socket>();
+
+ /* list of digests connections should be stored */
+ private final List<String> allowedConnections = Collections.synchronizedList(new LinkedList<String>());
+
+ private final Set<String> localAddresses = Collections.synchronizedSet(new LinkedHashSet<String>());
+
+ /**
+ * Private constructor.
+ */
+ private Socks5Proxy() {
+ this.serverProcess = new Socks5ServerProcess();
+
+ // add default local address
+ try {
+ this.localAddresses.add(InetAddress.getLocalHost().getHostAddress());
+ }
+ catch (UnknownHostException e) {
+ // do nothing
+ }
+
+ }
+
+ /**
+ * Returns the local SOCKS5 proxy server.
+ *
+ * @return the local SOCKS5 proxy server
+ */
+ public static synchronized Socks5Proxy getSocks5Proxy() {
+ if (socks5Server == null) {
+ socks5Server = new Socks5Proxy();
+ }
+ if (SmackConfiguration.isLocalSocks5ProxyEnabled()) {
+ socks5Server.start();
+ }
+ return socks5Server;
+ }
+
+ /**
+ * Starts the local SOCKS5 proxy server. If it is already running, this method does nothing.
+ */
+ public synchronized void start() {
+ if (isRunning()) {
+ return;
+ }
+ try {
+ if (SmackConfiguration.getLocalSocks5ProxyPort() < 0) {
+ int port = Math.abs(SmackConfiguration.getLocalSocks5ProxyPort());
+ for (int i = 0; i < 65535 - port; i++) {
+ try {
+ this.serverSocket = new ServerSocket(port + i);
+ break;
+ }
+ catch (IOException e) {
+ // port is used, try next one
+ }
+ }
+ }
+ else {
+ this.serverSocket = new ServerSocket(SmackConfiguration.getLocalSocks5ProxyPort());
+ }
+
+ if (this.serverSocket != null) {
+ this.serverThread = new Thread(this.serverProcess);
+ this.serverThread.start();
+ }
+ }
+ catch (IOException e) {
+ // couldn't setup server
+ System.err.println("couldn't setup local SOCKS5 proxy on port "
+ + SmackConfiguration.getLocalSocks5ProxyPort() + ": " + e.getMessage());
+ }
+ }
+
+ /**
+ * Stops the local SOCKS5 proxy server. If it is not running this method does nothing.
+ */
+ public synchronized void stop() {
+ if (!isRunning()) {
+ return;
+ }
+
+ try {
+ this.serverSocket.close();
+ }
+ catch (IOException e) {
+ // do nothing
+ }
+
+ if (this.serverThread != null && this.serverThread.isAlive()) {
+ try {
+ this.serverThread.interrupt();
+ this.serverThread.join();
+ }
+ catch (InterruptedException e) {
+ // do nothing
+ }
+ }
+ this.serverThread = null;
+ this.serverSocket = null;
+
+ }
+
+ /**
+ * Adds the given address to the list of local network addresses.
+ * <p>
+ * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request.
+ * This may be necessary if your application is running on a machine with multiple network
+ * interfaces or if you want to provide your public address in case you are behind a NAT router.
+ * <p>
+ * The order of the addresses used is determined by the order you add addresses.
+ * <p>
+ * Note that the list of addresses initially contains the address returned by
+ * <code>InetAddress.getLocalHost().getHostAddress()</code>. You can replace the list of
+ * addresses by invoking {@link #replaceLocalAddresses(List)}.
+ *
+ * @param address the local network address to add
+ */
+ public void addLocalAddress(String address) {
+ if (address == null) {
+ throw new IllegalArgumentException("address may not be null");
+ }
+ this.localAddresses.add(address);
+ }
+
+ /**
+ * Removes the given address from the list of local network addresses. This address will then no
+ * longer be used of outgoing SOCKS5 Bytestream requests.
+ *
+ * @param address the local network address to remove
+ */
+ public void removeLocalAddress(String address) {
+ this.localAddresses.remove(address);
+ }
+
+ /**
+ * Returns an unmodifiable list of the local network addresses that will be used for streamhost
+ * candidates of outgoing SOCKS5 Bytestream requests.
+ *
+ * @return unmodifiable list of the local network addresses
+ */
+ public List<String> getLocalAddresses() {
+ return Collections.unmodifiableList(new ArrayList<String>(this.localAddresses));
+ }
+
+ /**
+ * Replaces the list of local network addresses.
+ * <p>
+ * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request and
+ * want to define their order. This may be necessary if your application is running on a machine
+ * with multiple network interfaces or if you want to provide your public address in case you
+ * are behind a NAT router.
+ *
+ * @param addresses the new list of local network addresses
+ */
+ public void replaceLocalAddresses(List<String> addresses) {
+ if (addresses == null) {
+ throw new IllegalArgumentException("list must not be null");
+ }
+ this.localAddresses.clear();
+ this.localAddresses.addAll(addresses);
+
+ }
+
+ /**
+ * Returns the port of the local SOCKS5 proxy server. If it is not running -1 will be returned.
+ *
+ * @return the port of the local SOCKS5 proxy server or -1 if proxy is not running
+ */
+ public int getPort() {
+ if (!isRunning()) {
+ return -1;
+ }
+ return this.serverSocket.getLocalPort();
+ }
+
+ /**
+ * Returns the socket for the given digest. A socket will be returned if the given digest has
+ * been in the list of allowed transfers (see {@link #addTransfer(String)}) while the peer
+ * connected to the SOCKS5 proxy.
+ *
+ * @param digest identifying the connection
+ * @return socket or null if there is no socket for the given digest
+ */
+ protected Socket getSocket(String digest) {
+ return this.connectionMap.get(digest);
+ }
+
+ /**
+ * Add the given digest to the list of allowed transfers. Only connections for allowed transfers
+ * are stored and can be retrieved by invoking {@link #getSocket(String)}. All connections to
+ * the local SOCKS5 proxy that don't contain an allowed digest are discarded.
+ *
+ * @param digest to be added to the list of allowed transfers
+ */
+ protected void addTransfer(String digest) {
+ this.allowedConnections.add(digest);
+ }
+
+ /**
+ * Removes the given digest from the list of allowed transfers. After invoking this method
+ * already stored connections with the given digest will be removed.
+ * <p>
+ * The digest should be removed after establishing the SOCKS5 Bytestream is finished, an error
+ * occurred while establishing the connection or if the connection is not allowed anymore.
+ *
+ * @param digest to be removed from the list of allowed transfers
+ */
+ protected void removeTransfer(String digest) {
+ this.allowedConnections.remove(digest);
+ this.connectionMap.remove(digest);
+ }
+
+ /**
+ * Returns <code>true</code> if the local SOCKS5 proxy server is running, otherwise
+ * <code>false</code>.
+ *
+ * @return <code>true</code> if the local SOCKS5 proxy server is running, otherwise
+ * <code>false</code>
+ */
+ public boolean isRunning() {
+ return this.serverSocket != null;
+ }
+
+ /**
+ * Implementation of a simplified SOCKS5 proxy server.
+ */
+ private class Socks5ServerProcess implements Runnable {
+
+ public void run() {
+ while (true) {
+ Socket socket = null;
+
+ try {
+
+ if (Socks5Proxy.this.serverSocket.isClosed()
+ || Thread.currentThread().isInterrupted()) {
+ return;
+ }
+
+ // accept connection
+ socket = Socks5Proxy.this.serverSocket.accept();
+
+ // initialize connection
+ establishConnection(socket);
+
+ }
+ catch (SocketException e) {
+ /*
+ * do nothing, if caused by closing the server socket, thread will terminate in
+ * next loop
+ */
+ }
+ catch (Exception e) {
+ try {
+ if (socket != null) {
+ socket.close();
+ }
+ }
+ catch (IOException e1) {
+ /* do nothing */
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Negotiates a SOCKS5 connection and stores it on success.
+ *
+ * @param socket connection to the client
+ * @throws XMPPException if client requests a connection in an unsupported way
+ * @throws IOException if a network error occurred
+ */
+ private void establishConnection(Socket socket) throws XMPPException, IOException {
+ DataOutputStream out = new DataOutputStream(socket.getOutputStream());
+ DataInputStream in = new DataInputStream(socket.getInputStream());
+
+ // first byte is version should be 5
+ int b = in.read();
+ if (b != 5) {
+ throw new XMPPException("Only SOCKS5 supported");
+ }
+
+ // second byte number of authentication methods supported
+ b = in.read();
+
+ // read list of supported authentication methods
+ byte[] auth = new byte[b];
+ in.readFully(auth);
+
+ byte[] authMethodSelectionResponse = new byte[2];
+ authMethodSelectionResponse[0] = (byte) 0x05; // protocol version
+
+ // only authentication method 0, no authentication, supported
+ boolean noAuthMethodFound = false;
+ for (int i = 0; i < auth.length; i++) {
+ if (auth[i] == (byte) 0x00) {
+ noAuthMethodFound = true;
+ break;
+ }
+ }
+
+ if (!noAuthMethodFound) {
+ authMethodSelectionResponse[1] = (byte) 0xFF; // no acceptable methods
+ out.write(authMethodSelectionResponse);
+ out.flush();
+ throw new XMPPException("Authentication method not supported");
+ }
+
+ authMethodSelectionResponse[1] = (byte) 0x00; // no-authentication method
+ out.write(authMethodSelectionResponse);
+ out.flush();
+
+ // receive connection request
+ byte[] connectionRequest = Socks5Utils.receiveSocks5Message(in);
+
+ // extract digest
+ String responseDigest = new String(connectionRequest, 5, connectionRequest[4]);
+
+ // return error if digest is not allowed
+ if (!Socks5Proxy.this.allowedConnections.contains(responseDigest)) {
+ connectionRequest[1] = (byte) 0x05; // set return status to 5 (connection refused)
+ out.write(connectionRequest);
+ out.flush();
+
+ throw new XMPPException("Connection is not allowed");
+ }
+
+ connectionRequest[1] = (byte) 0x00; // set return status to 0 (success)
+ out.write(connectionRequest);
+ out.flush();
+
+ // store connection
+ Socks5Proxy.this.connectionMap.put(responseDigest, socket);
+ }
+
+ }
+
+}