/** * * Copyright the original author or authors * * 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.InterfaceAddress; import java.net.NetworkInterface; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashSet; 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 java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Level; import java.util.logging.Logger; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.util.CloseableUtil; /** * The Socks5Proxy class represents a local SOCKS5 proxy server. It can be enabled/disabled by * invoking {@link #setLocalSocks5ProxyEnabled(boolean)}. The proxy is enabled by default. *
* The port of the local SOCKS5 proxy can be configured by invoking * {@link #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. *
* 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(InetAddress)} or {@link #replaceLocalAddresses(Collection)} to modify the list of * local network addresses used for outgoing SOCKS5 Bytestream requests. *
* 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(org.jxmpp.jid.Jid)}). *
* This Implementation has the following limitations: *
connect
command and will not answer correctly to other
* commandstrue
, then all connections are allowed and the digest is not verified. Should be set to
* false
for production usage and true
for (unit) testing purposes.
*/
private final boolean allowAllConnections;
/**
* Private constructor.
*/
Socks5Proxy() {
this.serverProcess = new Socks5ServerProcess();
allowAllConnections = false;
Enumeration* 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. *
* The order of the addresses used is determined by the order you add addresses. *
* Note that the list of addresses initially contains the address returned by
*
* 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(Collection extends InetAddress> addresses) {
if (addresses == null) {
throw new IllegalArgumentException("list must not be null");
}
synchronized (localAddresses) {
localAddresses.clear();
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
*/
public 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.
*
* 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 InetAddress.getLocalHost().getHostAddress()
. You can replace the list of
* addresses by invoking {@link #replaceLocalAddresses(Collection)}.
*
* @param address the local network address to add
*/
public void addLocalAddress(InetAddress address) {
if (address == null) {
return;
}
synchronized (localAddresses) {
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
* @return true if the address was removed.
*/
public boolean removeLocalAddress(InetAddress address) {
synchronized (localAddresses) {
return localAddresses.remove(address);
}
}
/**
* Returns an set of the local network addresses that will be used for streamhost
* candidates of outgoing SOCKS5 Bytestream requests.
*
* @return set of the local network addresses
*/
public Listtrue
if the local SOCKS5 proxy server is running, otherwise
* false
.
*
* @return true
if the local SOCKS5 proxy server is running, otherwise
* false
*/
public boolean isRunning() {
return this.serverSocket != null;
}
/**
* Implementation of a simplified SOCKS5 proxy server.
*/
private class Socks5ServerProcess implements Runnable {
@Override
public void run() {
while (true) {
ServerSocket serverSocket = Socks5Proxy.this.serverSocket;
if (serverSocket == null || serverSocket.isClosed() || Thread.currentThread().isInterrupted()) {
return;
}
// accept connection
Socket socket = null;
try {
socket = serverSocket.accept();
// initialize connection
establishConnection(socket);
} catch (SmackException | IOException e) {
// Do nothing, if caused by closing the server socket, thread will terminate in next loop.
LOGGER.log(Level.FINE, "Exception while " + Socks5Proxy.this + " was handling connection", e);
CloseableUtil.maybeClose(socket, LOGGER);
}
}
}
/**
* Negotiates a SOCKS5 connection and stores it on success.
*
* @param socket connection to the client
* @throws SmackException if client requests a connection in an unsupported way
* @throws IOException if a network error occurred
*/
private void establishConnection(Socket socket) throws SmackException, 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 SmackException.SmackMessageException("Only SOCKS5 supported: Peer send " + b + " but we expect 5");
}
// 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 SmackException.SmackMessageException("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], StandardCharsets.UTF_8);
// return error if digest is not allowed
if (!allowAllConnections && !Socks5Proxy.this.allowedConnections.contains(responseDigest)) {
connectionRequest[1] = (byte) 0x05; // set return status to 5 (connection refused)
out.write(connectionRequest);
out.flush();
throw new SmackException.SmackMessageException(
"Connection with digest '" + responseDigest + "' is not allowed");
}
// Store the connection before we send the return status.
Socks5Proxy.this.connectionMap.put(responseDigest, socket);
connectionRequest[1] = (byte) 0x00; // set return status to 0 (success)
out.write(connectionRequest);
out.flush();
}
}
public static Socket getSocketForDigest(String digest) {
for (Socks5Proxy socks5Proxy : RUNNING_PROXIES) {
Socket socket = socks5Proxy.getSocket(digest);
if (socket != null) {
return socket;
}
}
return null;
}
static List