forked from I2P_Developers/i2p.i2p
i2ptunnel: Caching of outproxy selection, avoid last-failed outproxy
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user