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