/* * JLibs: Common Utilities for Java * Copyright (C) 2009 Santhosh Kumar T <santhosh.tekuri@gmail.com> * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package jlibs.nio.listeners; import jlibs.core.io.IOUtil; import jlibs.nio.Reactor; import java.io.IOException; import java.net.Inet6Address; import java.net.InetSocketAddress; import java.net.SocketException; import java.nio.ByteBuffer; import static java.nio.channels.SelectionKey.OP_WRITE; import static jlibs.nio.listeners.Socks5Tunnel.Step.*; /** * @author Santhosh Kumar Tekuri */ public class Socks5Tunnel extends Task{ private static final byte VERSION = 5; private static final byte NO_AUTH = 0; private static final byte USER_PASSWORD = 2; private static final byte NO_METHODS = -1; private static final byte CONNECT = 1; private static final byte IPV4 = 1; private static final byte DOMAIN_NAME = 3; private static final byte IPV6 = 4; private static final byte REQUEST_OK = 0; private static final byte GENERAL_FAILURE = 1; private static final byte NOT_ALLOWED = 2; private static final byte NET_UNREACHABLE = 3; private static final byte HOST_UNREACHABLE = 4; private static final byte CONN_REFUSED = 5; private static final byte TTL_EXPIRED = 6; private static final byte CMD_NOT_SUPPORTED = 7; private static final byte ADDR_TYPE_NOT_SUP = 8; private ByteBuffer buffer = Reactor.current().allocator.allocate(); private String user; private String password; private InetSocketAddress endpoint; public Socks5Tunnel(String user, String password, InetSocketAddress endpoint){ super(OP_WRITE); this.user = user; this.password = password; this.endpoint = endpoint; buffer.put(VERSION); buffer.put((byte)2); // number of authentication methods supported buffer.put(NO_AUTH); buffer.put(USER_PASSWORD); buffer.flip(); } enum Step{ SEND_GREETING, READ_SERVER_CHOICE, SEND_AUTHENTICATION, READ_AUTHENTICATION_RESULT, // if authentication needed PREPARE_CONNECT_REQUEST, SEND_CONNECT_REQUEST, READ_CONNECT_REQUEST, READ_DESTINATION_ADDRESS } private Step step = SEND_GREETING; @Override protected boolean process(int readyOp) throws IOException{ while(true){ switch(step){ case SEND_GREETING: if(!send(buffer)) return false; buffer.position(0); buffer.limit(2); step = READ_SERVER_CHOICE; case READ_SERVER_CHOICE: if(!read(buffer)) return false; buffer.flip(); if(buffer.remaining()!=2 || buffer.get()!=VERSION){ setChild(new Socks4Tunnel(user, endpoint)); return true; } byte authenticationType = buffer.get(); if(authenticationType==NO_METHODS) throw new SocketException("SOCKS: No acceptable methods"); if(authenticationType==NO_AUTH){ step = PREPARE_CONNECT_REQUEST; break; } if(authenticationType!=USER_PASSWORD) // Username/Password throw new SocketException("SOCKS: unsupported authentication"); buffer.clear(); buffer.put((byte)1); if(user==null) user = System.getProperty("user.name"); buffer.put((byte)user.length()); buffer.put(user.getBytes(IOUtil.ISO_8859_1)); if(password!=null) { buffer.put((byte)password.length()); buffer.put(password.getBytes(IOUtil.ISO_8859_1)); } else buffer.put((byte)0); buffer.flip(); step = SEND_AUTHENTICATION; case SEND_AUTHENTICATION: if(!send(buffer)) return false; buffer.position(0); buffer.limit(2); step = READ_AUTHENTICATION_RESULT; case READ_AUTHENTICATION_RESULT: if(!read(buffer)) return false; buffer.flip(); if(buffer.remaining()!=2 || buffer.get(1)!=0) throw new SocketException("SOCKS: authentication failed"); step = PREPARE_CONNECT_REQUEST; case PREPARE_CONNECT_REQUEST: buffer.clear(); buffer.put(VERSION); // SOCKS version number buffer.put(CONNECT); // CONNECT command code buffer.put((byte)0); if(endpoint.isUnresolved()){ buffer.put(DOMAIN_NAME); buffer.put((byte)endpoint.getHostName().length()); buffer.put(endpoint.getHostName().getBytes(IOUtil.ISO_8859_1)); }else{ buffer.put(endpoint.getAddress() instanceof Inet6Address ? IPV6 : IPV4); buffer.put(endpoint.getAddress().getAddress()); } buffer.put((byte)((endpoint.getPort()>>8)&0xff)); buffer.put((byte)(endpoint.getPort()&0xff)); buffer.flip(); step = SEND_CONNECT_REQUEST; case SEND_CONNECT_REQUEST: if(!send(buffer)) return false; buffer.position(0); buffer.limit(4); step = READ_CONNECT_REQUEST; case READ_CONNECT_REQUEST: if(!read(buffer)) return false; buffer.flip(); if(buffer.remaining()!=4) throw new SocketException("Reply from SOCKS server has bad length"); switch(buffer.get(1)){ case GENERAL_FAILURE: throw new SocketException("SOCKS server general failure"); case NOT_ALLOWED: throw new SocketException("SOCKS: Connection not allowed by ruleset"); case NET_UNREACHABLE: throw new SocketException("SOCKS: Network unreachable"); case HOST_UNREACHABLE: throw new SocketException("SOCKS: Host unreachable"); case CONN_REFUSED: throw new SocketException("SOCKS: Connection refused"); case TTL_EXPIRED: throw new SocketException("SOCKS: TTL expired"); case CMD_NOT_SUPPORTED: throw new SocketException("SOCKS: Command not supported"); case ADDR_TYPE_NOT_SUP: throw new SocketException("SOCKS: address type not supported"); case REQUEST_OK: int len; switch(buffer.get(3)){ case IPV4: len = 4; break; case IPV6: case DOMAIN_NAME: len = buffer.get(1); break; default: throw new SocketException("Reply from SOCKS server contains wrong code"); } buffer.position(0); buffer.limit(len+2); step = READ_DESTINATION_ADDRESS; } case READ_DESTINATION_ADDRESS: if(!read(buffer)) return false; if(buffer.hasRemaining()) throw new SocketException("Reply from SOCKS server badly formatted"); return true; } } } @Override protected void cleanup(Throwable thr){ Reactor.current().allocator.free(buffer); buffer = null; } }