/**
* 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 {
/**
* Implementation of a simplified SOCKS5 proxy server.
*/
private class Socks5ServerProcess implements Runnable {
/**
* 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 {
final DataOutputStream out = new DataOutputStream(
socket.getOutputStream());
final 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
final byte[] auth = new byte[b];
in.readFully(auth);
final byte[] authMethodSelectionResponse = new byte[2];
authMethodSelectionResponse[0] = (byte) 0x05; // protocol version
// only authentication method 0, no authentication, supported
boolean noAuthMethodFound = false;
for (final byte element : auth) {
if (element == (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
final byte[] connectionRequest = Socks5Utils
.receiveSocks5Message(in);
// extract digest
final String responseDigest = new String(connectionRequest, 5,
connectionRequest[4]);
// return error if digest is not allowed
if (!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
connectionMap.put(responseDigest, socket);
}
@Override
public void run() {
while (true) {
Socket socket = null;
try {
if (serverSocket.isClosed()
|| Thread.currentThread().isInterrupted()) {
return;
}
// accept connection
socket = serverSocket.accept();
// initialize connection
establishConnection(socket);
} catch (final SocketException e) {
/*
* do nothing, if caused by closing the server socket,
* thread will terminate in next loop
*/
} catch (final Exception e) {
try {
if (socket != null) {
socket.close();
}
} catch (final IOException e1) {
/* do nothing */
}
}
}
}
}
/* SOCKS5 proxy singleton */
private static Socks5Proxy socks5Server;
/**
* 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;
}
/* reusable implementation of a SOCKS5 proxy server process */
private final 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() {
serverProcess = new Socks5ServerProcess();
// add default local address
try {
localAddresses.add(InetAddress.getLocalHost().getHostAddress());
} catch (final UnknownHostException e) {
// do nothing
}
}
/**
* 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");
}
localAddresses.add(address);
}
/**
* 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) {
allowedConnections.add(digest);
}
/**
* 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>(
localAddresses));
}
/**
* 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 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 connectionMap.get(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 serverSocket != null;
}
/**
* 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) {
localAddresses.remove(address);
}
/**
* 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) {
allowedConnections.remove(digest);
connectionMap.remove(digest);
}
/**
* 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");
}
localAddresses.clear();
localAddresses.addAll(addresses);
}
/**
* 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) {
final int port = Math.abs(SmackConfiguration
.getLocalSocks5ProxyPort());
for (int i = 0; i < 65535 - port; i++) {
try {
serverSocket = new ServerSocket(port + i);
break;
} catch (final IOException e) {
// port is used, try next one
}
}
} else {
serverSocket = new ServerSocket(
SmackConfiguration.getLocalSocks5ProxyPort());
}
if (serverSocket != null) {
serverThread = new Thread(serverProcess);
serverThread.start();
}
} catch (final 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 {
serverSocket.close();
} catch (final IOException e) {
// do nothing
}
if (serverThread != null && serverThread.isAlive()) {
try {
serverThread.interrupt();
serverThread.join();
} catch (final InterruptedException e) {
// do nothing
}
}
serverThread = null;
serverSocket = null;
}
}