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";