package uc.protocols;
import helpers.GH;
import helpers.PreferenceChangedAdapter;
import java.io.IOException;
import java.net.BindException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.concurrent.CopyOnWriteArraySet;
import logger.LoggerFactory;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import uc.PI;
import uc.protocols.MultiStandardConnection.IUnblocking;
/**
* encapsulation for Socks5 protocol..
*
*
* @author Quicksilver
*
*/
public class Socks {
private static final Logger logger = LoggerFactory.make(Level.DEBUG);
private static Socks defaultSocks;
private static final CopyOnWriteArraySet<UDPRelay> RELAYS = new CopyOnWriteArraySet<UDPRelay>();
static {
updateSocks();
new PreferenceChangedAdapter(PI.get(),PI.socksProxyEnabled,PI.socksProxyHost,PI.socksProxyPassword,PI.socksProxyPort,PI.socksProxyUsername) {
@Override
public void preferenceChanged(String preference, String oldValue,
String newValue) {
updateSocks();
}
};
}
private synchronized static void updateSocks() {
for (UDPRelay u:RELAYS) {
u.close();
}
boolean e = PI.getBoolean(PI.socksProxyEnabled) && !GH.isEmpty(PI.get(PI.socksProxyHost));
System.setProperty("socksProxyHost", e ? PI.get(PI.socksProxyHost): "");
System.setProperty("socksProxyPort", e ? PI.get(PI.socksProxyPort): "");
System.setProperty("java.net.socks.username", e? PI.get(PI.socksProxyUsername):"");
System.setProperty("java.net.socks.password", e? PI.get(PI.socksProxyPassword):"");
if (e) {
InetSocketAddress proxy = new InetSocketAddress(
PI.get(PI.socksProxyHost),PI.getInt(PI.socksProxyPort));
defaultSocks = new Socks(PI.get(PI.socksProxyUsername),PI.get(PI.socksProxyPassword),proxy);
} else {
defaultSocks = null;
}
}
/**
*
* @return an instance of Socks as set in the jucy settings,
* or null if using Socks is currently disabled..
*/
public synchronized static Socks getDefaultSocks() {
return defaultSocks;
}
public synchronized static boolean isEnabled() {
return defaultSocks != null;
}
private static final byte NO_AUTH = 0x00, USERNAME_PW = 0x02;
private final String username;
private final String password;
private final InetSocketAddress socksServerAddy;
public Socks(String username,String password, InetSocketAddress socksServer) {
this.username = username;
this.password = password;
this.socksServerAddy = socksServer;
}
/**
* creates connection to a specified target address
*
* @param target - where we want that our data go to
* @param socksServer - a connection to an existing Server
* @return
*/
public void connect(SocketChannel unconnectedSocket,InetSocketAddress target) throws IOException {
if (!unconnectedSocket.isBlocking()) {
unconnectedSocket.configureBlocking(true);
}
connectAndAuth( unconnectedSocket );
InetAddress ia = target.getAddress();
request(unconnectedSocket,CMD.CONNECT,ia,target.getPort());
readResponse(unconnectedSocket,ia);
//if we reach here... then all went well
}
public UDPRelay openUDPRelay() throws IOException {
UDPRelay relay = new UDPRelay();
relay.connect();
return relay;
}
private void connectAndAuth(SocketChannel unconnectedSocket) throws IOException {
unconnectedSocket.socket().setSoTimeout(5000);
unconnectedSocket.connect(socksServerAddy);
// byte[] b;
int read;
boolean pAuth = !GH.isNullOrEmpty(username) && !GH.isNullOrEmpty(password);
ByteBuffer auth = ByteBuffer.allocate(4);
auth.put((byte)0x05).put((byte)(pAuth?0x02:0x01)).put(NO_AUTH);
if (pAuth) { // start hello
auth.put(USERNAME_PW);
}
auth.flip();
unconnectedSocket.write(auth);
ByteBuffer methodSelected = ByteBuffer.allocate(2); // new byte[2];
read = unconnectedSocket.read(methodSelected);
if (read != 2) {
throw new IOException();
}
if (methodSelected.get(0) == 5) {
switch(methodSelected.get(1)) {
case NO_AUTH:
break;
case USERNAME_PW:
/*
* The client's authentication request is:
* field 1: version number, 1 byte (must be 0x01)
* field 2: username length, 1 byte
* field 3: username
* field 4: password length, 1 byte
* field 5: password
*/
if (pAuth) {
byte[] usr = username.getBytes();
byte[] pass= password.getBytes();
ByteBuffer buf = ByteBuffer.allocate(usr.length+pass.length+3 );
buf.put((byte)0x01).put((byte)usr.length).put(usr).put((byte)pass.length).put(pass).flip();
unconnectedSocket.write(buf);
ByteBuffer authSuccess = ByteBuffer.allocate(2);
read = unconnectedSocket.read(authSuccess);
if (read != 2 || authSuccess.get(1) != 0) {
throw new IOException("Auth Failed! "+read+ " "+authSuccess.get(1));
}
} else {
throw new IOException("No Password Auth possible");
}
break;
default:
throw new IOException("Authentication Failed: "+methodSelected.get(1));
}
}
}
private void request(SocketChannel sc,CMD c,InetAddress ia, int port) throws IOException {
boolean ipv4 = ia instanceof Inet4Address;
//now open up the socket...
byte[] head = new byte[]{5,c.getVal(),0, (byte)(ipv4? 1:6)};
ByteBuffer send = ByteBuffer.allocate(head.length+ ia.getAddress().length+2);
send.put(head).put(ia.getAddress()).putShort((short)port).flip();
sc.write(send);
}
private InetSocketAddress readResponse(SocketChannel sc,InetAddress originalSentIA) throws IOException {
ByteBuffer response = ByteBuffer.allocate(6+originalSentIA.getAddress().length);
int read = sc.read(response);
response.flip();
if (read != response.capacity()) {
throw new IOException("unexpected length for response! "+read);
}
if (response.get(1) != 0x00) { //if not successful throw Error
String error= "";
switch(response.get(1)) {
case 1: error = "General SOCKS server failure"; break;
case 2: error = "connection not allowed by ruleset"; break;
case 3: error = "Network unreachable"; break;
case 4: error = "Host unreachable"; break;
case 5: error = "Connection refused"; break;
case 6: error = "TTL expired"; break;
case 7: error = "Command not supported"; break;
case 8: error = "Address type not supported"; break;
default: error = "Unknown: "+response.get(1); break;
}
throw new IOException("Error: "+error);
}
byte[] address = new byte[originalSentIA.getAddress().length];
response.position(4);
response.get(address);
InetAddress ia = InetAddress.getByAddress(address);
response.position(response.limit()-2);
int port = response.getShort() & 0xffff;
return new InetSocketAddress(ia,port);
}
private static enum CMD {
CONNECT(1),BIND(2),UDP_ASC(3);
CMD(int val) {
this.val = (byte)val;
}
private final byte val;
public byte getVal() {
return val;
}
}
public class UDPRelay implements IUnblocking {
private InetSocketAddress udpServer;
private SocketChannel connectionToSocks;
private SelectionKey key;
private DatagramChannel datagramChannel;
private UDPRelay() {}
/**
* closes the tcp socket associated with the Socks server..
* so the UDP connection is finally closed..
*/
public synchronized void close() {
GH.close(connectionToSocks);
if (key != null) {
key.cancel();
}
RELAYS.remove(this);
}
public synchronized void connect() throws IOException {
connectionToSocks = SocketChannel.open();
connectAndAuth( connectionToSocks );
InetAddress ia = InetAddress.getByAddress(new byte[]{0,0,0,0});
request(connectionToSocks,CMD.UDP_ASC,ia ,0);
udpServer = readResponse(connectionToSocks, ia);
logger.debug("Socketaddy fo UDP Server of Socks: "+udpServer);
connectionToSocks.configureBlocking(false);
MultiStandardConnection.get().register(connectionToSocks, this, true);
RELAYS.add(this);
datagramChannel = DatagramChannel.open();
datagramChannel.socket().bind(null); //bind to random port...
datagramChannel.configureBlocking(true);
datagramChannel.connect(udpServer);
}
/**
* sends the provided packet over the UDP relay server..
*
* @param toSend - packet to send
* @param target - where the packet should be targeted to..
*/
public synchronized void send(byte[] toSend,InetSocketAddress target) {
ByteBuffer buf = ByteBuffer.allocate(toSend.length+6 + target.getAddress().getAddress().length);
buf.putShort((short)0); //reserved
buf.put((byte)0); //fragment number always zero
byte[] address = target.getAddress().getAddress();
buf.put((byte) (address.length == 4 ? 0x01: 0x04) ); //address type
buf.put( address ); //address
buf.putShort((short)target.getPort()); // port
buf.put(toSend); // data
buf.flip();
sendPacket( buf , udpServer );
}
private void sendPacket(ByteBuffer packet,InetSocketAddress target ){
if (0 < target.getPort() && target.getPort() <= 65535) {
logger.debug("sending udp packet: "+target+ " "+ packet);
try {
int i = datagramChannel.send(packet, target);
logger.debug("sent bytes: "+i);
} catch(BindException be) {
logger.debug("Packet could not be send: "+be,be);
} catch (IOException ioe) {
logger.warn(ioe+" packet: "+target+" "+new String(packet.array()),ioe);
}
}
}
/**
* disconnects should never happen -> may be
* TODO reconnect on next UDP packet..
*/
public void onDisconnect() throws IOException {
throw new IllegalStateException();
}
/**
* read method -> not used...
*/
public void read() throws IOException {
ByteBuffer readBytes = ByteBuffer.allocate(1024);
int read = connectionToSocks.read(readBytes);
if (read > 0) {
readBytes.flip();
logger.debug("Read bytes from Socks: "+new String(readBytes.array()));
} else {
close();
}
}
/*
* (non-Javadoc)
* @see uc.protocols.MultiStandardConnection.IUnblocking#setKey(java.nio.channels.SelectionKey)
*/
public synchronized void setKey(SelectionKey key) {
this.key = key;
}
/*
* (non-Javadoc)
* @see uc.protocols.MultiStandardConnection.IUnblocking#write()
*/
public void write() throws IOException {
throw new IllegalStateException();
}
/*
* (non-Javadoc)
* @see uc.protocols.MultiStandardConnection.IUnblocking#connected()
*/
public void connected() {
throw new IllegalStateException();
}
}
}