/* I2PSOCKSTunnel is released under the terms of the GNU GPL,
* with an additional exception. For further details, see the
* licensing terms in I2PTunnel.java.
*
* Copyright (c) 2004 by human
*/
package net.i2p.i2ptunnel.socks;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import net.i2p.I2PAppContext;
import net.i2p.I2PException;
import net.i2p.app.ClientApp;
import net.i2p.app.ClientAppManager;
import net.i2p.app.Outproxy;
import net.i2p.client.streaming.I2PSocket;
import net.i2p.client.streaming.I2PSocketOptions;
import net.i2p.data.DataFormatException;
import net.i2p.data.Destination;
import net.i2p.util.HexDump;
import net.i2p.util.Log;
/*
* Class that manages SOCKS 4/4a connections, and forwards them to
* destination hosts or (eventually) some outproxy.
*
* @author zzz modded from SOCKS5Server
*/
class SOCKS4aServer extends SOCKSServer {
private boolean setupCompleted;
/**
* Create a SOCKS4a server that communicates with the client using
* the specified socket. This method should not be invoked
* directly: new SOCKS4aServer objects should be created by using
* SOCKSServerFactory.createSOCSKServer(). It is assumed that the
* SOCKS VER field has been stripped from the input stream of the
* client socket.
*
* @param clientSock client socket
* @param props non-null
*/
public SOCKS4aServer(I2PAppContext ctx, Socket clientSock, Properties props) {
super(ctx, clientSock, props);
}
public Socket getClientSocket() throws SOCKSException {
setupServer();
return clientSock;
}
protected void setupServer() throws SOCKSException {
if (setupCompleted) { return; }
DataInputStream in;
DataOutputStream out;
try {
in = new DataInputStream(clientSock.getInputStream());
out = new DataOutputStream(clientSock.getOutputStream());
manageRequest(in, out);
} catch (IOException e) {
throw new SOCKSException("Connection error (" + e.getMessage() + ")");
}
setupCompleted = true;
}
/**
* SOCKS4a request management. This method assumes that all the
* stuff preceding or enveloping the actual request
* has been stripped out of the input/output streams.
*/
private void manageRequest(DataInputStream in, DataOutputStream out) throws IOException, SOCKSException {
int command = in.readByte() & 0xff;
switch (command) {
case Command.CONNECT:
break;
case Command.BIND:
_log.debug("BIND command is not supported!");
sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out);
throw new SOCKSException("BIND command not supported");
default:
_log.debug("unknown command in request (" + Integer.toHexString(command) + ")");
sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out);
throw new SOCKSException("Invalid command in request");
}
connPort = in.readUnsignedShort();
if (connPort == 0) {
_log.debug("trying to connect to TCP port 0? Dropping!");
sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out);
throw new SOCKSException("Invalid port number in request");
}
StringBuilder builder = new StringBuilder();
boolean alreadyWarned = false;
for (int i = 0; i < 4; ++i) {
int octet = in.readByte() & 0xff;
builder.append(Integer.toString(octet));
if (i != 3) {
builder.append(".");
if (octet != 0 && !alreadyWarned) {
_log.warn("IPV4 address type in request: " + connHostName + ". Is your client secure?");
alreadyWarned = true;
}
}
}
connHostName = builder.toString();
// Check if the requested IP should be mapped to a domain name
String mappedDomainName = getMappedDomainNameForIP(connHostName);
if (mappedDomainName != null) {
_log.debug("IPV4 address " + connHostName + " was mapped to domain name " + mappedDomainName);
connHostName = mappedDomainName;
}
// discard user name
readString(in);
// SOCKS 4a
if (connHostName.startsWith("0.0.0.") && !connHostName.equals("0.0.0.0"))
connHostName = readString(in);
}
private String readString(DataInputStream in) throws IOException {
StringBuilder sb = new StringBuilder(16);
char c;
while ((c = (char) (in.readByte() & 0xff)) != 0)
sb.append(c);
return sb.toString();
}
protected void confirmConnection() throws SOCKSException {
DataOutputStream out;
try {
out = new DataOutputStream(clientSock.getOutputStream());
sendRequestReply(Reply.SUCCEEDED, InetAddress.getByName("127.0.0.1"), 1, out);
} catch (IOException e) {
throw new SOCKSException("Connection error (" + e.getMessage() + ")");
}
}
/**
* Send the specified reply to a request of the client. Either
* one of inetAddr or domainName can be null, depending on
* addressType.
*/
private void sendRequestReply(int replyCode, InetAddress inetAddr,
int bindPort, DataOutputStream out) throws IOException {
ByteArrayOutputStream reps = new ByteArrayOutputStream();
DataOutputStream dreps = new DataOutputStream(reps);
// Reserved byte, should be 0x00
dreps.write(0x00);
dreps.write(replyCode);
dreps.writeShort(bindPort);
dreps.write(inetAddr.getAddress());
byte[] reply = reps.toByteArray();
if (_log.shouldLog(Log.DEBUG)) {
_log.debug("Sending request reply:\n" + HexDump.dump(reply));
}
out.write(reply);
}
/**
* Get an I2PSocket that can be used to send/receive 8-bit clean data
* to/from the destination of the SOCKS connection.
*
* @return an I2PSocket connected with the destination
*/
public I2PSocket getDestinationI2PSocket(I2PSOCKSTunnel t) throws SOCKSException {
setupServer();
if (connHostName == null) {
_log.error("BUG: destination host name has not been initialized!");
throw new SOCKSException("BUG! See the logs!");
}
if (connPort == 0) {
_log.error("BUG: destination port has not been initialized!");
throw new SOCKSException("BUG! See the logs!");
}
DataOutputStream out; // for errors
try {
out = new DataOutputStream(clientSock.getOutputStream());
} catch (IOException e) {
throw new SOCKSException("Connection error (" + e.getMessage() + ")");
}
// FIXME: here we should read our config file, select an
// outproxy, and instantiate the proper socket class that
// handles the outproxy itself (SOCKS4a, SOCKS4a, HTTP CONNECT...).
I2PSocket destSock;
try {
if (connHostName.toLowerCase(Locale.US).endsWith(".i2p")) {
Destination dest = _context.namingService().lookup(connHostName);
if (dest == null) {
try {
sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out);
} catch (IOException ioe) {}
throw new SOCKSException("Host not found");
}
if (_log.shouldDebug())
_log.debug("connecting to " + connHostName + "...");
Properties overrides = new Properties();
I2PSocketOptions sktOpts = t.buildOptions(overrides);
sktOpts.setPort(connPort);
destSock = t.createI2PSocket(dest, sktOpts);
} else if ("localhost".equals(connHostName) || "127.0.0.1".equals(connHostName)) {
String err = "No localhost accesses allowed through the Socks Proxy";
_log.error(err);
try {
sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out);
} catch (IOException ioe) {}
throw new SOCKSException(err);
/****
} else if (connPort == 80) {
// rewrite GET line to include hostname??? or add Host: line???
// or forward to local eepProxy (but that's a Socket not an I2PSocket)
// use eepProxy configured outproxies?
String err = "No handler for HTTP outproxy implemented - to: " + connHostName;
_log.error(err);
try {
sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out);
} catch (IOException ioe) {}
throw new SOCKSException(err);
****/
} else {
Outproxy outproxy = getOutproxyPlugin();
if (outproxy != null) {
try {
destSock = new SocketWrapper(outproxy.connect(connHostName, connPort));
} catch (IOException ioe) {
try {
sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out);
} catch (IOException ioe2) {}
throw new SOCKSException("connect failed via outproxy plugin", ioe);
}
} else {
List<String> proxies = t.getProxies(connPort);
if (proxies == null || proxies.isEmpty()) {
String err = "No outproxy configured for port " + connPort + " and no default configured either - host: " + connHostName;
_log.error(err);
try {
sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out);
} catch (IOException ioe) {}
throw new SOCKSException(err);
}
int p = _context.random().nextInt(proxies.size());
String proxy = proxies.get(p);
Destination dest = _context.namingService().lookup(proxy);
if (dest == null) {
try {
sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out);
} catch (IOException ioe) {}
throw new SOCKSException("Outproxy not found");
}
if (_log.shouldDebug())
_log.debug("connecting to port " + connPort + " proxy " + proxy + " for " + connHostName + "...");
// this isn't going to work, these need to be socks outproxies so we need
// to do a socks session to them?
destSock = t.createI2PSocket(dest);
}
}
confirmConnection();
_log.debug("connection confirmed - exchanging data...");
} catch (DataFormatException e) {
try {
sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out);
} catch (IOException ioe) {}
throw new SOCKSException("Error in destination format");
} catch (SocketException e) {
try {
sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out);
} catch (IOException ioe) {}
throw new SOCKSException("Error connecting ("
+ e.getMessage() + ")");
} catch (IOException e) {
try {
sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out);
} catch (IOException ioe) {}
throw new SOCKSException("Error connecting ("
+ e.getMessage() + ")");
} catch (I2PException e) {
try {
sendRequestReply(Reply.CONNECTION_REFUSED, InetAddress.getByName("127.0.0.1"), 0, out);
} catch (IOException ioe) {}
throw new SOCKSException("Error connecting ("
+ e.getMessage() + ")");
}
return destSock;
}
/*
* Some namespaces to enclose SOCKS protocol codes
*/
private static class Command {
private static final int CONNECT = 0x01;
private static final int BIND = 0x02;
}
private static class Reply {
private static final int SUCCEEDED = 0x5a;
private static final int CONNECTION_REFUSED = 0x5b;
}
}