From e5b9f9f1348ba4643e5057259aec67a09df869a5 Mon Sep 17 00:00:00 2001 From: zzz <zzz@mail.i2p> Date: Sun, 14 Jan 2024 11:42:13 +0000 Subject: [PATCH] i2ptunnel: Prep for keepalive --- .../i2ptunnel/HTTPResponseOutputStream.java | 1 + .../localServer/LocalHTTPServer.java | 2 +- .../i2ptunnel/util/ByteLimitOutputStream.java | 77 +++++++ .../i2ptunnel/util/DechunkedOutputStream.java | 188 ++++++++++++++++++ .../i2p/i2ptunnel/util/DummyOutputStream.java | 25 +++ .../{ => util}/GunzipOutputStream.java | 33 ++- .../i2p/i2ptunnel/util/LimitOutputStream.java | 58 ++++++ .../src/net/i2p/i2ptunnel/util/package.html | 9 + 8 files changed, 389 insertions(+), 4 deletions(-) create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/util/ByteLimitOutputStream.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/util/DechunkedOutputStream.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/util/DummyOutputStream.java rename apps/i2ptunnel/java/src/net/i2p/i2ptunnel/{ => util}/GunzipOutputStream.java (91%) create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/util/LimitOutputStream.java create mode 100644 apps/i2ptunnel/java/src/net/i2p/i2ptunnel/util/package.html diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/HTTPResponseOutputStream.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/HTTPResponseOutputStream.java index 75cdde926e..0fd3c538b7 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/HTTPResponseOutputStream.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/HTTPResponseOutputStream.java @@ -16,6 +16,7 @@ import java.util.Locale; import net.i2p.I2PAppContext; import net.i2p.data.ByteArray; import net.i2p.data.DataHelper; +import net.i2p.i2ptunnel.util.GunzipOutputStream; import net.i2p.util.ByteCache; import net.i2p.util.Log; diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/localServer/LocalHTTPServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/localServer/LocalHTTPServer.java index dd1e0f9eab..1b00b680c4 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/localServer/LocalHTTPServer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/localServer/LocalHTTPServer.java @@ -28,7 +28,7 @@ import net.i2p.data.Destination; import net.i2p.data.PrivateKey; import net.i2p.data.PublicKey; import net.i2p.data.SigningPublicKey; -import net.i2p.i2ptunnel.GunzipOutputStream; +import net.i2p.i2ptunnel.util.GunzipOutputStream; import net.i2p.i2ptunnel.I2PTunnelHTTPClientBase; import net.i2p.util.FileUtil; import net.i2p.util.PortMapper; diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/util/ByteLimitOutputStream.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/util/ByteLimitOutputStream.java new file mode 100644 index 0000000000..4963c2718c --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/util/ByteLimitOutputStream.java @@ -0,0 +1,77 @@ +package net.i2p.i2ptunnel.util; + +import java.io.EOFException; +import java.io.IOException; +import java.io.OutputStream; + +/** + * An OutputStream that limits how many bytes are written + * + * @since 0.9.62 + */ +public class ByteLimitOutputStream extends LimitOutputStream { + + private final long _limit; + private long _count; + + /** + * @param limit greater than zero + */ + public ByteLimitOutputStream(OutputStream out, DoneCallback done, long limit) { + super(out, done); + if (limit <= 0) + throw new IllegalArgumentException(); + _limit = limit; + } + + @Override + public void write(byte src[], int off, int len) throws IOException { + if (len == 0) + return; + if (_isDone) + throw new EOFException("done"); + long togo = _limit - _count; + boolean last = len >= togo; + if (last) + len = (int) togo; + super.write(src, off, len); + _count += len; + if (last) + setDone(); + } + +/* + public static void main(String[] args) throws Exception { + if (args.length != 1) { + System.err.println("Usage: ByteLimitOutputStream length < in > out"); + System.exit(1); + } + Test test = new Test(); + long limit = Long.parseLong(args[0]); + test.test(limit); + } + + static class Test implements DoneCallback { + private boolean run = true; + + public void test(long limit) throws Exception { + LimitOutputStream lout = new ByteLimitOutputStream(System.out, this, limit); + final byte buf[] = new byte[4096]; + try { + int read; + while (run && (read = System.in.read(buf)) != -1) { + lout.write(buf, 0, read); + } + } finally { + lout.close(); + } + } + + public void streamDone() { + System.err.println("Done"); + run = false; + } + } +*/ + +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/util/DechunkedOutputStream.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/util/DechunkedOutputStream.java new file mode 100644 index 0000000000..aa1a72ba47 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/util/DechunkedOutputStream.java @@ -0,0 +1,188 @@ +package net.i2p.i2ptunnel.util; + +import java.io.EOFException; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import net.i2p.data.DataHelper; + +/** + * Simple stream for checking and optionally removing RFC2616 chunked encoding to the output. + * + * @since 0.9.62 + */ +public class DechunkedOutputStream extends LimitOutputStream { + private final boolean _strip; + private State _state = State.LEN; + // During main part, how much is remaining in the chunk + // During the trailer, counts the trailer header size + private int _remaining; + + private static final byte[] CRLF = DataHelper.getASCII("\r\n"); + + private enum State { LEN, CR, LF, DATA, TRAILER, DONE } + + public DechunkedOutputStream(OutputStream raw, DoneCallback callback, boolean strip) { + super(raw, callback); + _strip = strip; + } + + @Override + public void write(byte buf[], int off, int len) throws IOException { + if (len <= 0) + return; + + for (int i = 0; i < len; i++) { + // _state is what we are expecting next + //System.err.println("State: " + _state + " i=" + i + " len=" + len + " remaining=" + _remaining + " char=0x" + Integer.toHexString(buf[off + i] & 0xff)); + switch (_state) { + // collect chunk len and possible ';' then wait for extension if any and CRLF + case LEN: { + int c = buf[off + i] & 0xff; + if (c >= '0' && c <= '9') { + if (_remaining >= 0x8000000) + throw new IOException("Chunk length too big"); + _remaining <<= 4; + _remaining |= c - '0'; + } else if (c >= 'a' && c <= 'f') { + if (_remaining >= 0x800000) + throw new IOException("Chunk length too big"); + _remaining <<= 4; + _remaining |= 10 + c - 'a'; + } else if (c >= 'A' && c <= 'F') { + if (_remaining >= 0x800000) + throw new IOException("Chunk length too big"); + _remaining <<= 4; + _remaining |= 10 + c - 'A'; + } else if (c == ';') { + _state = State.CR; + } else if (c == '\r') { + _state = State.LF; + } else if (c == '\n') { + if (_remaining > 0) + _state = State.DATA; + else + _state = State.TRAILER; + } else { + throw new IOException("Unexpected length char 0x" + Integer.toHexString(c)); + } + if (!_strip) + out.write(buf, off + i, 1); + break; + } + + // collect any chunk extension and CR then wait for LF + case CR: { + int c = buf[off + i] & 0xff; + if (c == '\r') { + _state = State.LF; + } else if (c == '\n') { + if (_remaining > 0) + _state = State.DATA; + else + _state = State.TRAILER; + } else { + // chunk extension between the ';' and the CR + } + if (!_strip) + out.write(buf, off + i, 1); + break; + } + + // collect LF then wait for DATA + case LF: { + int c = buf[off + i] & 0xff; + if (c == '\n') { + if (_remaining > 0) + _state = State.DATA; + else + _state = State.TRAILER; + } else { + throw new IOException("no LF after CR"); + } + if (!_strip) + out.write(buf, off + i, 1); + break; + } + + // collect DATA then wait for LEN + case DATA: { + int towrite = Math.min(_remaining, len - i); + out.write(buf, off + i, towrite); + // loop will increment + i += towrite - 1; + _remaining -= towrite; + if (_remaining <= 0) + _state = State.LEN; + break; + } + + // swallow and discard the Trailer headers until we find a plain CRLF + // we reuse _remaining here to count the size of the header + case TRAILER: { + int c = buf[off + i] & 0xff; + if (c == '\r') { + // stay here + } else if (c == '\n') { + if (_remaining <= 0) { + // that's it! + if (!_strip) + out.write(buf, off + i, 1); + _state = State.DONE; + setDone(); + return; + } else { + // stay here + _remaining = 0; + } + } else { + _remaining++; + } + if (!_strip) + out.write(buf, off + i, 1); + break; + } + + case DONE: { + throw new EOFException((len - i) + " extra bytes written after chunking done"); + } + } + } + } + +/* + public static void main(String[] args) throws Exception { + if (args.length != 1) { + System.err.println("Usage: DechunkedOutputStream true/false < in > out"); + System.exit(1); + } + Test test = new Test(); + boolean strip = Boolean.parseBoolean(args[0]); + test.test(strip); + } + + static class Test implements DoneCallback { + private boolean run = true; + + public void test(boolean strip) throws Exception { + LimitOutputStream cout = new DechunkedOutputStream(System.out, this, strip); + final byte buf[] = new byte[4096]; + try { + int read; + while (run && (read = System.in.read(buf)) != -1) { + cout.write(buf, 0, read); + } + } finally { + cout.close(); + } + } + + public void streamDone() { + System.err.println("Done"); + run = false; + } + } +*/ + +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/util/DummyOutputStream.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/util/DummyOutputStream.java new file mode 100644 index 0000000000..fa8393eadb --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/util/DummyOutputStream.java @@ -0,0 +1,25 @@ +package net.i2p.i2ptunnel.util; + +import java.io.OutputStream; + +/** + * Write to nowhere + * + * @since 0.9.62 copied from susimail + */ +public class DummyOutputStream extends OutputStream { + + public void write(int val) {} + + @Override + public void write(byte src[]) {} + + @Override + public void write(byte src[], int off, int len) {} + + @Override + public void flush() {} + + @Override + public void close() {} +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/GunzipOutputStream.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/util/GunzipOutputStream.java similarity index 91% rename from apps/i2ptunnel/java/src/net/i2p/i2ptunnel/GunzipOutputStream.java rename to apps/i2ptunnel/java/src/net/i2p/i2ptunnel/util/GunzipOutputStream.java index 705e7a37a7..cca91dcc9f 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/GunzipOutputStream.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/util/GunzipOutputStream.java @@ -1,5 +1,6 @@ -package net.i2p.i2ptunnel; +package net.i2p.i2ptunnel.util; +import java.io.EOFException; import java.io.FilterOutputStream; import java.io.IOException; import java.io.OutputStream; @@ -7,7 +8,10 @@ import java.util.zip.CRC32; import java.util.zip.Inflater; import java.util.zip.InflaterOutputStream; +import net.i2p.I2PAppContext; import net.i2p.data.DataHelper; +import net.i2p.i2ptunnel.util.LimitOutputStream.DoneCallback; +import net.i2p.util.Log; /** * Gunzip implementation per @@ -22,7 +26,7 @@ import net.i2p.data.DataHelper; * Not a public API, subject to change, not for external use. * * Modified from net.i2p.util.ResettableGZIPInputStream to use Java 6 InflaterOutputstream - * @since 0.9.21, public since 0.9.50 for LocalHTTPServer + * @since 0.9.21, public since 0.9.50 for LocalHTTPServer, moved to util in 0.9.62 */ public class GunzipOutputStream extends InflaterOutputStream { private static final int FOOTER_SIZE = 8; // CRC32 + ISIZE @@ -38,12 +42,28 @@ public class GunzipOutputStream extends InflaterOutputStream { private HeaderState _state = HeaderState.MB1; private int _flags; private int _extHdrToRead; + private final DoneCallback _callback; + private final Log _log; + private static final OutputStream DUMMY_OUT = new DummyOutputStream(); + /** * Build a new Gunzip stream */ public GunzipOutputStream(OutputStream uncompressedStream) throws IOException { + this(uncompressedStream, null); + } + + /** + * With a callback when done + * + * @param cb may be null + * @since 0.9.62 + */ + public GunzipOutputStream(OutputStream uncompressedStream, DoneCallback cb) throws IOException { super(new CRC32OutputStream(uncompressedStream), new Inflater(true)); + _log = I2PAppContext.getGlobalContext().logManager().getLog(GunzipOutputStream.class); + _callback = cb; } @Override @@ -57,7 +77,10 @@ public class GunzipOutputStream extends InflaterOutputStream { if (_complete) { // shortcircuit so the inflater doesn't try to refill // with the footer's data (which would fail, causing ZLIB err) - return; + IOException ioe = new EOFException("Extra data written to gunzipper"); + if (_log.shouldWarn()) + _log.warn("EOF", ioe); + throw ioe; } boolean isFinished = inf.finished(); for (int i = off; i < off + len; i++) { @@ -85,6 +108,8 @@ public class GunzipOutputStream extends InflaterOutputStream { verifyFooter(); _complete = true; _validated = true; + if (_callback != null) + _callback.streamDone(); return; } catch (IOException ioe) { // failed at 7, retry at 8 @@ -147,6 +172,8 @@ public class GunzipOutputStream extends InflaterOutputStream { @Override public void close() throws IOException { + if (_log.shouldWarn()) + _log.warn("Closing " + this); _complete = true; _state = HeaderState.DONE; super.close(); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/util/LimitOutputStream.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/util/LimitOutputStream.java new file mode 100644 index 0000000000..37f9f0f2c7 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/util/LimitOutputStream.java @@ -0,0 +1,58 @@ +package net.i2p.i2ptunnel.util; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * Base class for limiting writes and calling a callback when finished + * + * @since 0.9.62 + */ +public abstract class LimitOutputStream extends FilterOutputStream { + + private final byte _buf1[]; + protected final DoneCallback _callback; + protected boolean _isDone; + + public interface DoneCallback { public void streamDone(); } + + /** + * @param done non-null + */ + public LimitOutputStream(OutputStream out, DoneCallback done) { + super(out); + _callback = done; + _buf1 = new byte[1]; + } + + @Override + public void write(int c) throws IOException { + _buf1[0] = (byte)c; + write(_buf1, 0, 1); + } + + /** + * Subclasses MUST override the following method + * such that it calls done() when finished + * and throws EOFException if called again + */ + @Override + public void write(byte buf[], int off, int len) throws IOException { + out.write(buf, off, len); + } + + + protected boolean isDone() { return _isDone; } + + /** + * flush(), call the callback, and set _isDone + */ + protected void setDone() throws IOException { + if (_isDone) + throw new IllegalStateException("already done"); + flush(); + _callback.streamDone(); + _isDone = true; + } +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/util/package.html b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/util/package.html new file mode 100644 index 0000000000..2570040b51 --- /dev/null +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/util/package.html @@ -0,0 +1,9 @@ +<html> +<body> +<p> +HTTP utilities. +Not for external use; not maintained as a stable API. +Since 0.9.62 +</p> +</body> +</html> -- GitLab