This is an automated email from the ASF dual-hosted git repository.

tomaswolf pushed a commit to branch dev_3.0
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git


The following commit(s) were added to refs/heads/dev_3.0 by this push:
     new 68f0a95fd Handle fragmented proxy responses
68f0a95fd is described below

commit 68f0a95fd6864ac69ac24e8ec6dba6b1b5f084f8
Author: Thomas Wolf <[email protected]>
AuthorDate: Tue Sep 22 21:32:08 2026 +0200

    Handle fragmented proxy responses
    
    If the response is not properly terminated by two CRLFs then wait for
    more data until we have the full headers. Accumulate data only until we
    do have the two CRLFs, or otherwise up to a fixed maximum limit.
---
 .../sshd/client/session/proxy/Accumulator.java     | 114 +++++++++++++++++++++
 .../client/session/proxy/HttpProxyConnector.java   |  34 +++---
 .../sshd/client/session/proxy/AccumulatorTest.java |  92 +++++++++++++++++
 .../sshd/client/session/proxy/HttpParserTest.java  |   2 +
 4 files changed, 226 insertions(+), 16 deletions(-)

diff --git 
a/sshd-core/src/main/java/org/apache/sshd/client/session/proxy/Accumulator.java 
b/sshd-core/src/main/java/org/apache/sshd/client/session/proxy/Accumulator.java
new file mode 100644
index 000000000..2e21200c2
--- /dev/null
+++ 
b/sshd-core/src/main/java/org/apache/sshd/client/session/proxy/Accumulator.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sshd.client.session.proxy;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import org.apache.sshd.common.util.Readable;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+
+/**
+ * Accumulates bytes until the first occurrence of a given byte sequence.
+ */
+class Accumulator {
+
+    private final int limit;
+
+    private final byte[] endOfData;
+
+    private final String errorMessage;
+
+    private ByteArrayBuffer buf = new ByteArrayBuffer();
+
+    private int split = -1;
+
+    Accumulator(int limit, byte[] endOfData, String errorMessage) {
+        ValidateUtils.checkTrue(endOfData.length > 0, "end marker must be 
non-empty");
+        ValidateUtils.checkTrue(limit >= endOfData.length, "limit must be >= 
length of end marker");
+        this.endOfData = endOfData.clone();
+        this.limit = limit;
+        this.errorMessage = errorMessage;
+    }
+
+    /**
+     * Accumulates the given input and returns whether the end marker was 
found.
+     *
+     * @param  input       to accumulate
+     * @return             {@code true} if the data accumulated so far 
contains the end marker, {@code false} otherwise
+     * @throws IOException if the limit is exceeded
+     */
+    boolean accumulate(Readable input) throws IOException {
+        int lastCheck = buf.available();
+        buf.putBuffer(input);
+        int len = buf.available();
+        if (split < 0 && len > lastCheck && len >= endOfData.length) {
+            // Avoid re-checking already checked parts of the buffer. If the 
last check didn't find the end marker up to
+            // lastCheck, we don't have to start over again at the beginning: 
it suffices to double check the last N-1
+            // bytes (plus then the new bytes) if N is endOfData.length.
+            int from = (lastCheck >= endOfData.length) ? lastCheck + 1 - 
endOfData.length : buf.rpos();
+            split = indexOf(buf.array(), from, buf.rpos() + len - from, 
endOfData);
+            if (split >= 0) {
+                return true;
+            }
+        }
+        if (len > limit) {
+            if (split >= 0) {
+                throw new IOException("Limit exceeded after marker was found 
at index " + split + "; limit = " + limit);
+            } else {
+                throw new IOException("Accumulated too much data (max. " + 
limit + " bytes): " + errorMessage);
+            }
+        }
+        return split >= 0;
+    }
+
+    byte[] getData() {
+        if (split < 0) {
+            throw new IllegalStateException("Data not yet determined");
+        }
+        return Arrays.copyOfRange(buf.array(), buf.rpos(), split + 
endOfData.length);
+    }
+
+    byte[] getRest() {
+        if (split < 0) {
+            throw new IllegalStateException("Data not yet determined, so rest 
still unknown");
+        }
+        return Arrays.copyOfRange(buf.array(), split + endOfData.length, 
buf.wpos());
+    }
+
+    void clear() {
+        buf = new ByteArrayBuffer();
+        split = -1;
+    }
+
+    private static int indexOf(byte[] data, int from, int len, byte[] needle) {
+        int to = from + len - needle.length;
+        for (int i = from; i <= to; i++) {
+            int j = 0;
+            for (int k = i; j < needle.length && data[k] == needle[j]; k++) {
+                j++;
+            }
+            if (j == needle.length) {
+                return i;
+            }
+        }
+        return -1;
+    }
+}
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/client/session/proxy/HttpProxyConnector.java
 
b/sshd-core/src/main/java/org/apache/sshd/client/session/proxy/HttpProxyConnector.java
index 75d981fac..1f006f5b3 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/client/session/proxy/HttpProxyConnector.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/client/session/proxy/HttpProxyConnector.java
@@ -57,6 +57,11 @@ public class HttpProxyConnector extends 
AbstractProxyConnector {
 
     private static final String HTTP_HEADER_PROXY_AUTHORIZATION = 
"Proxy-Authorization:";
 
+    /**
+     * Maximum allowed length of the HTTP reply (status line + headers, 
including the final CRLF).
+     */
+    private static final int MAX_REPLY_LENGTH = 32 * 1024;
+
     private HttpAuthenticationHandler basic;
 
     private HttpAuthenticationHandler negotiate;
@@ -69,6 +74,8 @@ public class HttpProxyConnector extends 
AbstractProxyConnector {
 
     private boolean ongoing;
 
+    private Accumulator data;
+
     /**
      * Creates a new {@link HttpProxyConnector}. The connector supports 
anonymous proxy connections as well as Basic and
      * Negotiate (SPNEGO) authentication.
@@ -88,6 +95,7 @@ public class HttpProxyConnector extends 
AbstractProxyConnector {
         availableAuthentications.add(negotiate);
         availableAuthentications.add(basic);
         clientAuthentications = availableAuthentications.iterator();
+        data = new Accumulator(MAX_REPLY_LENGTH, new byte[] { '\r', '\n', 
'\r', '\n' }, "HTTP headers too large");
     }
 
     @Override
@@ -147,24 +155,18 @@ public class HttpProxyConnector extends 
AbstractProxyConnector {
     @Override
     public Buffer received(Readable buffer) throws Exception {
         try {
-            int length = buffer.available();
-            byte[] data = new byte[length];
-            buffer.getRawBytes(data, 0, length);
-            Buffer rest = null;
-            // HTTP responses end with two CRLFs. Find them.
-            String msg = new String(data, US_ASCII);
-            int end = msg.indexOf("\r\n\r\n");
-            if (end < 0) {
-                end = data.length;
-            } else {
-                end += 4;
+            if (!data.accumulate(buffer)) {
+                // Need more input
+                return null;
             }
-            if (end < data.length) {
-                msg = msg.substring(0, end);
-                data = Arrays.copyOfRange(data, end, data.length);
-                rest = new ByteArrayBuffer(data);
+            byte[] headers = data.getData();
+            byte[] body = data.getRest();
+            data.clear();
+            Buffer rest = null;
+            if (body.length > 0) {
+                rest = new ByteArrayBuffer(body);
             }
-            String[] reply = msg.split("\r\n");
+            String[] reply = new String(headers, US_ASCII).split("\r\n");
             handleMessage(Arrays.asList(reply));
             return rest;
         } catch (Exception e) {
diff --git 
a/sshd-core/src/test/java/org/apache/sshd/client/session/proxy/AccumulatorTest.java
 
b/sshd-core/src/test/java/org/apache/sshd/client/session/proxy/AccumulatorTest.java
new file mode 100644
index 000000000..f9cb75d97
--- /dev/null
+++ 
b/sshd-core/src/test/java/org/apache/sshd/client/session/proxy/AccumulatorTest.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sshd.client.session.proxy;
+
+import java.nio.charset.StandardCharsets;
+
+import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+import org.apache.sshd.util.test.JUnitTestSupport;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Basic tests for {@link Accumulator}.
+ */
+@Tag("NoIoTestCase")
+class AccumulatorTest extends JUnitTestSupport {
+
+    private static final byte[] END = { '\r', '\n', '\r', '\n' };
+
+    @Test
+    void normalInput() throws Exception {
+        byte[] data = "Complete 
line\r\nHeader:foo\r\nOther:bar\r\n\r\nSomething else\r\n\r\nanother line"
+                .getBytes(StandardCharsets.US_ASCII);
+        Accumulator acc = new Accumulator(100, END, "Headers too long");
+        assertTrue(acc.accumulate(new ByteArrayBuffer(data)));
+        String headers = new String(acc.getData(), StandardCharsets.US_ASCII);
+        String body = new String(acc.getRest(), StandardCharsets.US_ASCII);
+        assertEquals("Something else\r\n\r\nanother line", body);
+        assertArrayEquals(data, (headers + 
body).getBytes(StandardCharsets.US_ASCII));
+    }
+
+    @Test
+    void noBody() throws Exception {
+        byte[] data = "Complete 
line\r\nHeader:foo\r\nOther:bar\r\n\r\n".getBytes(StandardCharsets.US_ASCII);
+        Accumulator acc = new Accumulator(100, END, "Headers too long");
+        assertTrue(acc.accumulate(new ByteArrayBuffer(data)));
+        String headers = new String(acc.getData(), StandardCharsets.US_ASCII);
+        String body = new String(acc.getRest(), StandardCharsets.US_ASCII);
+        assertEquals("", body);
+        assertArrayEquals(data, headers.getBytes(StandardCharsets.US_ASCII));
+    }
+
+    @Test
+    void fragmentedInput() throws Exception {
+        byte[] data = "Complete 
line\r\nHeader:foo\r\nOther:bar\r\n\r\nSomething else\r\n\r\nanother line"
+                .getBytes(StandardCharsets.US_ASCII);
+        Accumulator acc = new Accumulator(100, END, "Headers too long");
+        for (int i = 0; i < data.length; i++) {
+            if (i < 39) {
+                assertFalse(acc.accumulate(new ByteArrayBuffer(data, i, 1)), 
"Failed false at " + i);
+            } else {
+                assertTrue(acc.accumulate(new ByteArrayBuffer(data, i, 1)), 
"Failed  true at " + i);
+            }
+        }
+        String headers = new String(acc.getData(), StandardCharsets.US_ASCII);
+        String body = new String(acc.getRest(), StandardCharsets.US_ASCII);
+        assertEquals("Something else\r\n\r\nanother line", body);
+        assertArrayEquals(data, (headers + 
body).getBytes(StandardCharsets.US_ASCII));
+    }
+
+    @Test
+    void limitExceeded() throws Exception {
+        byte[] data = new byte[11];
+        Accumulator acc = new Accumulator(10, END, "Headers too long");
+        assertThrows(Exception.class, () -> acc.accumulate(new 
ByteArrayBuffer(data)));
+    }
+
+    @Test
+    void limitExceededFragmented() throws Exception {
+        byte[] data = new byte[8];
+        Accumulator acc = new Accumulator(10, END, "Headers too long");
+        assertFalse(acc.accumulate(new ByteArrayBuffer(data)));
+        assertThrows(Exception.class, () -> acc.accumulate(new 
ByteArrayBuffer(data)));
+    }
+
+}
diff --git 
a/sshd-core/src/test/java/org/apache/sshd/client/session/proxy/HttpParserTest.java
 
b/sshd-core/src/test/java/org/apache/sshd/client/session/proxy/HttpParserTest.java
index 84305ec3a..101e2b7d9 100644
--- 
a/sshd-core/src/test/java/org/apache/sshd/client/session/proxy/HttpParserTest.java
+++ 
b/sshd-core/src/test/java/org/apache/sshd/client/session/proxy/HttpParserTest.java
@@ -24,11 +24,13 @@ import java.util.List;
 import java.util.Map;
 
 import org.apache.sshd.util.test.JUnitTestSupport;
+import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 
 /**
  * Basic tests for {@link HttpParser}.
  */
+@Tag("NoIoTestCase")
 class HttpParserTest extends JUnitTestSupport {
 
     private static final String STATUS_LINE = "HTTP/1.1. 407 Authentication 
required";

Reply via email to