package net.sourceforge.jsocks;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PushbackInputStream;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.NoRouteToHostException;
import java.net.ServerSocket;
import java.net.Socket;
import net.sourceforge.jsocks.server.ServerAuthenticator;
/**
* SOCKS4 and SOCKS5 proxy, handles both protocols simultaniously. Implements
* all SOCKS commands, including UDP relaying.
* <p>
* In order to use it you will need to implement ServerAuthenticator interface.
* There is an implementation of this interface which does no authentication
* ServerAuthenticatorNone, but it is very dangerous to use, as it will give
* access to your local network to anybody in the world. One should never use
* this authentication scheme unless one have pretty good reason to do so. There
* is a couple of other authentication schemes in socks.server package.
*
* @see socks.server.ServerAuthenticator
*/
public class ProxyServer implements Runnable {
ServerAuthenticator auth;
ProxyMessage msg = null;
Socket sock = null, remote_sock = null;
ServerSocket ss = null;
UDPRelayServer relayServer = null;
InputStream in, remote_in;
OutputStream out, remote_out;
int mode;
static final int START_MODE = 0;
static final int ACCEPT_MODE = 1;
static final int PIPE_MODE = 2;
static final int ABORT_MODE = 3;
static final int BUF_SIZE = 8192;
Thread pipe_thread1, pipe_thread2;
long lastReadTime;
protected static int iddleTimeout = 180000; // 3 minutes
static int acceptTimeout = 180000; // 3 minutes
static PrintStream log = null;
static Proxy proxy;
// Public Constructors
// ///////////////////
static final String command_names[] = { "CONNECT", "BIND", "UDP_ASSOCIATE" };
// Other constructors
// //////////////////
static final String command2String(int cmd) {
if (cmd > 0 && cmd < 4)
return command_names[cmd - 1];
else
return "Unknown Command " + cmd;
}
// Public methods
// ///////////////
/**
* Get proxy.
*
* @return Proxy wich is used to handle user requests.
*/
public static Proxy getProxy() {
return proxy;
}
static final void log(ProxyMessage msg) {
log("Request version:" + msg.version + "\tCommand: "
+ command2String(msg.command));
log("IP:" + msg.ip + "\tPort:" + msg.port
+ (msg.version == 4 ? "\tUser:" + msg.user : ""));
}
static final void log(String s) {
if (log != null) {
log.println(s);
log.flush();
}
}
/**
* Sets the timeout for BIND command, how long the server should wait for
* the incoming connection.<br>
* Zero timeout implies infinity.<br>
* Default timeout is 3 minutes.
*/
public static void setAcceptTimeout(int timeout) {
acceptTimeout = timeout;
}
/**
* Sets the size of the datagrams used in the UDPRelayServer.<br>
* Default size is 64K, a bit more than maximum possible size of the
* datagram.
*/
public static void setDatagramSize(int size) {
UDPRelayServer.setDatagramSize(size);
}
/**
* Sets the timeout for connections, how long shoud server wait for data to
* arrive before dropping the connection.<br>
* Zero timeout implies infinity.<br>
* Default timeout is 3 minutes.
*/
public static void setIddleTimeout(int timeout) {
iddleTimeout = timeout;
}
/**
* Set the logging stream. Specifying null disables logging.
*/
public static void setLog(OutputStream out) {
if (out == null) {
log = null;
} else {
log = new PrintStream(out, true);
}
UDPRelayServer.log = log;
}
/**
* Set proxy.
* <p>
* Allows Proxy chaining so that one Proxy server is connected to another
* and so on. If proxy supports SOCKSv4, then only some SOCKSv5 requests can
* be handled, UDP would not work, however CONNECT and BIND will be
* translated.
*
* @param p
* Proxy which should be used to handle user requests.
*/
public static void setProxy(Proxy p) {
proxy = p;
UDPRelayServer.proxy = proxy;
}
/**
* Sets the timeout for UDPRelay server.<br>
* Zero timeout implies infinity.<br>
* Default timeout is 3 minutes.
*/
public static void setUDPTimeout(int timeout) {
UDPRelayServer.setTimeout(timeout);
}
/**
* Creates a proxy server with given Authentication scheme.
*
* @param auth
* Authentication scheme to be used.
*/
public ProxyServer(ServerAuthenticator auth) {
this.auth = auth;
}
protected ProxyServer(ServerAuthenticator auth, Socket s) {
this.auth = auth;
this.sock = s;
mode = START_MODE;
}
private synchronized void abort() {
if (mode == ABORT_MODE)
return;
mode = ABORT_MODE;
try {
log("Aborting operation");
if (remote_sock != null)
remote_sock.close();
if (sock != null)
sock.close();
if (relayServer != null)
relayServer.stop();
if (ss != null)
ss.close();
if (pipe_thread1 != null)
pipe_thread1.interrupt();
if (pipe_thread2 != null)
pipe_thread2.interrupt();
} catch (IOException ioe) {
}
}
private void doAccept() throws IOException {
Socket s;
long startTime = System.currentTimeMillis();
while (true) {
s = ss.accept();
if (s.getInetAddress().equals(msg.ip)) {
// got the connection from the right host
// Close listenning socket.
ss.close();
break;
} else if (ss instanceof SocksServerSocket) {
// We can't accept more then one connection
s.close();
ss.close();
throw new SocksException(Proxy.SOCKS_FAILURE);
} else {
if (acceptTimeout != 0) { // If timeout is not infinit
int newTimeout = acceptTimeout
- (int) (System.currentTimeMillis() - startTime);
if (newTimeout <= 0)
throw new InterruptedIOException("In doAccept()");
ss.setSoTimeout(newTimeout);
}
s.close(); // Drop all connections from other hosts
}
}
// Accepted connection
remote_sock = s;
remote_in = s.getInputStream();
remote_out = s.getOutputStream();
// Set timeout
remote_sock.setSoTimeout(iddleTimeout);
log("Accepted from " + s.getInetAddress() + ":" + s.getPort());
ProxyMessage response;
if (msg.version == 5)
response = new Socks5Message(Proxy.SOCKS_SUCCESS,
s.getInetAddress(), s.getPort());
else
response = new Socks4Message(Socks4Message.REPLY_OK,
s.getInetAddress(), s.getPort());
response.write(out);
}
private void handleException(IOException ioe) {
// If we couldn't read the request, return;
if (msg == null)
return;
// If have been aborted by other thread
if (mode == ABORT_MODE)
return;
// If the request was successfully completed, but exception happened
// later
if (mode == PIPE_MODE)
return;
int error_code = Proxy.SOCKS_FAILURE;
if (ioe instanceof SocksException)
error_code = ((SocksException) ioe).errCode;
else if (ioe instanceof NoRouteToHostException)
error_code = Proxy.SOCKS_HOST_UNREACHABLE;
else if (ioe instanceof ConnectException)
error_code = Proxy.SOCKS_CONNECTION_REFUSED;
else if (ioe instanceof InterruptedIOException)
error_code = Proxy.SOCKS_TTL_EXPIRE;
if (error_code > Proxy.SOCKS_ADDR_NOT_SUPPORTED || error_code < 0) {
error_code = Proxy.SOCKS_FAILURE;
}
sendErrorMessage(error_code);
}
protected void handleRequest(ProxyMessage msg) throws IOException {
if (!auth.checkRequest(msg))
throw new SocksException(Proxy.SOCKS_FAILURE);
if (msg.ip == null) {
if (msg instanceof Socks5Message) {
msg.ip = InetAddress.getByName(msg.host);
} else
throw new SocksException(Proxy.SOCKS_FAILURE);
}
log(msg);
switch (msg.command) {
case Proxy.SOCKS_CMD_CONNECT:
onConnect(msg);
break;
case Proxy.SOCKS_CMD_BIND:
onBind(msg);
break;
case Proxy.SOCKS_CMD_UDP_ASSOCIATE:
onUDP(msg);
break;
default:
throw new SocksException(Proxy.SOCKS_CMD_NOT_SUPPORTED);
}
}
private void onBind(ProxyMessage msg) throws IOException {
ProxyMessage response = null;
if (proxy == null)
ss = new ServerSocket(0);
else
ss = new SocksServerSocket(proxy, msg.ip, msg.port);
ss.setSoTimeout(acceptTimeout);
log("Trying accept on " + ss.getInetAddress() + ":" + ss.getLocalPort());
if (msg.version == 5)
response = new Socks5Message(Proxy.SOCKS_SUCCESS,
ss.getInetAddress(), ss.getLocalPort());
else
response = new Socks4Message(Socks4Message.REPLY_OK,
ss.getInetAddress(), ss.getLocalPort());
response.write(out);
mode = ACCEPT_MODE;
pipe_thread1 = Thread.currentThread();
pipe_thread2 = new Thread(this);
pipe_thread2.start();
// Make timeout infinit.
sock.setSoTimeout(0);
int eof = 0;
try {
while ((eof = in.read()) >= 0) {
if (mode != ACCEPT_MODE) {
if (mode != PIPE_MODE)
return;// Accept failed
remote_out.write(eof);
break;
}
}
} catch (EOFException eofe) {
// System.out.println("EOF exception");
return;// Connection closed while we were trying to accept.
} catch (InterruptedIOException iioe) {
// Accept thread interrupted us.
// System.out.println("Interrupted");
if (mode != PIPE_MODE)
return;// If accept thread was not successfull return.
} finally {
// System.out.println("Finnaly!");
}
if (eof < 0)// Connection closed while we were trying to accept;
return;
// Do not restore timeout, instead timeout is set on the
// remote socket. It does not make any difference.
pipe(in, remote_out);
}
private void onConnect(ProxyMessage msg) throws IOException {
Socket s;
ProxyMessage response = null;
s = new Socket(msg.ip, msg.port);
log("Connected to " + s.getInetAddress() + ":" + s.getPort());
if (msg instanceof Socks5Message) {
response = new Socks5Message(Proxy.SOCKS_SUCCESS,
s.getLocalAddress(), s.getLocalPort());
} else {
response = new Socks4Message(Socks4Message.REPLY_OK,
s.getLocalAddress(), s.getLocalPort());
}
response.write(out);
startPipe(s);
}
// Private methods
// ////////////////
private void onUDP(ProxyMessage msg) throws IOException {
if (msg.ip.getHostAddress().equals("0.0.0.0"))
msg.ip = sock.getInetAddress();
log("Creating UDP relay server for " + msg.ip + ":" + msg.port);
relayServer = new UDPRelayServer(msg.ip, msg.port,
Thread.currentThread(), sock, auth);
ProxyMessage response;
response = new Socks5Message(Proxy.SOCKS_SUCCESS, relayServer.relayIP,
relayServer.relayPort);
response.write(out);
relayServer.start();
// Make timeout infinit.
sock.setSoTimeout(0);
try {
while (in.read() >= 0)
/* do nothing */;
} catch (EOFException eofe) {
}
}
private void pipe(InputStream in, OutputStream out) throws IOException {
lastReadTime = System.currentTimeMillis();
byte[] buf = new byte[BUF_SIZE];
int len = 0;
while (len >= 0) {
try {
if (len != 0) {
out.write(buf, 0, len);
out.flush();
}
len = in.read(buf);
lastReadTime = System.currentTimeMillis();
} catch (InterruptedIOException iioe) {
if (iddleTimeout == 0)
return;// Other thread interrupted us.
long timeSinceRead = System.currentTimeMillis() - lastReadTime;
if (timeSinceRead >= iddleTimeout - 1000) // -1s for adjustment.
return;
len = 0;
}
}
}
protected ProxyMessage readMsg(InputStream in) throws IOException {
PushbackInputStream push_in;
if (in instanceof PushbackInputStream)
push_in = (PushbackInputStream) in;
else
push_in = new PushbackInputStream(in);
int version = push_in.read();
push_in.unread(version);
ProxyMessage msg;
if (version == 5) {
msg = new Socks5Message(push_in, false);
} else if (version == 4) {
msg = new Socks4Message(push_in, false);
} else {
throw new SocksException(Proxy.SOCKS_FAILURE);
}
return msg;
}
// Runnable interface
// //////////////////
@Override
public void run() {
switch (mode) {
case START_MODE:
try {
startSession();
} catch (IOException ioe) {
handleException(ioe);
// ioe.printStackTrace();
} finally {
abort();
if (auth != null)
auth.endSession();
log("Main thread(client->remote)stopped.");
}
break;
case ACCEPT_MODE:
try {
doAccept();
mode = PIPE_MODE;
pipe_thread1.interrupt(); // Tell other thread that connection
// have
// been accepted.
pipe(remote_in, out);
} catch (IOException ioe) {
// log("Accept exception:"+ioe);
handleException(ioe);
} finally {
abort();
log("Accept thread(remote->client) stopped");
}
break;
case PIPE_MODE:
try {
pipe(remote_in, out);
} catch (IOException ioe) {
} finally {
abort();
log("Support thread(remote->client) stopped");
}
break;
case ABORT_MODE:
break;
default:
log("Unexpected MODE " + mode);
}
}
private void sendErrorMessage(int error_code) {
ProxyMessage err_msg;
if (msg instanceof Socks4Message)
err_msg = new Socks4Message(Socks4Message.REPLY_REJECTED);
else
err_msg = new Socks5Message(error_code);
try {
err_msg.write(out);
} catch (IOException ioe) {
}
}
/**
* Start the Proxy server at given port.<br>
* This methods blocks.
*/
public void start(int port) {
start(port, 5, null);
}
/**
* Create a server with the specified port, listen backlog, and local IP
* address to bind to. The localIP argument can be used on a multi-homed
* host for a ServerSocket that will only accept connect requests to one of
* its addresses. If localIP is null, it will default accepting connections
* on any/all local addresses. The port must be between 0 and 65535,
* inclusive. <br>
* This methods blocks.
*/
public void start(int port, int backlog, InetAddress localIP) {
try {
ss = new ServerSocket(port, backlog, localIP);
log("Starting SOCKS Proxy on:"
+ ss.getInetAddress().getHostAddress() + ":"
+ ss.getLocalPort());
while (true) {
Socket s = ss.accept();
log("Accepted from:" + s.getInetAddress().getHostName() + ":"
+ s.getPort());
ProxyServer ps = new ProxyServer(auth, s);
(new Thread(ps)).start();
}
} catch (IOException ioe) {
ioe.printStackTrace();
} finally {
}
}
private void startPipe(Socket s) {
mode = PIPE_MODE;
remote_sock = s;
try {
remote_in = s.getInputStream();
remote_out = s.getOutputStream();
pipe_thread1 = Thread.currentThread();
pipe_thread2 = new Thread(this);
pipe_thread2.start();
pipe(in, remote_out);
} catch (IOException ioe) {
}
}
// Private methods
// ///////////////
private void startSession() throws IOException {
sock.setSoTimeout(iddleTimeout);
try {
auth = auth.startSession(sock);
} catch (IOException ioe) {
log("Auth throwed exception:" + ioe);
auth = null;
return;
}
if (auth == null) { // Authentication failed
log("Authentication failed");
return;
}
in = auth.getInputStream();
out = auth.getOutputStream();
msg = readMsg(in);
handleRequest(msg);
}
/**
* Stop server operation.It would be wise to interrupt thread running the
* server afterwards.
*/
public void stop() {
try {
if (ss != null)
ss.close();
} catch (IOException ioe) {
}
}
}