diff --git a/apps/routerconsole/jsp/configreseed.jsp b/apps/routerconsole/jsp/configreseed.jsp index 1fbc2dda2..e35902542 100644 --- a/apps/routerconsole/jsp/configreseed.jsp +++ b/apps/routerconsole/jsp/configreseed.jsp @@ -124,7 +124,6 @@ " > <% } // shouldShowHTTPProxy %> - +<% } // shouldShowHTTPSProxy %> diff --git a/core/java/src/net/i2p/util/EepGet.java b/core/java/src/net/i2p/util/EepGet.java index 9534d79db..33ad0b6d7 100644 --- a/core/java/src/net/i2p/util/EepGet.java +++ b/core/java/src/net/i2p/util/EepGet.java @@ -48,8 +48,8 @@ public class EepGet { protected final I2PAppContext _context; protected final Log _log; protected final boolean _shouldProxy; - private final String _proxyHost; - private final int _proxyPort; + protected final String _proxyHost; + protected final int _proxyPort; protected final int _numRetries; private final long _minSize; // minimum and maximum acceptable response size, -1 signifies unlimited, private final long _maxSize; // applied both against whole responses and chunks @@ -85,7 +85,6 @@ public class EepGet { protected boolean _notModified; protected String _contentType; protected boolean _transferFailed; - protected boolean _headersRead; protected boolean _aborted; protected int _fetchHeaderTimeout; private long _fetchEndTime; @@ -695,13 +694,8 @@ public class EepGet { * @param timeout may be null */ protected void doFetch(SocketTimeout timeout) throws IOException { - _headersRead = false; _aborted = false; - try { - readHeaders(); - } finally { - _headersRead = true; - } + readHeaders(); if (_aborted) throw new IOException("Timed out reading the HTTP headers"); @@ -1079,11 +1073,14 @@ public class EepGet { buf.setLength(0); byte lookahead[] = new byte[3]; + // "prime" the lookahead buffer with a '\n', + // so it works if there's no header lines at all, like a HTTPS proxy + increment(lookahead, '\n'); while (true) { int cur = _proxyIn.read(); switch (cur) { case -1: - throw new IOException("Headers ended too soon"); + throw new IOException("EOF reading headers"); case ':': if (key == null) { key = buf.toString(); @@ -1105,7 +1102,7 @@ public class EepGet { increment(lookahead, cur); if (isEndOfHeaders(lookahead)) { if (!rcOk) - throw new IOException("Invalid HTTP response code: " + _responseCode + ' ' + _responseText); + throw new IOException("Invalid HTTP response: " + _responseCode + ' ' + _responseText); if (_encodingChunked) { _bytesRemaining = readChunkLength(); } @@ -1502,6 +1499,8 @@ public class EepGet { * this will replace the etag or last-modified value given in the constructor. * Note that headers may be subsequently modified or removed in the I2PTunnel HTTP Client proxy. * + * In proxied SSLEepGet, these headers are sent to the remote server, NOT the proxy. + * * @since 0.8.8 */ public void addHeader(String name, String value) { diff --git a/core/java/src/net/i2p/util/EepHead.java b/core/java/src/net/i2p/util/EepHead.java index c9f375e45..609e4a573 100644 --- a/core/java/src/net/i2p/util/EepHead.java +++ b/core/java/src/net/i2p/util/EepHead.java @@ -160,13 +160,8 @@ public class EepHead extends EepGet { /** return true if the URL was completely retrieved */ @Override protected void doFetch(SocketTimeout timeout) throws IOException { - _headersRead = false; _aborted = false; - try { - readHeaders(); - } finally { - _headersRead = true; - } + readHeaders(); if (_aborted) throw new IOException("Timed out reading the HTTP headers"); diff --git a/core/java/src/net/i2p/util/PartialEepGet.java b/core/java/src/net/i2p/util/PartialEepGet.java index 9d853137d..c647a1e65 100644 --- a/core/java/src/net/i2p/util/PartialEepGet.java +++ b/core/java/src/net/i2p/util/PartialEepGet.java @@ -27,7 +27,7 @@ import net.i2p.I2PAppContext; * @author zzz */ public class PartialEepGet extends EepGet { - long _fetchSize; + private final long _fetchSize; /** * Instantiate an EepGet that will fetch exactly size bytes when fetch() is called. diff --git a/core/java/src/net/i2p/util/SSLEepGet.java b/core/java/src/net/i2p/util/SSLEepGet.java index 4e0ea7d4e..92694dfa7 100644 --- a/core/java/src/net/i2p/util/SSLEepGet.java +++ b/core/java/src/net/i2p/util/SSLEepGet.java @@ -54,6 +54,7 @@ import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.Locale; +import java.net.Socket; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLException; @@ -72,13 +73,15 @@ import net.i2p.crypto.KeyStoreUtil; import net.i2p.data.DataHelper; /** - * HTTPS only, non-proxied only, no retries, no min and max size options, no timeout option + * HTTPS only, no retries, no min and max size options, no timeout option * Fails on 301 or 302 (doesn't follow redirect) * Fails on bad certs (must have a valid cert chain) * Self-signed certs or CAs not in the JVM key store must be loaded to be trusted. * * Since 0.8.2, loads additional trusted CA certs from $I2P/certificates/ssl/ and ~/.i2p/certificates/ssl/ * + * Since 0.9.33, HTTP proxies (CONNECT) supported. Proxy auth not supported. + * * @author zzz * @since 0.7.10 */ @@ -93,9 +96,17 @@ public class SSLEepGet extends EepGet { private final SSLContext _sslContext; /** may be null if init failed */ private SavingTrustManager _stm; + private final ProxyType _proxyType; private static final String CERT_DIR = "certificates/ssl"; + /** + * Not all may be supported. + * @since 0.9.33 + */ + public enum ProxyType { NONE, HTTP, HTTPS, INTERNAL, SOCKS4, SOCKS5 } + + /** * A new SSLEepGet with a new SSLState */ @@ -131,6 +142,78 @@ public class SSLEepGet extends EepGet { this(ctx, outputFile, null, url, null); } + /** + * Use a proxy. + * + * @param proxyPort must be valid, -1 disallowed, no default + * @since 0.9.33 + */ + public SSLEepGet(I2PAppContext ctx, ProxyType type, String proxyHost, int proxyPort, + OutputStream outputStream, String url) { + this(ctx, type, proxyHost, proxyPort, outputStream, url, null); + } + + /** + * Use a proxy. + * + * @param proxyPort must be valid, -1 disallowed, no default + * @param state an SSLState retrieved from a previous SSLEepGet with getSSLState(), or null. + * This makes repeated fetches from the same host MUCH faster, + * and prevents repeated key store loads even for different hosts. + * @since 0.9.33 + */ + public SSLEepGet(I2PAppContext ctx, ProxyType type, String proxyHost, int proxyPort, + OutputStream outputStream, String url, SSLState state) { + // we're using this constructor: + // public EepGet(I2PAppContext ctx, boolean shouldProxy, String proxyHost, int proxyPort, int numRetries, long minSize, long maxSize, String outputFile, OutputStream outputStream, String url, boolean allowCaching, String etag, String postData) { + super(ctx, type != ProxyType.NONE, proxyHost, proxyPort, 0, -1, -1, null, outputStream, url, true, null, null); + if (type != ProxyType.NONE && !_shouldProxy) + throw new IllegalArgumentException("Bad proxy params"); + _proxyType = type; + if (state != null && state.context != null) + _sslContext = state.context; + else + _sslContext = initSSLContext(); + if (_sslContext == null) + _log.error("Failed to initialize custom SSL context, using default context"); + } + + /** + * Use a proxy. + * + * @param proxyPort must be valid, -1 disallowed, no default + * @since 0.9.33 + */ + public SSLEepGet(I2PAppContext ctx, ProxyType type, String proxyHost, int proxyPort, + String outputFile, String url) { + this(ctx, type, proxyHost, proxyPort, outputFile, url, null); + } + + /** + * Use a proxy. + * + * @param proxyPort must be valid, -1 disallowed, no default + * @param state an SSLState retrieved from a previous SSLEepGet with getSSLState(), or null. + * This makes repeated fetches from the same host MUCH faster, + * and prevents repeated key store loads even for different hosts. + * @since 0.9.33 + */ + public SSLEepGet(I2PAppContext ctx, ProxyType type, String proxyHost, int proxyPort, + String outputFile, String url, SSLState state) { + // we're using this constructor: + // public EepGet(I2PAppContext ctx, boolean shouldProxy, String proxyHost, int proxyPort, int numRetries, long minSize, long maxSize, String outputFile, OutputStream outputStream, String url, boolean allowCaching, String etag, String postData) { + super(ctx, type != ProxyType.NONE, proxyHost, proxyPort, 0, -1, -1, outputFile, null, url, true, null, null); + if (type != ProxyType.NONE && !_shouldProxy) + throw new IllegalArgumentException("Bad proxy params"); + _proxyType = type; + if (state != null && state.context != null) + _sslContext = state.context; + else + _sslContext = initSSLContext(); + if (_sslContext == null) + _log.error("Failed to initialize custom SSL context, using default context"); + } + /** * outputFile, outputStream: One null, one non-null * @@ -143,6 +226,7 @@ public class SSLEepGet extends EepGet { // we're using this constructor: // public EepGet(I2PAppContext ctx, boolean shouldProxy, String proxyHost, int proxyPort, int numRetries, long minSize, long maxSize, String outputFile, OutputStream outputStream, String url, boolean allowCaching, String etag, String postData) { super(ctx, false, null, -1, 0, -1, -1, outputFile, outputStream, url, true, null, null); + _proxyType = ProxyType.NONE; if (state != null && state.context != null) _sslContext = state.context; else @@ -159,12 +243,28 @@ public class SSLEepGet extends EepGet { public static void main(String args[]) { int saveCerts = 0; boolean noVerify = false; + String proxyHost = "127.0.0.1"; + int proxyPort = 80; boolean error = false; - Getopt g = new Getopt("ssleepget", args, "sz"); + Getopt g = new Getopt("ssleepget", args, "p:sz"); try { int c; while ((c = g.getopt()) != -1) { switch (c) { + case 'p': + String s = g.getOptarg(); + int colon = s.indexOf(':'); + if (colon >= 0) { + // Todo IPv6 [a:b:c]:4444 + proxyHost = s.substring(0, colon); + String port = s.substring(colon + 1); + proxyPort = Integer.parseInt(port); + } else { + proxyHost = s; + // proxyPort remains default + } + break; + case 's': saveCerts++; break; @@ -192,16 +292,12 @@ public class SSLEepGet extends EepGet { String url = args[g.getOptind()]; String saveAs = suggestName(url); - OutputStream out; - try { - // resume from a previous eepget won't work right doing it this way - out = new FileOutputStream(saveAs); - } catch (IOException ioe) { - System.err.println("Failed to create output file " + saveAs); - return; - } - SSLEepGet get = new SSLEepGet(I2PAppContext.getGlobalContext(), out, url); + SSLEepGet get; + if (proxyHost != null) + get = new SSLEepGet(I2PAppContext.getGlobalContext(), ProxyType.HTTP, proxyHost, proxyPort, saveAs, url); + else + get = new SSLEepGet(I2PAppContext.getGlobalContext(), saveAs, url); if (saveCerts > 0) get._saveCerts = saveCerts; if (noVerify) @@ -213,7 +309,8 @@ public class SSLEepGet extends EepGet { } private static void usage() { - System.err.println("Usage: SSLEepGet [-sz] https://url\n" + + System.err.println("Usage: SSLEepGet [-psz] https://url\n" + + " -p proxyHost[:proxyPort] // default port 80\n" + " -s save unknown certs\n" + " -s -s save all certs\n" + " -z bypass hostname verification"); @@ -410,13 +507,8 @@ public class SSLEepGet extends EepGet { @Override protected void doFetch(SocketTimeout timeout) throws IOException { - _headersRead = false; _aborted = false; - try { - readHeaders(); - } finally { - _headersRead = true; - } + readHeaders(); if (_aborted) throw new IOException("Timed out reading the HTTP headers"); @@ -573,17 +665,76 @@ public class SSLEepGet extends EepGet { port = url.getPort(); if (port == -1) port = 443; - // Warning, createSocket() followed by connect(InetSocketAddress) - // disables SNI, at least on Java 7. - // So we must do createSocket(host, port) and then setSoTimeout; - // we can't crate a disconnected socket and then call setSoTimeout, sadly. - if (_sslContext != null) - _proxy = _sslContext.getSocketFactory().createSocket(host, port); - else - _proxy = SSLSocketFactory.getDefault().createSocket(host, port); - if (_fetchHeaderTimeout > 0) { - _proxy.setSoTimeout(_fetchHeaderTimeout); + + if (_shouldProxy) { + if (_proxyType != ProxyType.HTTP) + throw new IOException("Unsupported proxy type " + _proxyType); + + // connect to the proxy + // _proxyPort validated in superconstrutor, no need to set default here + if (_fetchHeaderTimeout > 0) { + _proxy = new Socket(); + _proxy.setSoTimeout(_fetchHeaderTimeout); + _proxy.connect(new InetSocketAddress(_proxyHost, _proxyPort), _fetchHeaderTimeout); + } else { + _proxy = new Socket(_proxyHost, _proxyPort); + } + _proxyIn = _proxy.getInputStream(); + _proxyOut = _proxy.getOutputStream(); + StringBuilder buf = new StringBuilder(64); + buf.append("CONNECT ").append(host).append(':').append(port).append(" HTTP/1.1\r\n"); + // TODO if we need extra headers to the proxy, add a new method and list. + // Standard extra headers go the server, not the proxy + //if (_extraPHeaders != null) { + // for (String hdr : _extraPHeaders) { + // buf.append(hdr).append("\r\n"); + //} + if (_authState != null && _authState.authMode != AUTH_MODE.NONE) { + // TODO untested, is this right? + buf.append("Proxy-Authorization: "); + buf.append(_authState.getAuthHeader("CONNECT", host)); + buf.append("\r\n"); + } + buf.append("\r\n"); + _proxyOut.write(DataHelper.getUTF8(buf.toString())); + _proxyOut.flush(); + + // read the proxy response + _aborted = false; + readHeaders(); + if (_aborted) + throw new IOException("Timed out reading the proxy headers"); + if (_responseCode == 407) { + // TODO + throw new IOException("Proxy auth unsupported"); + } else if (_responseCode != 200) { + // readHeaders() will throw on most errors, but here we ensure it is 200 + throw new IOException("Invalid proxy response: " + _responseCode + ' ' + _responseText); + } + if (_redirectLocation != null) + throw new IOException("Proxy redirect not allowed"); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("proxy headers read completely"); + + // wrap the socket in an SSLSocket + if (_sslContext != null) + _proxy = _sslContext.getSocketFactory().createSocket(_proxy, host, port, true); + else + _proxy = ((SSLSocketFactory) SSLSocketFactory.getDefault()).createSocket(_proxy, host, port, true); + } else { + // Warning, createSocket() followed by connect(InetSocketAddress) + // disables SNI, at least on Java 7. + // So we must do createSocket(host, port) and then setSoTimeout; + // we can't create a disconnected socket and then call setSoTimeout, sadly. + if (_sslContext != null) + _proxy = _sslContext.getSocketFactory().createSocket(host, port); + else + _proxy = SSLSocketFactory.getDefault().createSocket(host, port); + if (_fetchHeaderTimeout > 0) { + _proxy.setSoTimeout(_fetchHeaderTimeout); + } } + SSLSocket socket = (SSLSocket) _proxy; I2PSSLSocketFactory.setProtocolsAndCiphers(socket); if (!_bypassVerification) { diff --git a/history.txt b/history.txt index d7b21a8b5..7d0bbcd6f 100644 --- a/history.txt +++ b/history.txt @@ -1,4 +1,8 @@ 2017-11-16 zzz + * Reseed: Add HTTPS proxy support (ticket #423) + +2017-11-16 zzz + * Console: Hide Reseed HTTP proxy options if no HTTP URLs (ticket #2007) * i2psnark: Fix bad completion status after recheck (ticket #2046) * Jetty 9.2.22 diff --git a/installer/resources/themes/console/light/console.css b/installer/resources/themes/console/light/console.css index 2df51281c..a4f6c189c 100644 --- a/installer/resources/themes/console/light/console.css +++ b/installer/resources/themes/console/light/console.css @@ -5016,14 +5016,14 @@ table#consolepass input[name="name"] { margin-right: 20px; } -#consolepass input[name="name"], #externali2cp input[name="user"], #reseedconfig input[name="username"], +#consolepass input[name="name"], #externali2cp input[name="user"], #reseedconfig input[name="username"], #reseedconfig input[name="susername"], #consolepass input[name="nofilter_pw"], #externali2cp input[name="nofilter_pw"], #reseedconfig input[type="password"] { padding: 4px 5px 4px 26px !important; background-size: 16px 16px !important; background-blend-mode: luminosity; } -#consolepass input[name="name"], #externali2cp input[name="user"], #reseedconfig input[name="username"] { +#consolepass input[name="name"], #externali2cp input[name="user"], #reseedconfig input[name="username"], #reseedconfig input[name="susername"] { background: #f8f8ff url(/themes/console/images/buttons/user.png) 5px center no-repeat; } diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 282c18b42..0725033fa 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -18,7 +18,7 @@ public class RouterVersion { /** deprecated */ public final static String ID = "Monotone"; public final static String VERSION = CoreVersion.VERSION; - public final static long BUILD = 2; + public final static long BUILD = 3; /** for example "-test" */ public final static String EXTRA = ""; diff --git a/router/java/src/net/i2p/router/networkdb/reseed/Reseeder.java b/router/java/src/net/i2p/router/networkdb/reseed/Reseeder.java index c6198d6c9..8a7b68ca7 100644 --- a/router/java/src/net/i2p/router/networkdb/reseed/Reseeder.java +++ b/router/java/src/net/i2p/router/networkdb/reseed/Reseeder.java @@ -255,8 +255,8 @@ public class Reseeder { private class ReseedRunner implements Runnable, EepGet.StatusListener { private boolean _isRunning; - private String _proxyHost; - private int _proxyPort; + private String _proxyHost, _sproxyHost; + private int _proxyPort, _sproxyPort; private SSLEepGet.SSLState _sslState; private int _gotDate; private long _attemptStarted; @@ -308,6 +308,10 @@ public class Reseeder { _proxyHost = _context.getProperty(PROP_PROXY_HOST); _proxyPort = _context.getProperty(PROP_PROXY_PORT, -1); } + if (_context.getBooleanProperty(PROP_SPROXY_ENABLE)) { + _sproxyHost = _context.getProperty(PROP_SPROXY_HOST); + _sproxyPort = _context.getProperty(PROP_SPROXY_PORT, -1); + } System.out.println("Reseed start"); int total; if (_url != null) { @@ -333,6 +337,10 @@ public class Reseeder { System.out.println("Reseed failed, check network connection"); System.out.println("Ensure that nothing blocks outbound HTTP or HTTPS, check the logs, " + "and if nothing helps, read the FAQ about reseeding manually."); + if (_sproxyHost != null && _sproxyPort > 0) + System.out.println("Check HTTPS proxy setting - host: " + _sproxyHost + " port: " + _sproxyPort); + else + System.out.println("Consider enabling an HTTPS proxy on the reseed configuration page"); } // else < 0, no valid URLs String old = _checker.getError(); _checker.setError(_t("Reseed failed.") + ' ' + @@ -912,21 +920,35 @@ public class Reseeder { EepGet get; boolean ssl = url.toString().startsWith("https"); if (ssl) { + boolean shouldProxy = _sproxyHost != null && _sproxyHost.length() > 0 && _sproxyPort > 0; SSLEepGet sslget; - // TODO SSL PROXY if (_sslState == null) { - sslget = new SSLEepGet(I2PAppContext.getGlobalContext(), baos, url.toString()); + if (shouldProxy) + sslget = new SSLEepGet(_context, SSLEepGet.ProxyType.HTTP, _sproxyHost, _sproxyPort, + baos, url.toString()); + else + sslget = new SSLEepGet(_context, baos, url.toString()); // save state for next time _sslState = sslget.getSSLState(); } else { - sslget = new SSLEepGet(I2PAppContext.getGlobalContext(), baos, url.toString(), _sslState); + if (shouldProxy) + sslget = new SSLEepGet(_context, SSLEepGet.ProxyType.HTTP, _sproxyHost, _sproxyPort, + baos, url.toString(), _sslState); + else + sslget = new SSLEepGet(_context, baos, url.toString(), _sslState); } get = sslget; - // TODO SSL PROXY AUTH + if (shouldProxy && _context.getBooleanProperty(PROP_SPROXY_AUTH_ENABLE)) { + String user = _context.getProperty(PROP_SPROXY_USERNAME); + String pass = _context.getProperty(PROP_SPROXY_PASSWORD); + if (user != null && user.length() > 0 && + pass != null && pass.length() > 0) + get.addAuthorization(user, pass); + } } else { // Do a (probably) non-proxied eepget into our ByteArrayOutputStream with 0 retries boolean shouldProxy = _proxyHost != null && _proxyHost.length() > 0 && _proxyPort > 0; - get = new EepGet(I2PAppContext.getGlobalContext(), shouldProxy, _proxyHost, _proxyPort, 0, 0, MAX_RESEED_RESPONSE_SIZE, + get = new EepGet(_context, shouldProxy, _proxyHost, _proxyPort, 0, 0, MAX_RESEED_RESPONSE_SIZE, null, baos, url.toString(), false, null, null); if (shouldProxy && _context.getBooleanProperty(PROP_PROXY_AUTH_ENABLE)) { String user = _context.getProperty(PROP_PROXY_USERNAME); @@ -957,21 +979,35 @@ public class Reseeder { EepGet get; boolean ssl = url.toString().startsWith("https"); if (ssl) { + boolean shouldProxy = _sproxyHost != null && _sproxyHost.length() > 0 && _sproxyPort > 0; SSLEepGet sslget; - // TODO SSL PROXY if (_sslState == null) { - sslget = new SSLEepGet(I2PAppContext.getGlobalContext(), out.getPath(), url.toString()); + if (shouldProxy) + sslget = new SSLEepGet(_context, SSLEepGet.ProxyType.HTTP, _sproxyHost, _sproxyPort, + out.getPath(), url.toString()); + else + sslget = new SSLEepGet(_context, out.getPath(), url.toString()); // save state for next time _sslState = sslget.getSSLState(); } else { - sslget = new SSLEepGet(I2PAppContext.getGlobalContext(), out.getPath(), url.toString(), _sslState); + if (shouldProxy) + sslget = new SSLEepGet(_context, SSLEepGet.ProxyType.HTTP, _sproxyHost, _sproxyPort, + out.getPath(), url.toString(), _sslState); + else + sslget = new SSLEepGet(_context, out.getPath(), url.toString(), _sslState); } get = sslget; - // TODO SSL PROXY AUTH + if (shouldProxy && _context.getBooleanProperty(PROP_SPROXY_AUTH_ENABLE)) { + String user = _context.getProperty(PROP_SPROXY_USERNAME); + String pass = _context.getProperty(PROP_SPROXY_PASSWORD); + if (user != null && user.length() > 0 && + pass != null && pass.length() > 0) + get.addAuthorization(user, pass); + } } else { // Do a (probably) non-proxied eepget into file with 0 retries boolean shouldProxy = _proxyHost != null && _proxyHost.length() > 0 && _proxyPort > 0; - get = new EepGet(I2PAppContext.getGlobalContext(), shouldProxy, _proxyHost, _proxyPort, 0, 0, MAX_SU3_RESPONSE_SIZE, + get = new EepGet(_context, shouldProxy, _proxyHost, _proxyPort, 0, 0, MAX_SU3_RESPONSE_SIZE, out.getPath(), null, url.toString(), false, null, null); if (shouldProxy && _context.getBooleanProperty(PROP_PROXY_AUTH_ENABLE)) { String user = _context.getProperty(PROP_PROXY_USERNAME);