From f40a359df36edef8df5a72e3c072967924636264 Mon Sep 17 00:00:00 2001 From: Jaikiran Pai Date: Mon, 6 Apr 2026 13:21:44 +0000 Subject: [PATCH] 8373778: java.util.NoSuchElementException in HttpURLConnection.doTunneling0 when connecting via HTTPS Reviewed-by: michaelm, vyazici --- .../www/protocol/http/HttpURLConnection.java | 43 ++- .../HttpURLConnection/ProxyBadStatusLine.java | 266 ++++++++++++++++++ 2 files changed, 306 insertions(+), 3 deletions(-) create mode 100644 test/jdk/java/net/HttpURLConnection/ProxyBadStatusLine.java diff --git a/src/java.base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java b/src/java.base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java index 3a915cf96df..480553e9a62 100644 --- a/src/java.base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java +++ b/src/java.base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java @@ -1924,9 +1924,15 @@ public class HttpURLConnection extends java.net.HttpURLConnection { } statusLine = responses.getValue(0); - StringTokenizer st = new StringTokenizer(statusLine); - st.nextToken(); - respCode = Integer.parseInt(st.nextToken().trim()); + respCode = parseConnectResponseCode(statusLine); + if (respCode == -1) { + // a respCode of -1, due to a invalid status line, + // will (rightly) result in an IOException being thrown + // later in this code. here we merely log the invalid status line. + if (logger.isLoggable(PlatformLogger.Level.FINE)) { + logger.fine("invalid status line: \"" + statusLine + "\""); + } + } if (respCode == HTTP_PROXY_AUTH) { // Read comments labeled "Failed Negotiate" for details. boolean dontUseNegotiate = false; @@ -2027,6 +2033,37 @@ public class HttpURLConnection extends java.net.HttpURLConnection { responses.reset(); } + // parses the status line, that was returned for a CONNECT request, and returns + // the response code from that line. returns -1 if the response code could not be + // parsed. + private static int parseConnectResponseCode(final String statusLine) { + final int invalidStatusLine = -1; + if (statusLine == null || statusLine.isBlank()) { + return invalidStatusLine; + } + // + // status-line = HTTP-version SP status-code SP [ reason-phrase ] + // SP = space character + // + final StringTokenizer st = new StringTokenizer(statusLine, " "); + if (!st.hasMoreTokens()) { + return invalidStatusLine; + } + st.nextToken(); // the HTTP version part (ex: HTTP/1.1) + if (!st.hasMoreTokens()) { + return invalidStatusLine; + } + final String v = st.nextToken().trim(); // status code + try { + return Integer.parseInt(v); + } catch (NumberFormatException nfe) { + if (logger.isLoggable(PlatformLogger.Level.FINE)) { + logger.fine("invalid response code: " + v); + } + } + return invalidStatusLine; + } + /** * Overridden in https to also include the server certificate */ diff --git a/test/jdk/java/net/HttpURLConnection/ProxyBadStatusLine.java b/test/jdk/java/net/HttpURLConnection/ProxyBadStatusLine.java new file mode 100644 index 00000000000..be881030715 --- /dev/null +++ b/test/jdk/java/net/HttpURLConnection/ProxyBadStatusLine.java @@ -0,0 +1,266 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.URL; +import java.util.List; + +import jdk.test.lib.net.URIBuilder; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import static java.net.Proxy.Type.HTTP; +import static java.nio.charset.StandardCharsets.ISO_8859_1; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/* @test + * @bug 8373778 + * @summary Verify that a IOException gets thrown from HttpURLConnection, if the proxy returns + * an invalid status line in response to a CONNECT request + * @library /test/lib + * @build jdk.test.lib.net.URIBuilder + * @run junit ${test.main.class} + */ +class ProxyBadStatusLine { + + static List badStatusLines() { + return List.of( + Arguments.of("", "Unexpected end of file from server"), + Arguments.of(" ", "Unexpected end of file from server"), + Arguments.of("\t", "Unexpected end of file from server"), + Arguments.of("\r\n", "Unexpected end of file from server"), + + Arguments.of("HTTP/1.", "Unable to tunnel through proxy"), + Arguments.of("HTTP/1.1", "Unable to tunnel through proxy"), + Arguments.of("HTTP/1.0", "Unable to tunnel through proxy"), + Arguments.of("HTTP/1.1 ", "Unable to tunnel through proxy"), + Arguments.of("HTTP/1.1\r\n", "Unable to tunnel through proxy"), + Arguments.of("HTTP/1.1\n", "Unable to tunnel through proxy"), + Arguments.of("HTTP/1.1 301 ", "Unable to tunnel through proxy"), + Arguments.of("HTTP/1.1 404 ", "Unable to tunnel through proxy"), + Arguments.of("HTTP/1.1 503 ", "Unable to tunnel through proxy"), + Arguments.of("HTTP/1.1\n200 ", "Unable to tunnel through proxy"), + Arguments.of("HTTP/1.1\r200 ", "Unable to tunnel through proxy"), + Arguments.of("HTTP/1.1\f200 ", "Unable to tunnel through proxy") + ); + } + + /* + * Uses HttpURLConnection to initiate a HTTP request that results in a CONNECT + * request to a proxy server. The proxy server then responds with a bad status line. + * The test expects that an IOException gets thrown back to the application (instead + * of some unspecified exception). + */ + @ParameterizedTest + @MethodSource(value = "badStatusLines") + void testProxyConnectResponse(final String badStatusLine, final String expectedExceptionMsg) + throws Exception { + final InetSocketAddress irrelevantTargetServerAddr = + new InetSocketAddress(InetAddress.getLoopbackAddress(), 12345); + final URL url = URIBuilder.newBuilder() + .scheme("https") + .host(irrelevantTargetServerAddr.getAddress()) + .port(irrelevantTargetServerAddr.getPort()) + .path("/doesnotmatter") + .build().toURL(); + + Thread proxyServerThread = null; + try (final BadProxyServer proxy = new BadProxyServer(badStatusLine)) { + + proxyServerThread = Thread.ofPlatform().name("proxy-server").start(proxy); + final HttpURLConnection urlc = (HttpURLConnection) + url.openConnection(new Proxy(HTTP, proxy.getAddress())); + + final IOException ioe = assertThrows(IOException.class, () -> urlc.getInputStream()); + final String exMsg = ioe.getMessage(); + if (exMsg == null || !exMsg.contains(expectedExceptionMsg)) { + // unexpected message in the exception, propagate the exception + throw ioe; + } + System.err.println("got excepted exception: " + ioe); + } finally { + if (proxyServerThread != null) { + System.err.println("waiting for proxy server thread to complete"); + proxyServerThread.join(); + } + } + } + + private static final class BadProxyServer implements Runnable, AutoCloseable { + private static final int CR = '\r'; + private static final int LF = '\n'; + + private final ServerSocket serverSocket; + private final String connectRespStatusLine; + private volatile boolean closed; + + /** + * + * @param connectRespStatusLine the status line that this server writes + * out in response to a CONNECT request + * @throws IOException + */ + BadProxyServer(final String connectRespStatusLine) throws IOException { + this.connectRespStatusLine = connectRespStatusLine; + final int port = 0; + final int backlog = 0; + this.serverSocket = new ServerSocket(port, backlog, InetAddress.getLoopbackAddress()); + } + + InetSocketAddress getAddress() { + return (InetSocketAddress) this.serverSocket.getLocalSocketAddress(); + } + + @Override + public void close() { + if (this.closed) { + return; + } + synchronized (this) { + if (this.closed) { + return; + } + this.closed = true; + } + try { + this.serverSocket.close(); + } catch (IOException e) { + System.err.println("failed to close proxy server: " + e); + e.printStackTrace(); + } + } + + @Override + public void run() { + try { + doRun(); + } catch (Throwable t) { + if (!closed) { + System.err.println("Proxy server ran into exception: " + t); + t.printStackTrace(); + } + } + System.err.println("Proxy server " + this.serverSocket + " exiting"); + } + + private void doRun() throws IOException { + while (!closed) { + System.err.println("waiting for incoming connection at " + this.serverSocket); + try (final Socket accepted = this.serverSocket.accept()) { + System.err.println("accepted incoming connection from " + accepted); + handleIncomingConnection(accepted); + } + } + } + + private static int findCRLF(final byte[] b) { + for (int i = 0; i < b.length - 1; i++) { + if (b[i] == CR && b[i + 1] == LF) { + return i; + } + } + return -1; + } + + // writes out a status line in response to a CONNECT request + private void handleIncomingConnection(final Socket acceptedSocket) throws IOException { + final byte[] req = readRequest(acceptedSocket.getInputStream()); + final int crlfIndex = findCRLF(req); + if (crlfIndex < 0) { + System.err.println("unexpected request content from " + acceptedSocket); + // nothing to process, ignore this connection + return; + } + final String requestLine = new String(req, 0, crlfIndex, ISO_8859_1); + System.err.println("received request line: \"" + requestLine + "\""); + final String[] parts = requestLine.split(" "); + if (parts[0].equals("CONNECT")) { + // reply back with the status line + try (final OutputStream os = acceptedSocket.getOutputStream()) { + System.err.println("responding to CONNECT request from " + acceptedSocket + + ", response status line: \"" + connectRespStatusLine + "\""); + final byte[] statusLine = connectRespStatusLine.getBytes(ISO_8859_1); + os.write(statusLine); + } + } else { + System.err.println("unexpected request from " + acceptedSocket + ": \"" + requestLine + "\""); + return; + } + } + + private static byte[] readRequest(InputStream is) throws IOException { + // we don't expect the HTTP request body in this test to be larger than this size + final byte[] buff = new byte[4096]; + int crlfcount = 0; + int numRead = 0; + int c; + while ((c = is.read()) != -1 && numRead < buff.length) { + buff[numRead++] = (byte) c; + // + // HTTP-message = start-line CRLF + // *( field-line CRLF ) + // CRLF + // [ message-body ] + // + // start-line = request-line / status-line + // + // we are not interested in the message body, so this loop is + // looking for the CRLFCRLF sequence to stop parsing the request + // content + if (c == CR || c == LF) { + switch (crlfcount) { + case 0, 2 -> { + if (c == CR) { + crlfcount++; + } + } + case 1, 3 -> { + if (c == LF) { + crlfcount++; + } + } + } + } else { + crlfcount = 0; + } + if (crlfcount == 4) { + break; + } + } + if (crlfcount != 4) { + throw new IOException("Could not locate a CRLFCRLF sequence in the request"); + } + final byte[] request = new byte[numRead]; + System.arraycopy(buff, 0, request, 0, numRead); + return request; + } + } +}