From 1a385b6dca6051fd33aa158b3d4f05473cd9ddd2 Mon Sep 17 00:00:00 2001 From: zzz <zzz@mail.i2p> Date: Fri, 18 Sep 2015 18:15:32 +0000 Subject: [PATCH] i2ptunnel: - Pass Accept-Encoding header through HTTP client and server proxies, to allow end-to-end compression - Don't do transparent response compression if response Content-Encoding indicates it is already compressed - Minor encoding cleanups EepGet: - Send Accept-Encoding: gzip even when proxied - Minor cleanups --- .../i2ptunnel/HTTPResponseOutputStream.java | 24 ++++++++++++------- .../i2p/i2ptunnel/I2PTunnelHTTPClient.java | 8 ++++--- .../i2p/i2ptunnel/I2PTunnelHTTPServer.java | 23 +++++++++++------- core/java/src/net/i2p/util/EepGet.java | 15 ++++++------ 4 files changed, 42 insertions(+), 28 deletions(-) diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/HTTPResponseOutputStream.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/HTTPResponseOutputStream.java index 83fbb6d0a6..2fc344a714 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/HTTPResponseOutputStream.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/HTTPResponseOutputStream.java @@ -39,7 +39,10 @@ class HTTPResponseOutputStream extends FilterOutputStream { private final byte _buf1[]; protected boolean _gzip; protected long _dataExpected; + /** lower-case, trimmed */ protected String _contentType; + /** lower-case, trimmed */ + protected String _contentEncoding; private static final int CACHE_SIZE = 8*1024; private static final ByteCache _cache = ByteCache.getInstance(8, CACHE_SIZE); @@ -151,7 +154,7 @@ class HTTPResponseOutputStream extends FilterOutputStream { responseLine = (responseLine.trim() + "\r\n"); if (_log.shouldLog(Log.INFO)) _log.info("Response: " + responseLine.trim()); - out.write(responseLine.getBytes()); + out.write(DataHelper.getUTF8(responseLine)); } else { for (int j = lastEnd+1; j < i; j++) { if (_headerBuffer.getData()[j] == ':') { @@ -160,7 +163,7 @@ class HTTPResponseOutputStream extends FilterOutputStream { if ( (keyLen <= 0) || (valLen < 0) ) throw new IOException("Invalid header @ " + j); String key = DataHelper.getUTF8(_headerBuffer.getData(), lastEnd+1, keyLen); - String val = null; + String val; if (valLen == 0) val = ""; else @@ -171,10 +174,10 @@ class HTTPResponseOutputStream extends FilterOutputStream { String lcKey = key.toLowerCase(Locale.US); if ("connection".equals(lcKey)) { - out.write("Connection: close\r\n".getBytes()); + out.write(DataHelper.getASCII("Connection: close\r\n")); connectionSent = true; } else if ("proxy-connection".equals(lcKey)) { - out.write("Proxy-Connection: close\r\n".getBytes()); + out.write(DataHelper.getASCII("Proxy-Connection: close\r\n")); proxyConnectionSent = true; } else if ("content-encoding".equals(lcKey) && "x-i2p-gzip".equals(val.toLowerCase(Locale.US))) { _gzip = true; @@ -189,7 +192,10 @@ class HTTPResponseOutputStream extends FilterOutputStream { } catch (NumberFormatException nfe) {} } else if ("content-type".equals(lcKey)) { // save for compress decision on server side - _contentType = val; + _contentType = val.toLowerCase(Locale.US); + } else if ("content-encoding".equals(lcKey)) { + // save for compress decision on server side + _contentEncoding = val.toLowerCase(Locale.US); } else if ("set-cookie".equals(lcKey)) { String lcVal = val.toLowerCase(Locale.US); if (lcVal.contains("domain=b32.i2p") || @@ -203,7 +209,7 @@ class HTTPResponseOutputStream extends FilterOutputStream { break; } } - out.write((key.trim() + ": " + val.trim() + "\r\n").getBytes()); + out.write(DataHelper.getUTF8(key.trim() + ": " + val + "\r\n")); } break; } @@ -214,9 +220,9 @@ class HTTPResponseOutputStream extends FilterOutputStream { } if (!connectionSent) - out.write("Connection: close\r\n".getBytes()); + out.write(DataHelper.getASCII("Connection: close\r\n")); if (!proxyConnectionSent) - out.write("Proxy-Connection: close\r\n".getBytes()); + out.write(DataHelper.getASCII("Proxy-Connection: close\r\n")); finishHeaders(); @@ -237,7 +243,7 @@ class HTTPResponseOutputStream extends FilterOutputStream { protected boolean shouldCompress() { return _gzip; } protected void finishHeaders() throws IOException { - out.write("\r\n".getBytes()); // end of the headers + out.write(DataHelper.getASCII("\r\n")); // end of the headers } @Override diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java index 74fcaccb1c..8645b686f4 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java @@ -881,7 +881,9 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn } else if(lowercaseLine.startsWith("accept")) { // strip the accept-blah headers, as they vary dramatically from // browser to browser - if(!Boolean.parseBoolean(getTunnel().getClientOptions().getProperty(PROP_ACCEPT))) { + // But allow Accept-Encoding: gzip, deflate + if(!lowercaseLine.startsWith("accept-encoding: ") && + !Boolean.parseBoolean(getTunnel().getClientOptions().getProperty(PROP_ACCEPT))) { line = null; continue; } @@ -933,8 +935,8 @@ public class I2PTunnelHTTPClient extends I2PTunnelHTTPClientBase implements Runn // according to rfc2616 s14.3, this *should* force identity, even if // an explicit q=0 for gzip doesn't. tested against orion.i2p, and it // seems to work. - if(!Boolean.parseBoolean(getTunnel().getClientOptions().getProperty(PROP_ACCEPT))) - newRequest.append("Accept-Encoding: \r\n"); + //if (!Boolean.parseBoolean(getTunnel().getClientOptions().getProperty(PROP_ACCEPT))) + // newRequest.append("Accept-Encoding: \r\n"); if (!usingInternalOutproxy) newRequest.append("X-Accept-Encoding: x-i2p-gzip;q=1.0, identity;q=0.5, deflate;q=0, gzip;q=0, *;q=0\r\n"); } diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java index 6b3b417201..35dd6f1326 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java @@ -419,12 +419,14 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer { setEntry(headers, "Connection", "close"); // we keep the enc sent by the browser before clobbering it, since it may have // been x-i2p-gzip - String enc = getEntryOrNull(headers, "Accept-encoding"); - String altEnc = getEntryOrNull(headers, "X-Accept-encoding"); + String enc = getEntryOrNull(headers, "Accept-Encoding"); + String altEnc = getEntryOrNull(headers, "X-Accept-Encoding"); // according to rfc2616 s14.3, this *should* force identity, even if // "identity;q=1, *;q=0" didn't. - setEntry(headers, "Accept-encoding", ""); + // as of 0.9.23, the client passes this header through, and we do the same, + // so if the server and browser can do the compression/decompression, we don't have to + //setEntry(headers, "Accept-Encoding", ""); socket.setReadTimeout(readTimeout); Socket s = getSocket(socket.getPeerDestination().calculateHash(), socket.getLocalPort()); @@ -432,7 +434,7 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer { // instead of i2ptunnelrunner, use something that reads the HTTP // request from the socket, modifies the headers, sends the request to the // server, reads the response headers, rewriting to include Content-encoding: x-i2p-gzip - // if it was one of the Accept-encoding: values, and gzip the payload + // if it was one of the Accept-Encoding: values, and gzip the payload boolean allowGZIP = true; String val = opts.getProperty("i2ptunnel.gzip"); if ( (val != null) && (!Boolean.parseBoolean(val)) ) @@ -443,7 +445,7 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer { boolean useGZIP = alt || ( (enc != null) && (enc.indexOf("x-i2p-gzip") >= 0) ); // Don't pass this on, outproxies should strip so I2P traffic isn't so obvious but they probably don't if (alt) - headers.remove("X-Accept-encoding"); + headers.remove("X-Accept-Encoding"); String modifiedHeader = formatHeaders(headers, command); if (_log.shouldLog(Log.DEBUG)) @@ -671,6 +673,7 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer { /** * Don't compress small responses or images. + * Don't compress things that are already compressed. * Compression is inline but decompression on the client side * creates a new thread. */ @@ -687,7 +690,11 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer { (!_contentType.equals("application/x-bzip")) && (!_contentType.equals("application/x-bzip2")) && (!_contentType.equals("application/x-gzip")) && - (!_contentType.equals("application/zip")))); + (!_contentType.equals("application/zip")))) && + (_contentEncoding == null || + ((!_contentEncoding.equals("gzip")) && + (!_contentEncoding.equals("compress")) && + (!_contentEncoding.equals("deflate")))); } @Override @@ -877,9 +884,9 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer { String lcName = name.toLowerCase(Locale.US); if ("accept-encoding".equals(lcName)) - name = "Accept-encoding"; + name = "Accept-Encoding"; else if ("x-accept-encoding".equals(lcName)) - name = "X-Accept-encoding"; + name = "X-Accept-Encoding"; else if ("x-forwarded-for".equals(lcName)) name = "X-Forwarded-For"; else if ("x-forwarded-server".equals(lcName)) diff --git a/core/java/src/net/i2p/util/EepGet.java b/core/java/src/net/i2p/util/EepGet.java index c31184dbdd..a098a997a3 100644 --- a/core/java/src/net/i2p/util/EepGet.java +++ b/core/java/src/net/i2p/util/EepGet.java @@ -755,6 +755,8 @@ public class EepGet { Thread pusher = null; _decompressException = null; if (_isGzippedResponse) { + if (_log.shouldInfo()) + _log.info("Gzipped response, starting decompressor"); PipedInputStream pi = BigPipedInputStream.getInstance(); PipedOutputStream po = new PipedOutputStream(pi); pusher = new I2PAppThread(new Gunzipper(pi, _out), "EepGet Decompressor"); @@ -1160,17 +1162,13 @@ public class EepGet { lookahead[1] = lookahead[2]; lookahead[2] = (byte)cur; } + private static boolean isEndOfHeaders(byte lookahead[]) { - byte first = lookahead[0]; - byte second = lookahead[1]; - byte third = lookahead[2]; - return (isNL(second) && isNL(third)) || // \n\n - (isNL(first) && isNL(third)); // \n\r\n + return lookahead[2] == NL && + (lookahead[0] == NL || lookahead[1] == NL); // \n\n or \n\r\n } - /** we ignore any potential \r, since we trim it on write anyway */ private static final byte NL = '\n'; - private static boolean isNL(byte b) { return (b == NL); } /** * @param timeout may be null @@ -1315,7 +1313,8 @@ public class EepGet { buf.append("Content-length: ").append(_postData.length()).append("\r\n"); // This will be replaced if we are going through I2PTunnelHTTPClient buf.append("Accept-Encoding: "); - if ((!_shouldProxy) && + // as of 0.9.23, the proxy passes the Accept-Encoding header through + if ( /* (!_shouldProxy) && */ // This is kindof a hack, but if we are downloading a gzip file // we don't want to transparently gunzip it and save it as a .gz file. (!path.endsWith(".gz")) && (!path.endsWith(".tgz"))) -- GitLab