diff --git a/src/jdk.httpserver/share/classes/sun/net/httpserver/Request.java b/src/jdk.httpserver/share/classes/sun/net/httpserver/Request.java index 4a4afc8d7ce14..75eeb29f015b0 100644 --- a/src/jdk.httpserver/share/classes/sun/net/httpserver/Request.java +++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/Request.java @@ -25,6 +25,7 @@ package sun.net.httpserver; +import java.net.ProtocolException; import java.nio.*; import java.io.*; import java.nio.channels.*; @@ -39,15 +40,18 @@ class Request { static final int BUF_LEN = 2048; static final byte CR = 13; static final byte LF = 10; + static final byte FIRST_CHAR = 32; private String startLine; private SocketChannel chan; private InputStream is; private OutputStream os; private final int maxReqHeaderSize; + private final boolean firstClearRequest; - Request(InputStream rawInputStream, OutputStream rawout) throws IOException { + Request(InputStream rawInputStream, OutputStream rawout, boolean firstClearRequest) throws IOException { this.maxReqHeaderSize = ServerConfig.getMaxReqHeaderSize(); + this.firstClearRequest = firstClearRequest; is = rawInputStream; os = rawout; do { @@ -78,6 +82,25 @@ public String readLine() throws IOException { boolean gotCR = false, gotLF = false; pos = 0; lineBuf = new StringBuffer(); long lsize = 32; + + // For the first request that comes on a clear connection + // we will check that the first non CR/LF char on the + // request line is eligible. This should be the first char + // of a method name, so it should be at least greater or equal + // to 32 (FIRST_CHAR) which is the space character. + // The main goal here is to fail fast if we receive 0x16 (22) which + // happens to be the first byte of a TLS handshake record. + // This is typically what would be received if a TLS client opened + // a TLS connection on a non-TLS server. + // If we receive 0x16 we should close the connection immediately as + // it indicates we're receiving a ClientHello on a clear + // connection, and we will never receive the expected CRLF that + // terminates the first request line. + // Though we could check only for 0x16, any characters < 32 + // (excluding CRLF) is not expected at this position in a + // request line, so we can still fail here early if any of + // those are detected. + int offset = 0; while (!gotLF) { int c = is.read(); if (c == -1) { @@ -89,6 +112,12 @@ public String readLine() throws IOException { } else { gotCR = false; consume(CR); + if (firstClearRequest && offset == 0) { + if (c < FIRST_CHAR) { + throw new ProtocolException("Unexpected start of request line"); + } + offset++; + } consume(c); lsize = lsize + 2; } @@ -96,6 +125,12 @@ public String readLine() throws IOException { if (c == CR) { gotCR = true; } else { + if (firstClearRequest && offset == 0) { + if (c < FIRST_CHAR) { + throw new ProtocolException("Unexpected start of request line"); + } + offset++; + } consume(c); lsize = lsize + 1; } diff --git a/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerImpl.java b/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerImpl.java index 1eb918b4e66f7..e8c8d336e03ed 100644 --- a/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerImpl.java +++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerImpl.java @@ -45,6 +45,7 @@ import java.lang.System.Logger.Level; import java.net.BindException; import java.net.InetSocketAddress; +import java.net.ProtocolException; import java.net.ServerSocket; import java.net.URI; import java.net.URISyntaxException; @@ -733,7 +734,16 @@ public void run() { connection.raw = rawin; connection.rawout = rawout; } - Request req = new Request(rawin, rawout); + + Request req; + try { + req = new Request(rawin, rawout, newconnection && !https); + } catch (ProtocolException pe) { + logger.log(Level.DEBUG, "closing due to: " + pe); + reject(Code.HTTP_BAD_REQUEST, "", pe.getMessage()); + return; + } + requestLine = req.requestLine(); if (requestLine == null) { /* connection closed */ diff --git a/test/jdk/com/sun/net/httpserver/ClearTextServerSSL.java b/test/jdk/com/sun/net/httpserver/ClearTextServerSSL.java new file mode 100644 index 0000000000000..75e4f3fcf4532 --- /dev/null +++ b/test/jdk/com/sun/net/httpserver/ClearTextServerSSL.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2025, 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. + */ + +/* + * @test + * @bug 8373677 + * @summary Tests for verifying that a non-SSL server can detect + * when a client attempts to use SSL. + * @library /test/lib + * @run junit/othervm ${test.main.class} + */ + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import jdk.test.lib.net.SimpleSSLContext; +import jdk.test.lib.net.URIBuilder; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.logging.ConsoleHandler; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.net.ssl.SSLException; + +import static com.sun.net.httpserver.HttpExchange.RSPBODY_EMPTY; +import static java.net.http.HttpClient.Builder.NO_PROXY; +import static org.junit.jupiter.api.Assertions.*; + +public class ClearTextServerSSL { + + static final InetAddress LOOPBACK_ADDR = InetAddress.getLoopbackAddress(); + static final boolean ENABLE_LOGGING = true; + static final Logger logger = Logger.getLogger("com.sun.net.httpserver"); + + static final String CTXT_PATH = "/ClearTextServerSSL"; + + @BeforeAll + public static void setup() { + if (ENABLE_LOGGING) { + ConsoleHandler ch = new ConsoleHandler(); + logger.setLevel(Level.ALL); + ch.setLevel(Level.ALL); + logger.addHandler(ch); + } + } + + @Test + public void test() throws Exception { + var sslContext = new SimpleSSLContext().get(); + var handler = new TestHandler(); + var server = HttpServer.create(new InetSocketAddress(LOOPBACK_ADDR, 0), 0); + server.createContext(path(""), handler); + server.start(); + try (var client = HttpClient.newBuilder() + .sslContext(sslContext) + .proxy(NO_PROXY) + .build()) { + var request = HttpRequest.newBuilder() + .uri(uri("http", server, path("/clear"))) + .build(); + var response = client.send(request, HttpResponse.BodyHandlers.ofString()); + assertEquals(200, response.statusCode()); + var sslRequest = HttpRequest.newBuilder() + .uri(uri("https", server, path("/ssl"))) + .build(); + Assertions.assertThrows(SSLException.class, () -> { + client.send(sslRequest, HttpResponse.BodyHandlers.ofString()); + }); + try (var socket = new Socket()) { + socket.connect(server.getAddress()); + byte[] badRequest = { + 22, 'B', 'A', 'D', ' ', + '/', ' ' , + 'H', 'T', 'T', 'P', '/', '1', '.', '1' }; + socket.getOutputStream().write(badRequest); + socket.getOutputStream().flush(); + var reader = new InputStreamReader(socket.getInputStream()); + var line = reader.readAllLines(); + Assertions.assertEquals("HTTP/1.1 400 Bad Request", line.get(0)); + System.out.println("Got expected response:"); + line.stream().map(l -> "\t" + l).forEach(System.out::println); + } + + } finally { + server.stop(0); + } + } + + // --- infra --- + + static String path(String path) { + assert CTXT_PATH.startsWith("/"); + assert !CTXT_PATH.endsWith("/"); + if (path.startsWith("/")) { + return CTXT_PATH + path; + } else { + return CTXT_PATH + "/" + path; + } + } + + static URI uri(String scheme, HttpServer server, String path) throws URISyntaxException { + return URIBuilder.newBuilder() + .scheme(scheme) + .loopback() + .port(server.getAddress().getPort()) + .path(path) + .build(); + } + + /** + * A test handler that reads any request bytes and sends + * an empty 200 response + */ + static class TestHandler implements HttpHandler { + @java.lang.Override + public void handle(HttpExchange exchange) throws IOException { + try (var reqBody = exchange.getRequestBody()) { + reqBody.readAllBytes(); + exchange.sendResponseHeaders(200, RSPBODY_EMPTY); + } catch (Throwable t) { + t.printStackTrace(); + exchange.sendResponseHeaders(500, RSPBODY_EMPTY); + } + } + } +}