i2ptunnel: Caching of outproxy selection, avoid last-failed outproxy

This commit is contained in:
zzz
2019-02-01 13:39:57 +00:00
parent 428fb269f0
commit 4f8455040e
4 changed files with 235 additions and 45 deletions

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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

View File

@@ -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();
}