diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java index 9c6439264d7c0a113615e25721176c2d8c9a3709..d36ac58f1233cbba38f1ba2cac9057be222ca924 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java @@ -225,7 +225,7 @@ public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements R } if (!usingInternalOutproxy) { // The request must be forwarded to a outproxy - currentProxy = selectProxy(); + currentProxy = selectProxy(hostLowerCase); if (currentProxy == null) { if (_log.shouldLog(Log.WARN)) _log.warn(getPrefix(requestId) + "Host wants to be outproxied, but we dont have any!"); @@ -347,8 +347,13 @@ public class I2PTunnelConnectClient extends I2PTunnelHTTPClientBase implements R data = newRequest.toString().getBytes("ISO-8859-1"); else response = SUCCESS_RESPONSE.getBytes("UTF-8"); - OnTimeout onTimeout = new OnTimeout(s, s.getOutputStream(), targetRequest, usingWWWProxy, currentProxy, requestId); - Thread t = new I2PTunnelRunner(s, i2ps, sockLock, data, response, mySockets, onTimeout); + OnTimeout onTimeout = new OnTimeout(s, s.getOutputStream(), targetRequest, usingWWWProxy, + currentProxy, requestId, targetRequest, false); + I2PTunnelRunner t = new I2PTunnelRunner(s, i2ps, sockLock, data, response, mySockets, onTimeout); + if (usingWWWProxy) { + // isSSL must be false for ConnectClient + t.setSuccessCallback(new OnProxySuccess(currentProxy, host, false)); + } // we are called from an unlimited thread pool, so run inline //t.start(); t.run(); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java index ea18fc8f0cbc50f56ccb61fb3ec0ca8cb6c5eafa..a2603c24f663218aeac060ed852e2f0aa5435b33 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java @@ -367,8 +367,6 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn public static final String PROP_VIA = "i2ptunnel.httpclient.sendVia"; public static final String PROP_JUMP_SERVERS = "i2ptunnel.httpclient.jumpServers"; public static final String PROP_DISABLE_HELPER = "i2ptunnel.httpclient.disableAddressHelper"; - /** @since 0.9.11 */ - public static final String PROP_SSL_OUTPROXIES = "i2ptunnel.httpclient.SSLOutproxies"; /** @since 0.9.14 */ public static final String PROP_ACCEPT = "i2ptunnel.httpclient.sendAccept"; /** @since 0.9.14, overridden to true as of 0.9.35 unlesss PROP_SSL_SET is set */ @@ -406,6 +404,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn out = s.getOutputStream(); InputReader reader = new InputReader(s.getInputStream()); String line, method = null, protocol = null, host = null, destination = null; + String hostLowerCase = null; StringBuilder newRequest = new StringBuilder(); boolean ahelperPresent = false; boolean ahelperNew = false; @@ -581,7 +580,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn // in our addressbook (all naming is local), // and it is removed from the request line. - String hostLowerCase = host.toLowerCase(Locale.US); + hostLowerCase = host.toLowerCase(Locale.US); if(hostLowerCase.equals(LOCAL_SERVER)) { // so we don't do any naming service lookups destination = host; @@ -869,9 +868,9 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn } if ("https".equals(protocol) || method.toUpperCase(Locale.US).equals("CONNECT")) - currentProxy = selectSSLProxy(); + currentProxy = selectSSLProxy(hostLowerCase); else - currentProxy = selectProxy(); + currentProxy = selectProxy(hostLowerCase); if(_log.shouldLog(Log.DEBUG)) { _log.debug("After selecting outproxy for " + host + ": " + currentProxy); } @@ -945,8 +944,8 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn // Note that we only pass the original Host: line through to the outproxy // But we don't create a Host: line if it wasn't sent to us line = "Host: " + host; - if(_log.shouldLog(Log.INFO)) { - _log.info(getPrefix(requestId) + "Setting host = " + host); + if (_log.shouldDebug()) { + _log.debug(getPrefix(requestId) + "Setting host = " + host); } } else if(lowercaseLine.startsWith("user-agent: ")) { // save for deciding whether to offer address book form @@ -1306,9 +1305,11 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn if (remotePort > 0) sktOpts.setPort(remotePort); i2ps = createI2PSocket(clientDest, sktOpts); - OnTimeout onTimeout = new OnTimeout(s, s.getOutputStream(), targetRequest, usingWWWProxy, currentProxy, requestId); - Thread t; - if (method.toUpperCase(Locale.US).equals("CONNECT")) { + boolean isConnect = method.toUpperCase(Locale.US).equals("CONNECT"); + OnTimeout onTimeout = new OnTimeout(s, s.getOutputStream(), targetRequest, usingWWWProxy, + currentProxy, requestId, hostLowerCase, isConnect); + I2PTunnelRunner t; + if (isConnect) { byte[] data; byte[] response; if (usingWWWProxy) { @@ -1323,6 +1324,9 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn byte[] data = newRequest.toString().getBytes("ISO-8859-1"); t = new I2PTunnelHTTPClientRunner(s, i2ps, sockLock, data, mySockets, onTimeout); } + if (usingWWWProxy) { + t.setSuccessCallback(new OnProxySuccess(currentProxy, hostLowerCase, isConnect)); + } // we are called from an unlimited thread pool, so run inline //t.start(); t.run(); @@ -1347,26 +1351,6 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn } } - /** - * Unlike selectProxy(), we parse the option on the fly so it - * can be changed. selectProxy() requires restart... - * @return null if none - * @since 0.9.11 - */ - private String selectSSLProxy() { - String s = getTunnel().getClientOptions().getProperty(PROP_SSL_OUTPROXIES); - if (s == null) - return null; - String[] p = DataHelper.split(s, "[,; \r\n\t]"); - if (p.length == 0) - return null; - // todo doesn't check for "" - if (p.length == 1) - return p[0]; - int i = _context.random().nextInt(p.length); - return p[i]; - } - /** @since 0.8.7 */ private void writeHelperSaveForm(OutputStream outs, String destination, String ahelperKey, String targetRequest, String referer) throws IOException { diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java index 3e64286aed9b9d8f8794724afa366b8ad1e324ae..744abde1b4c61d2b979e26029a1184a1997260dd 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java @@ -18,6 +18,7 @@ import java.net.Socket; import java.net.SocketTimeoutException; import java.net.URI; import java.net.URISyntaxException; +import java.util.Arrays; import java.util.ArrayList; import java.util.BitSet; import java.util.Date; @@ -42,6 +43,7 @@ import net.i2p.data.i2cp.MessageStatusMessage; import net.i2p.util.EepGet; import net.i2p.util.EventDispatcher; import net.i2p.util.InternalSocket; +import net.i2p.util.LHMCache; import net.i2p.util.Log; import net.i2p.util.PasswordManager; import net.i2p.util.PortMapper; @@ -64,6 +66,9 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem private static final int MAX_NONCE_COUNT = 1024; /** @since 0.9.11, moved to Base in 0.9.29 */ public static final String PROP_USE_OUTPROXY_PLUGIN = "i2ptunnel.useLocalOutproxy"; + /** @since 0.9.11, moved to Base in 0.9.39 */ + public static final String PROP_SSL_OUTPROXIES = "i2ptunnel.httpclient.SSLOutproxies"; + /** * This is a standard soTimeout, not a total timeout. * We have no slowloris protection on the client side. @@ -122,19 +127,142 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem private final byte[] _proxyNonce; private final ConcurrentHashMap<String, NonceInfo> _nonces; private final AtomicInteger _nonceCleanCounter = new AtomicInteger(); + // clearnet host to proxy + private final Map<String, String> _proxyCache = new LHMCache<String, String>(32); + // very simple, remember last-failed only + private String _lastFailedProxy; + // clearnet host to proxy + private final Map<String, String> _proxySSLCache = new LHMCache<String, String>(32); + // very simple, remember last-failed only + private String _lastFailedSSLProxy; protected String getPrefix(long requestId) { return "HTTPClient[" + _clientId + '/' + requestId + "]: "; } + + // TODO standard proxy config changes require tunnel restart; + // SSL proxy config is parsed on the fly; + // allow both to be changed and store the SSL proxy list. + // TODO should track more than one failed proxy - protected String selectProxy() { + /** + * Simple random selection, with caching by hostname, + * and avoidance of the last one to fail. + * + * @param host the clearnet hostname we're targeting + * @return null if none configured + */ + protected String selectProxy(String host) { + String rv; synchronized (_proxyList) { int size = _proxyList.size(); if (size <= 0) return null; - int index = _context.random().nextInt(size); - return _proxyList.get(index); + if (size == 1) + return _proxyList.get(0); + rv = _proxyCache.get(host); + if (rv == null) { + List<String> tmpList; + if (_lastFailedProxy != null) { + // don't use last failed one + tmpList = new ArrayList<String>(_proxyList); + tmpList.remove(_lastFailedProxy); + size = tmpList.size(); + } else { + tmpList = _proxyList; + } + int index = _context.random().nextInt(size); + rv = tmpList.get(index); + _proxyCache.put(host, rv); + } + } + if (_log.shouldInfo()) + _log.info("Selected proxy for " + host + ": " + rv); + return rv; + } + + /** + * Only for SSL via HTTPClient. ConnectClient should use selectProxy() + * + * Unlike selectProxy(), we parse the option on the fly so it + * can be changed. selectProxy() requires restart... + * + * @return null if none configured + * @since 0.9.11, moved from I2PTunnelHTTPClient in 0.9.39 + */ + protected String selectSSLProxy(String host) { + String s = getTunnel().getClientOptions().getProperty(PROP_SSL_OUTPROXIES); + if (s == null) + return null; + String[] p = DataHelper.split(s, "[,; \r\n\t]"); + int size = p.length; + if (size == 0) + return null; + // todo doesn't check for "" + if (size == 1) + return p[0]; + String rv; + synchronized (_proxySSLCache) { + rv = _proxySSLCache.get(host); + if (rv == null) { + List<String> tmpList; + if (_lastFailedSSLProxy != null) { + // don't use last failed one + tmpList = new ArrayList<String>(Arrays.asList(p)); + tmpList.remove(_lastFailedSSLProxy); + size = tmpList.size(); + } else { + tmpList = Arrays.asList(p); + } + int index = _context.random().nextInt(size); + rv = tmpList.get(index); + _proxySSLCache.put(host, rv); + } + } + if (_log.shouldInfo()) + _log.info("Selected SSL proxy for " + host + ": " + rv); + return rv; + } + + /** + * Update the cache and note if failed. + * + * @param proxy which + * @param host clearnet hostname targeted + * @param isSSL set to FALSE for ConnectClient + * @param ok success or failure + * @since 0.9.39 + */ + protected void noteProxyResult(String proxy, String host, boolean isSSL, boolean ok) { + if (isSSL) { + synchronized (_proxySSLCache) { + if (ok) { + if (proxy.equals(_lastFailedSSLProxy)) + _lastFailedSSLProxy = null; + _proxySSLCache.put(host, proxy); + } else { + _lastFailedSSLProxy = proxy; + if (proxy.equals(_proxySSLCache.get(host))) + _proxySSLCache.remove(host); + } + } + } else { + synchronized (_proxyList) { + if (_proxyList.size() > 1) { + if (ok) { + if (proxy.equals(_lastFailedProxy)) + _lastFailedProxy = null; + _proxyCache.put(host, proxy); + } else { + _lastFailedProxy = proxy; + if (proxy.equals(_proxyCache.get(host))) + _proxyCache.remove(host); + } + } + } } + if (_log.shouldInfo()) + _log.info("Proxy result: to " + host + " through " + proxy + " SSL? " + isSSL + " success? " + ok); } /** @@ -610,7 +738,12 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem private final boolean _usingProxy; private final String _wwwProxy; private final long _requestId; + private final String _targetHost; + private final boolean _isSSL; + /** + * @param target the URI for an HTTP request, or the host name for CONNECT + */ public OnTimeout(Socket s, OutputStream out, String target, boolean usingProxy, String wwwProxy, long id) { _socket = s; _out = out; @@ -618,12 +751,35 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem _usingProxy = usingProxy; _wwwProxy = wwwProxy; _requestId = id; + _targetHost = null; + _isSSL = false; + } + + /** + * @param target the URI for an HTTP request, or the host name for CONNECT + * @param targetHost if non-null, call noteProxyResult() with this as host + * @param isSSL to pass to noteProxyResult(). FALSE for ConnectClient. + * @since 0.9.39 + */ + public OnTimeout(Socket s, OutputStream out, String target, boolean usingProxy, + String wwwProxy, long id, String targetHost, boolean isSSL) { + _socket = s; + _out = out; + _target = target; + _usingProxy = usingProxy; + _wwwProxy = wwwProxy; + _requestId = id; + _targetHost = targetHost; + _isSSL = isSSL; } /** * @param ex may be null */ public void onFail(Exception ex) { + if (_usingProxy && _targetHost != null) { + noteProxyResult(_wwwProxy, _targetHost, _isSSL, false); + } Throwable cause = ex != null ? ex.getCause() : null; if (cause != null && cause instanceof I2PSocketException) { I2PSocketException ise = (I2PSocketException) cause; @@ -635,6 +791,23 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem } } + /** + * @since 0.9.39 + */ + protected class OnProxySuccess implements I2PTunnelRunner.SuccessCallback { + private final String _proxy, _host; + private final boolean _isSSL; + + /** @param isSSL FALSE for ConnectClient */ + public OnProxySuccess(String proxy, String host, boolean isSSL) { + _proxy = proxy; _host = host; _isSSL = isSSL; + } + + public void onSuccess() { + noteProxyResult(_proxy, _host, _isSSL, true); + } + } + /** * @param ex may be null * @since 0.9.14 moved from subclasses diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelRunner.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelRunner.java index fd422adeada31bf05f46d1adf266813cc84d049a..a6f81bf0dbd31fbb228f345ab89423f2a51ebe99 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelRunner.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelRunner.java @@ -60,6 +60,7 @@ public class I2PTunnelRunner extends I2PAppThread implements I2PSocket.SocketErr /** if we die before receiving any data, run this job */ private final Runnable onTimeout; private final FailCallback _onFail; + private SuccessCallback _onSuccess; private long totalSent; private long totalReceived; @@ -74,6 +75,16 @@ public class I2PTunnelRunner extends I2PAppThread implements I2PSocket.SocketErr public void onFail(Exception e); } + /** + * @since 0.9.39 + */ + public interface SuccessCallback { + /** + * @param e may be null + */ + public void onSuccess(); + } + /** * Starts itself * @@ -228,6 +239,17 @@ public class I2PTunnelRunner extends I2PAppThread implements I2PSocket.SocketErr return startedOn; } + /** + * Will be called if we get any data back. + * This is called after the first byte of data is received, not on completion. + * Only one of SuccessCallback, onTimeout, or onFail will be called. + * + * @since 0.9.39 + */ + public void setSuccessCallback(SuccessCallback sc) { + _onSuccess = sc; + } + protected InputStream getSocketIn() throws IOException { return s.getInputStream(); } protected OutputStream getSocketOut() throws IOException { return s.getOutputStream(); } @@ -282,8 +304,8 @@ public class I2PTunnelRunner extends I2PAppThread implements I2PSocket.SocketErr + " written to the socket, starting forwarders"); if (!(s instanceof InternalSocket)) in = new BufferedInputStream(in, 2*NETWORK_BUFFER_SIZE); - toI2P = new StreamForwarder(in, i2pout, true); - fromI2P = new StreamForwarder(i2pin, out, false); + toI2P = new StreamForwarder(in, i2pout, true, null); + fromI2P = new StreamForwarder(i2pin, out, false, _onSuccess); toI2P.start(); // We are already a thread, so run the second one inline //fromI2P.start(); @@ -294,13 +316,13 @@ public class I2PTunnelRunner extends I2PAppThread implements I2PSocket.SocketErr } } if (_log.shouldLog(Log.DEBUG)) - _log.debug("At least one forwarder completed, closing and joining"); + _log.debug("Both forwarders completed, sent: " + totalSent + " received: " + totalReceived); // this task is useful for the httpclient if ((onTimeout != null || _onFail != null) && totalReceived <= 0) { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("runner has a timeout job, totalReceived = " + totalReceived - + " totalSent = " + totalSent + " job = " + onTimeout); + //if (_log.shouldLog(Log.DEBUG)) + // _log.debug("runner has a timeout job, totalReceived = " + totalReceived + // + " totalSent = " + totalSent + " job = " + onTimeout); // Run even if totalSent > 0, as that's probably POST data. // This will be run even if initialSocketData != null, it's the timeout job's // responsibility to know that and decide whether or not to write to the socket. @@ -460,15 +482,18 @@ public class I2PTunnelRunner extends I2PAppThread implements I2PSocket.SocketErr private final String direction; private final boolean _toI2P; private final ByteCache _cache; + private final SuccessCallback _callback; private volatile Exception _failure; /** * Does not start itself. Caller must start() + * @param cb may be null, only used for toI2P == false */ - public StreamForwarder(InputStream in, OutputStream out, boolean toI2P) { + public StreamForwarder(InputStream in, OutputStream out, boolean toI2P, SuccessCallback cb) { this.in = in; this.out = out; _toI2P = toI2P; + _callback = cb; direction = (toI2P ? "toI2P" : "fromI2P"); _cache = ByteCache.getInstance(32, NETWORK_BUFFER_SIZE); setName("StreamForwarder " + _runnerId + '.' + direction); @@ -495,10 +520,13 @@ public class I2PTunnelRunner extends I2PAppThread implements I2PSocket.SocketErr while ((len = in.read(buffer)) != -1) { if (len > 0) { out.write(buffer, 0, len); - if (_toI2P) + if (_toI2P) { totalSent += len; - else + } else { + if (totalReceived == 0 && _callback != null) + _callback.onSuccess(); totalReceived += len; + } //updateActivity(); }