From f698ef93e8182d4d12cbc705a72563f7fa30333c Mon Sep 17 00:00:00 2001 From: zzz <zzz@mail.i2p> Date: Wed, 17 Nov 2010 15:47:00 +0000 Subject: [PATCH] * I2PTunnel SOCKS and SOCKS IRC clients: - Add SOCKS 5 outproxy support, with username/password authorization * I2PTunnel - Index page outproxy display cleanup --- .../net/i2p/i2ptunnel/TunnelController.java | 15 ++ .../i2p/i2ptunnel/socks/I2PSOCKSTunnel.java | 30 ++- .../net/i2p/i2ptunnel/socks/SOCKS5Server.java | 196 +++++++++++++++--- .../net/i2p/i2ptunnel/socks/SOCKSServer.java | 5 +- .../src/net/i2p/i2ptunnel/web/IndexBean.java | 9 +- apps/i2ptunnel/jsp/editClient.jsp | 2 +- apps/i2ptunnel/jsp/index.jsp | 15 +- 7 files changed, 227 insertions(+), 45 deletions(-) diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java index 02777dafbd..7a9b1dbd67 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java @@ -16,6 +16,7 @@ import net.i2p.client.I2PClientFactory; import net.i2p.client.I2PSession; import net.i2p.data.Base32; import net.i2p.data.Destination; +import net.i2p.i2ptunnel.socks.I2PSOCKSTunnel; import net.i2p.util.I2PAppThread; import net.i2p.util.Log; import net.i2p.util.SecureFileOutputStream; @@ -226,6 +227,13 @@ public class TunnelController implements Logging { setListenOn(); String listenPort = getListenPort(); String sharedClient = getSharedClient(); + String proxyList = getProxyList(); + if (proxyList != null) { + // set the outproxy property the socks tunnel wants + Properties props = _tunnel.getClientOptions(); + if (!props.containsKey(I2PSOCKSTunnel.PROP_PROXY_DEFAULT)) + props.setProperty(I2PSOCKSTunnel.PROP_PROXY_DEFAULT, proxyList); + } _tunnel.runSOCKSTunnel(new String[] { listenPort, sharedClient }, this); } @@ -234,6 +242,13 @@ public class TunnelController implements Logging { setListenOn(); String listenPort = getListenPort(); String sharedClient = getSharedClient(); + String proxyList = getProxyList(); + if (proxyList != null) { + // set the outproxy property the socks tunnel wants + Properties props = _tunnel.getClientOptions(); + if (!props.containsKey(I2PSOCKSTunnel.PROP_PROXY_DEFAULT)) + props.setProperty(I2PSOCKSTunnel.PROP_PROXY_DEFAULT, proxyList); + } if (getPersistentClientKey()) { String privKeyFile = getPrivKeyFile(); _tunnel.runSOCKSIRCTunnel(new String[] { listenPort, "false", privKeyFile }, this); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java index 564085ca2e..740aa4549b 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/I2PSOCKSTunnel.java @@ -15,6 +15,7 @@ import java.util.Properties; import java.util.StringTokenizer; import net.i2p.client.streaming.I2PSocket; +import net.i2p.client.streaming.I2PSocketOptions; import net.i2p.data.Destination; import net.i2p.i2ptunnel.I2PTunnel; import net.i2p.i2ptunnel.I2PTunnelClientBase; @@ -61,15 +62,19 @@ public class I2PSOCKSTunnel extends I2PTunnelClientBase { } } - private static final String PROP_PROXY = "i2ptunnel.socks.proxy."; + /** add "default" or port number */ + public static final String PROP_PROXY_PREFIX = "i2ptunnel.socks.proxy."; + public static final String DEFAULT = "default"; + public static final String PROP_PROXY_DEFAULT = PROP_PROXY_PREFIX + DEFAULT; + private void parseOptions() { Properties opts = getTunnel().getClientOptions(); - proxies = new HashMap(0); + proxies = new HashMap(1); for (Map.Entry e : opts.entrySet()) { String prop = (String)e.getKey(); - if ((!prop.startsWith(PROP_PROXY)) || prop.length() <= PROP_PROXY.length()) + if ((!prop.startsWith(PROP_PROXY_PREFIX)) || prop.length() <= PROP_PROXY_PREFIX.length()) continue; - String port = prop.substring(PROP_PROXY.length()); + String port = prop.substring(PROP_PROXY_PREFIX.length()); List proxyList = new ArrayList(1); StringTokenizer tok = new StringTokenizer((String)e.getValue(), ", \t"); while (tok.hasMoreTokens()) { @@ -95,7 +100,22 @@ public class I2PSOCKSTunnel extends I2PTunnelClientBase { } public List<String> getDefaultProxies() { - return proxies.get("default"); + return proxies.get(DEFAULT); + } + + /** + * Because getDefaultOptions() in super() is protected + * @since 0.8.2 + */ + public I2PSocketOptions buildOptions(Properties overrides) { + Properties defaultOpts = getTunnel().getClientOptions(); + defaultOpts.putAll(overrides); + // delayed start + verifySocketManager(); + I2PSocketOptions opts = sockMgr.buildOptions(defaultOpts); + if (!defaultOpts.containsKey(I2PSocketOptions.PROP_CONNECT_TIMEOUT)) + opts.setConnectTimeout(60 * 1000); + return opts; } } diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java index 22f8f8b0ed..427ca4b48f 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKS5Server.java @@ -21,6 +21,7 @@ import java.util.Properties; import net.i2p.I2PAppContext; import net.i2p.I2PException; 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.i2ptunnel.I2PTunnelHTTPClientBase; @@ -83,7 +84,7 @@ public class SOCKS5Server extends SOCKSServer { if (manageRequest(in, out) == Command.UDP_ASSOCIATE) handleUDP(in, out); } catch (IOException e) { - throw new SOCKSException("Connection error (" + e.getMessage() + ")"); + throw new SOCKSException("Connection error: " + e); } setupCompleted = true; @@ -94,12 +95,12 @@ public class SOCKS5Server extends SOCKSServer { * SOCKS "VER" field has been stripped from the input stream. */ private void init(DataInputStream in, DataOutputStream out) throws IOException, SOCKSException { - int nMethods = in.readByte() & 0xff; + int nMethods = in.readUnsignedByte(); boolean methodOk = false; int method = Method.NO_ACCEPTABLE_METHODS; for (int i = 0; i < nMethods; ++i) { - int meth = in.readByte() & 0xff; + int meth = in.readUnsignedByte(); if (((!authRequired) && meth == Method.NO_AUTH_REQUIRED) || (authRequired && meth == Method.USERNAME_PASSWORD)) { // That's fine, we do support this method @@ -129,15 +130,15 @@ public class SOCKS5Server extends SOCKSServer { * @since 0.8.2 */ private void verifyPassword(DataInputStream in, DataOutputStream out) throws IOException, SOCKSException { - int c = in.readByte() & 0xff; + int c = in.readUnsignedByte(); if (c != AUTH_VERSION) throw new SOCKSException("Unsupported authentication version"); - c = in.readByte() & 0xff; + c = in.readUnsignedByte(); if (c <= 0) throw new SOCKSException("Bad authentication"); byte[] user = new byte[c]; in.readFully(user); - c = in.readByte() & 0xff; + c = in.readUnsignedByte(); if (c <= 0) throw new SOCKSException("Bad authentication"); byte[] pw = new byte[c]; @@ -165,13 +166,13 @@ public class SOCKS5Server extends SOCKSServer { * has been stripped out of the input/output streams. */ private int manageRequest(DataInputStream in, DataOutputStream out) throws IOException, SOCKSException { - int socksVer = in.readByte() & 0xff; + int socksVer = in.readUnsignedByte(); if (socksVer != SOCKS_VERSION_5) { _log.debug("error in SOCKS5 request (protocol != 5? wtf?)"); throw new SOCKSException("Invalid protocol version in request: " + socksVer); } - int command = in.readByte() & 0xff; + int command = in.readUnsignedByte(); switch (command) { case Command.CONNECT: break; @@ -192,17 +193,15 @@ public class SOCKS5Server extends SOCKSServer { throw new SOCKSException("Invalid command in request"); } - { - // Reserved byte, should be 0x00 - byte rsv = in.readByte(); - } + // Reserved byte, should be 0x00 + in.readByte(); - int addressType = in.readByte() & 0xff; + addressType = in.readUnsignedByte(); switch (addressType) { case AddressType.IPV4: connHostName = new String(""); for (int i = 0; i < 4; ++i) { - int octet = in.readByte() & 0xff; + int octet = in.readUnsignedByte(); connHostName += Integer.toString(octet); if (i != 3) { connHostName += "."; @@ -213,7 +212,7 @@ public class SOCKS5Server extends SOCKSServer { break; case AddressType.DOMAINNAME: { - int addrLen = in.readByte() & 0xff; + int addrLen = in.readUnsignedByte(); if (addrLen == 0) { _log.debug("0-sized address length? wtf?"); throw new SOCKSException("Illegal DOMAINNAME length"); @@ -254,7 +253,7 @@ public class SOCKS5Server extends SOCKSServer { sendRequestReply(Reply.SUCCEEDED, AddressType.IPV4, InetAddress.getByName("127.0.0.1"), null, 1, out); } catch (IOException e) { - throw new SOCKSException("Connection error (" + e.getMessage() + ")"); + throw new SOCKSException("Connection error: " + e); } } @@ -347,7 +346,7 @@ public class SOCKS5Server extends SOCKSServer { try { out = new DataOutputStream(clientSock.getOutputStream()); } catch (IOException e) { - throw new SOCKSException("Connection error (" + e.getMessage() + ")"); + throw new SOCKSException("Connection error: " + e); } // FIXME: here we should read our config file, select an @@ -376,6 +375,7 @@ public class SOCKS5Server extends SOCKSServer { sendRequestReply(Reply.CONNECTION_NOT_ALLOWED_BY_RULESET, AddressType.DOMAINNAME, null, "0.0.0.0", 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) @@ -386,6 +386,7 @@ public class SOCKS5Server extends SOCKSServer { sendRequestReply(Reply.CONNECTION_NOT_ALLOWED_BY_RULESET, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); } catch (IOException ioe) {} throw new SOCKSException(err); + ****/ } else { List<String> proxies = t.getProxies(connPort); if (proxies == null || proxies.isEmpty()) { @@ -398,41 +399,182 @@ public class SOCKS5Server extends SOCKSServer { } int p = I2PAppContext.getGlobalContext().random().nextInt(proxies.size()); String proxy = proxies.get(p); - _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(I2PTunnel.destFromName(proxy)); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("connecting to proxy " + proxy + " for " + connHostName + " port " + connPort); + + try { + destSock = outproxyConnect(t, proxy); + } catch (SOCKSException se) { + try { + sendRequestReply(Reply.HOST_UNREACHABLE, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); + } catch (IOException ioe) {} + throw se; + } } confirmConnection(); _log.debug("connection confirmed - exchanging data..."); } catch (DataFormatException e) { + if (_log.shouldLog(Log.WARN)) + _log.warn("socks error", e); try { sendRequestReply(Reply.HOST_UNREACHABLE, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); } catch (IOException ioe) {} throw new SOCKSException("Error in destination format"); } catch (SocketException e) { + if (_log.shouldLog(Log.WARN)) + _log.warn("socks error", e); try { sendRequestReply(Reply.HOST_UNREACHABLE, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); } catch (IOException ioe) {} - throw new SOCKSException("Error connecting (" - + e.getMessage() + ")"); + throw new SOCKSException("Error connecting: " + e); } catch (IOException e) { + if (_log.shouldLog(Log.WARN)) + _log.warn("socks error", e); try { sendRequestReply(Reply.HOST_UNREACHABLE, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); } catch (IOException ioe) {} - throw new SOCKSException("Error connecting (" - + e.getMessage() + ")"); + throw new SOCKSException("Error connecting: " + e); } catch (I2PException e) { + if (_log.shouldLog(Log.WARN)) + _log.warn("socks error", e); try { sendRequestReply(Reply.HOST_UNREACHABLE, AddressType.DOMAINNAME, null, "0.0.0.0", 0, out); } catch (IOException ioe) {} - throw new SOCKSException("Error connecting (" - + e.getMessage() + ")"); + throw new SOCKSException("Error connecting: " + e); } return destSock; } + /** + * Act as a SOCKS 5 client to connect to an outproxy + * @return open socket or throws error + * @since 0.8.2 + */ + private I2PSocket outproxyConnect(I2PSOCKSTunnel tun, String proxy) throws IOException, SOCKSException, DataFormatException, I2PException { + Properties overrides = new Properties(); + overrides.setProperty("option.i2p.streaming.connectDelay", "1000"); + I2PSocketOptions proxyOpts = tun.buildOptions(overrides); + Destination dest = I2PTunnel.destFromName(proxy); + if (dest == null) + throw new SOCKSException("Outproxy not found"); + I2PSocket destSock = tun.createI2PSocket(I2PTunnel.destFromName(proxy), proxyOpts); + try { + DataOutputStream out = new DataOutputStream(destSock.getOutputStream()); + boolean authAvail = Boolean.valueOf(props.getProperty(I2PTunnelHTTPClientBase.PROP_OUTPROXY_AUTH)).booleanValue(); + String configUser = null; + String configPW = null; + if (authAvail) { + configUser = props.getProperty(I2PTunnelHTTPClientBase.PROP_OUTPROXY_USER_PREFIX + proxy); + configPW = props.getProperty(I2PTunnelHTTPClientBase.PROP_OUTPROXY_PW_PREFIX + proxy); + if (configUser == null || configPW == null) { + configUser = props.getProperty(I2PTunnelHTTPClientBase.PROP_OUTPROXY_USER); + configPW = props.getProperty(I2PTunnelHTTPClientBase.PROP_OUTPROXY_PW); + if (configUser == null || configPW == null) + authAvail = false; + } + } + + // send the init + out.writeByte(SOCKS_VERSION_5); + if (authAvail) { + out.writeByte(2); + out.writeByte(Method.NO_AUTH_REQUIRED); + out.writeByte(Method.USERNAME_PASSWORD); + } else { + out.writeByte(1); + out.writeByte(Method.NO_AUTH_REQUIRED); + } + out.flush(); + + // read init reply + DataInputStream in = new DataInputStream(destSock.getInputStream()); + // is this right or should we not try to do 5-to-4 conversion? + int hisVersion = in.readByte(); + if (hisVersion != SOCKS_VERSION_5 /* && addrtype == AddressType.DOMAINNAME */ ) + throw new SOCKSException("SOCKS Outproxy is not Version 5"); + //else if (hisVersion != 4) + // throw new SOCKSException("Unsupported SOCKS Outproxy Version"); + + int method = in.readByte(); + if (method == Method.NO_AUTH_REQUIRED) { + // good + } else if (method == Method.USERNAME_PASSWORD) { + if (authAvail) { + // send the auth + out.writeByte(AUTH_VERSION); + byte[] user = configUser.getBytes("UTF-8"); + byte[] pw = configPW.getBytes("UTF-8"); + out.writeByte(user.length); + out.write(user); + out.writeByte(pw.length); + out.write(pw); + out.flush(); + // read the auth reply + if (in.readByte() != AUTH_VERSION) + throw new SOCKSException("Bad auth version from outproxy"); + if (in.readByte() != AUTH_SUCCESS) + throw new SOCKSException("Outproxy authorization failure"); + } else { + throw new SOCKSException("Outproxy requires authorization, please configure username/password"); + } + } else { + throw new SOCKSException("Outproxy authorization failure"); + } + + // send the connect command + out.writeByte(SOCKS_VERSION_5); + out.writeByte(Command.CONNECT); + out.writeByte(0); // reserved + out.writeByte(addressType); + if (addressType == AddressType.IPV4) { + out.write(InetAddress.getByName(connHostName).getAddress()); + } else if (addressType == AddressType.DOMAINNAME) { + byte[] d = connHostName.getBytes("ISO-8859-1"); + out.writeByte(d.length); + out.write(d); + } else { + // shouldn't happen + throw new SOCKSException("Unknown address type for outproxy?"); + } + out.writeShort(connPort); + out.flush(); + + // read the connect reply + hisVersion = in.readByte(); + if (hisVersion != SOCKS_VERSION_5) + throw new SOCKSException("Outproxy response is not Version 5"); + int reply = in.readByte(); + in.readByte(); // reserved + int type = in.readByte(); + int count = 0; + if (type == AddressType.IPV4) { + count = 4; + } else if (type == AddressType.DOMAINNAME) { + count = in.readUnsignedByte(); + } else if (type == AddressType.IPV6) { + count = 16; + } else { + throw new SOCKSException("Unsupported address type in outproxy response"); + } + byte[] addr = new byte[count]; + in.readFully(addr); // address + in.readUnsignedShort(); // port + if (reply != Reply.SUCCEEDED) + throw new SOCKSException("Outproxy rejected request, response = " + reply); + // throw away the address in the response + // todo pass the response through? + } catch (IOException e) { + try { destSock.close(); } catch (IOException ioe) {} + throw e; + } catch (SOCKSException e) { + try { destSock.close(); } catch (IOException ioe) {} + throw e; + } + // that's it, caller will send confirmation to our client + return destSock; + } + // This isn't really the right place for this, we can't stop the tunnel once it starts. static SOCKSUDPTunnel _tunnel; static final Object _startLock = new Object(); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServer.java index 09e9284deb..dcca83989a 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/socks/SOCKSServer.java @@ -20,8 +20,9 @@ public abstract class SOCKSServer { private static final Log _log = new Log(SOCKSServer.class); /* Details about the connection requested by client */ - protected String connHostName = null; - protected int connPort = 0; + protected String connHostName; + protected int connPort; + protected int addressType; /** * Perform server initialization (expecially regarding protected diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java index d5afbc38b1..c21b45ee9d 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java @@ -862,16 +862,15 @@ public class IndexBean { } // generic proxy stuff - if ("httpclient".equals(_type) || "connectclient".equals(_type) || "httpbidirserver".equals(_type) || + if ("httpclient".equals(_type) || "connectclient".equals(_type) || "sockstunnel".equals(_type) ||"socksirctunnel".equals(_type)) { for (String p : _booleanProxyOpts) config.setProperty("option." + p, "" + _booleanOptions.contains(p)); - } - - if ("httpclient".equals(_type) || "connectclient".equals(_type)) { if (_proxyList != null) config.setProperty("proxyList", _proxyList); - } else if ("ircclient".equals(_type) || "client".equals(_type) || "streamrclient".equals(_type)) { + } + + if ("ircclient".equals(_type) || "client".equals(_type) || "streamrclient".equals(_type)) { if (_targetDestination != null) config.setProperty("targetDestination", _targetDestination); } else if ("httpserver".equals(_type) || "httpbidirserver".equals(_type)) { diff --git a/apps/i2ptunnel/jsp/editClient.jsp b/apps/i2ptunnel/jsp/editClient.jsp index d755e4a74e..be48bb2b92 100644 --- a/apps/i2ptunnel/jsp/editClient.jsp +++ b/apps/i2ptunnel/jsp/editClient.jsp @@ -139,7 +139,7 @@ <hr /> </div> - <% if ("httpclient".equals(tunnelType) || "connectclient".equals(tunnelType)) { + <% if ("httpclient".equals(tunnelType) || "connectclient".equals(tunnelType) || "sockstunnel".equals(tunnelType) || "socksirctunnel".equals(tunnelType)) { %><div id="destinationField" class="rowItem"> <label for="proxyList" accesskey="x"> <%=intl._("Outproxies")%>(<span class="accessKey">x</span>): diff --git a/apps/i2ptunnel/jsp/index.jsp b/apps/i2ptunnel/jsp/index.jsp index bcb4c66796..8b7f8051ea 100644 --- a/apps/i2ptunnel/jsp/index.jsp +++ b/apps/i2ptunnel/jsp/index.jsp @@ -250,19 +250,24 @@ } %></div> - <% if (!("sockstunnel".equals(indexBean.getInternalType(curClient)) || - "socksirctunnel".equals(indexBean.getInternalType(curClient)))) { %> <div class="destinationField rowItem"> <label> - <% if ("httpclient".equals(indexBean.getInternalType(curClient)) || "connectclient".equals(indexBean.getInternalType(curClient))) { %> + <% if ("httpclient".equals(indexBean.getInternalType(curClient)) || "connectclient".equals(indexBean.getInternalType(curClient)) || + "sockstunnel".equals(indexBean.getInternalType(curClient)) || "socksirctunnel".equals(indexBean.getInternalType(curClient))) { %> <%=intl._("Outproxy")%>: <% } else { %> <%=intl._("Destination")%>: <% } %> </label> - <input class="freetext" size="40" readonly="readonly" value="<%=indexBean.getClientDestination(curClient)%>" /> + <div class="text"> + <% String cdest = indexBean.getClientDestination(curClient); + if (cdest.length() > 0) { + %><%=cdest%><% + } else { + %><i><%=intl._("none")%></i><% + } %> + </div> </div> - <% } %> <div class="descriptionField rowItem"> <label><%=intl._("Description")%>:</label> -- GitLab