From 7cfc4d1e8943c55e95784bbfc9189364dfd07fd7 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 09:40:00 +0100 Subject: [PATCH] Enhance JDK HTTP server to support HTTP upgrade --- .../com/sun/net/httpserver/HttpExchange.java | 48 +++++ .../net/httpserver/HttpUpgradeHandler.java | 71 +++++++ .../httpserver/DelegatingHttpExchange.java | 4 + .../sun/net/httpserver/ExchangeImpl.java | 37 ++++ .../sun/net/httpserver/HttpExchangeImpl.java | 4 + .../sun/net/httpserver/HttpsExchangeImpl.java | 4 + .../sun/net/httpserver/ServerImpl.java | 17 ++ .../sun/net/httpserver/HttpUpgradeTest.java | 188 ++++++++++++++++++ 8 files changed, 373 insertions(+) create mode 100644 src/jdk.httpserver/share/classes/com/sun/net/httpserver/HttpUpgradeHandler.java create mode 100644 test/jdk/com/sun/net/httpserver/HttpUpgradeTest.java diff --git a/src/jdk.httpserver/share/classes/com/sun/net/httpserver/HttpExchange.java b/src/jdk.httpserver/share/classes/com/sun/net/httpserver/HttpExchange.java index a56c20b53afac..51df3be0a86ee 100644 --- a/src/jdk.httpserver/share/classes/com/sun/net/httpserver/HttpExchange.java +++ b/src/jdk.httpserver/share/classes/com/sun/net/httpserver/HttpExchange.java @@ -319,4 +319,52 @@ protected HttpExchange() { * @return the {@code HttpPrincipal}, or {@code null} if no authenticator is set */ public abstract HttpPrincipal getPrincipal(); + + /** + * Upgrades the HTTP connection to use a different protocol. + * + *
This method enables support for HTTP protocol upgrade, such as + * upgrading to WebSocket. When called, the server will: + *
This method must be called before {@link #sendResponseHeaders(int, long)}. + * Appropriate upgrade response headers (such as {@code Upgrade} and + * {@code Connection: upgrade}) must be set before calling this method. + * + *
After this method is called, the exchange is considered complete and + * no further HTTP operations should be performed on it. The upgrade handler + * takes full control of the connection. + * + *
Example usage for WebSocket upgrade: + *
{@code
+ * public void handle(HttpExchange exchange) throws IOException {
+ * Headers requestHeaders = exchange.getRequestHeaders();
+ * if ("websocket".equalsIgnoreCase(requestHeaders.getFirst("Upgrade"))) {
+ * Headers responseHeaders = exchange.getResponseHeaders();
+ * responseHeaders.set("Upgrade", "websocket");
+ * responseHeaders.set("Connection", "Upgrade");
+ * // Set other required headers like Sec-WebSocket-Accept
+ *
+ * exchange.upgrade((input, output) -> {
+ * // Handle WebSocket protocol
+ * // Read and write WebSocket frames
+ * });
+ * }
+ * }
+ * }
+ *
+ * @param handler the handler for the upgraded protocol
+ * @throws IOException if an I/O error occurs
+ * @throws IllegalStateException if response headers have already been sent
+ * or if the connection cannot be upgraded
+ * @throws NullPointerException if handler is {@code null}
+ * @since 26
+ */
+ public abstract void upgrade(HttpUpgradeHandler handler) throws IOException;
}
diff --git a/src/jdk.httpserver/share/classes/com/sun/net/httpserver/HttpUpgradeHandler.java b/src/jdk.httpserver/share/classes/com/sun/net/httpserver/HttpUpgradeHandler.java
new file mode 100644
index 0000000000000..e8c7a2cea5b26
--- /dev/null
+++ b/src/jdk.httpserver/share/classes/com/sun/net/httpserver/HttpUpgradeHandler.java
@@ -0,0 +1,71 @@
+/*
+ * 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. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * 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.
+ */
+
+package com.sun.net.httpserver;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * A handler which is invoked to process HTTP protocol upgrade requests.
+ *
+ * When an HTTP upgrade is initiated via {@link HttpExchange#upgrade(int, HttpUpgradeHandler)}, + * the server will invoke the {@link #handle(InputStream, OutputStream)} method + * of this handler, providing direct access to the underlying socket streams. + * + *
The handler is responsible for: + *
Once the upgrade is complete, the HTTP server will no longer manage + * the connection, and the handler has full control over the socket. + * + * @since 26 + */ +@FunctionalInterface +public interface HttpUpgradeHandler { + + /** + * Handles the upgraded protocol on the given streams. + * + *
This method is called after a successful HTTP upgrade response has + * been sent to the client. The provided streams give direct access to + * the underlying socket, allowing the implementation to communicate + * using the upgraded protocol. + * + *
The implementation is responsible for closing both streams when + * the upgraded protocol session is complete. The streams may be the + * same object if using a socket channel. + * + * @param input the input stream from the client + * @param output the output stream to the client + * @throws IOException if an I/O error occurs during protocol handling + */ + void handle(InputStream input, OutputStream output) throws IOException; +} diff --git a/src/jdk.httpserver/share/classes/sun/net/httpserver/DelegatingHttpExchange.java b/src/jdk.httpserver/share/classes/sun/net/httpserver/DelegatingHttpExchange.java index 88399471a7f94..64b59e024511a 100644 --- a/src/jdk.httpserver/share/classes/sun/net/httpserver/DelegatingHttpExchange.java +++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/DelegatingHttpExchange.java @@ -105,4 +105,8 @@ public void setStreams(InputStream i, OutputStream o) { public HttpPrincipal getPrincipal() { return exchange.getPrincipal(); } + + public void upgrade(com.sun.net.httpserver.HttpUpgradeHandler handler) throws IOException { + exchange.upgrade(handler); + } } diff --git a/src/jdk.httpserver/share/classes/sun/net/httpserver/ExchangeImpl.java b/src/jdk.httpserver/share/classes/sun/net/httpserver/ExchangeImpl.java index 8da98cdcfa5e7..849e0061a0e2d 100644 --- a/src/jdk.httpserver/share/classes/sun/net/httpserver/ExchangeImpl.java +++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/ExchangeImpl.java @@ -86,6 +86,10 @@ class ExchangeImpl { int rcode = -1; HttpPrincipal principal; ServerImpl server; + + /* upgrade state */ + boolean upgraded = false; + HttpUpgradeHandler upgradeHandler = null; ExchangeImpl( String m, URI u, Request req, long len, HttpConnection connection @@ -411,6 +415,39 @@ void setPrincipal(HttpPrincipal principal) { this.principal = principal; } + void upgrade(HttpUpgradeHandler handler) throws IOException { + if (handler == null) { + throw new NullPointerException("handler cannot be null"); + } + if (sentHeaders) { + throw new IllegalStateException("headers already sent"); + } + this.upgraded = true; + this.upgradeHandler = handler; + this.rcode = 101; + + // Send upgrade response with 101 Switching Protocols + String statusLine = "HTTP/1.1 101" + Code.msg(101) + "\r\n"; + ByteArrayOutputStream tmpout = new ByteArrayOutputStream(); + tmpout.write(bytes(statusLine, 0), 0, statusLine.length()); + rspHdrs.set("Date", FORMATTER.format(Instant.now())); + + write(rspHdrs, tmpout); + tmpout.writeTo(ros); + ros.flush(); + sentHeaders = true; + + server.logReply(101, req.requestLine(), null); + } + + boolean isUpgraded() { + return upgraded; + } + + HttpUpgradeHandler getUpgradeHandler() { + return upgradeHandler; + } + static ExchangeImpl get(HttpExchange t) { if (t instanceof HttpExchangeImpl) { return ((HttpExchangeImpl)t).getExchangeImpl(); diff --git a/src/jdk.httpserver/share/classes/sun/net/httpserver/HttpExchangeImpl.java b/src/jdk.httpserver/share/classes/sun/net/httpserver/HttpExchangeImpl.java index d4606019c5660..0c7091f539309 100644 --- a/src/jdk.httpserver/share/classes/sun/net/httpserver/HttpExchangeImpl.java +++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/HttpExchangeImpl.java @@ -113,6 +113,10 @@ public HttpPrincipal getPrincipal() { return impl.getPrincipal(); } + public void upgrade(HttpUpgradeHandler handler) throws IOException { + impl.upgrade(handler); + } + ExchangeImpl getExchangeImpl() { return impl; } diff --git a/src/jdk.httpserver/share/classes/sun/net/httpserver/HttpsExchangeImpl.java b/src/jdk.httpserver/share/classes/sun/net/httpserver/HttpsExchangeImpl.java index 939b88d392829..d530b1d9eac9c 100644 --- a/src/jdk.httpserver/share/classes/sun/net/httpserver/HttpsExchangeImpl.java +++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/HttpsExchangeImpl.java @@ -117,6 +117,10 @@ public HttpPrincipal getPrincipal() { return impl.getPrincipal(); } + public void upgrade(HttpUpgradeHandler handler) throws IOException { + impl.upgrade(handler); + } + ExchangeImpl getExchangeImpl() { return impl; } 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 e8c8d336e03ed..642506e94ded4 100644 --- a/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerImpl.java +++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerImpl.java @@ -31,6 +31,7 @@ import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.HttpUpgradeHandler; import com.sun.net.httpserver.HttpsConfigurator; import sun.net.httpserver.HttpConnection.State; @@ -901,6 +902,22 @@ public void run() { uc.doFilter(new HttpExchangeImpl(tx)); } + // Handle protocol upgrade if requested + if (tx.isUpgraded()) { + HttpUpgradeHandler upgradeHandler = tx.getUpgradeHandler(); + if (upgradeHandler != null) { + try { + // Transfer control to the upgrade handler with raw streams + upgradeHandler.handle(rawin, rawout); + } catch (IOException e) { + logger.log(Level.DEBUG, "Upgrade handler exception", e); + } finally { + // After upgrade handler completes, close the connection + closeConnection(connection); + } + } + } + } catch (Exception e) { logger.log(Level.TRACE, "ServerImpl.Exchange", e); if (tx == null || !tx.writefinished) { diff --git a/test/jdk/com/sun/net/httpserver/HttpUpgradeTest.java b/test/jdk/com/sun/net/httpserver/HttpUpgradeTest.java new file mode 100644 index 0000000000000..5da3d8fb1d666 --- /dev/null +++ b/test/jdk/com/sun/net/httpserver/HttpUpgradeTest.java @@ -0,0 +1,188 @@ +/* + * 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 8373877 + * @summary Test HTTP upgrade functionality + * @run main HttpUpgradeTest + */ + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.HttpUpgradeHandler; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class HttpUpgradeTest { + + private static final CountDownLatch upgradeLatch = new CountDownLatch(1); + private static final CountDownLatch messageLatch = new CountDownLatch(1); + private static volatile String receivedMessage = null; + + public static void main(String[] args) throws Exception { + HttpServer server = null; + try { + // Create and start server + InetSocketAddress addr = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0); + server = HttpServer.create(addr, 0); + + server.createContext("/upgrade", new UpgradeHandler()); + server.setExecutor(null); + server.start(); + + int port = server.getAddress().getPort(); + System.out.println("Server started on port: " + port); + + // Test upgrade + testUpgrade(port); + + System.out.println("Test PASSED"); + } finally { + if (server != null) { + server.stop(0); + } + } + } + + static class UpgradeHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + var headers = exchange.getRequestHeaders(); + String upgrade = headers.getFirst("Upgrade"); + + if ("echo-protocol".equalsIgnoreCase(upgrade)) { + // Set upgrade response headers + var responseHeaders = exchange.getResponseHeaders(); + responseHeaders.set("Upgrade", "echo-protocol"); + responseHeaders.set("Connection", "Upgrade"); + + // Perform upgrade + exchange.upgrade(new EchoUpgradeHandler()); + } else { + // Not an upgrade request + exchange.sendResponseHeaders(400, -1); + } + } + } + + static class EchoUpgradeHandler implements HttpUpgradeHandler { + @Override + public void handle(InputStream input, OutputStream output) throws IOException { + upgradeLatch.countDown(); + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8)); + PrintWriter writer = new PrintWriter(output, true)) { + + String line; + while ((line = reader.readLine()) != null) { + System.out.println("Received in upgrade handler: " + line); + receivedMessage = line; + messageLatch.countDown(); + + // Echo back + writer.println("ECHO: " + line); + break; // Exit after one message for test simplicity + } + } + } + } + + private static void testUpgrade(int port) throws Exception { + try (Socket socket = new Socket(InetAddress.getLoopbackAddress(), port)) { + OutputStream out = socket.getOutputStream(); + InputStream in = socket.getInputStream(); + + // Send upgrade request + String request = "GET /upgrade HTTP/1.1\r\n" + + "Host: localhost:" + port + "\r\n" + + "Upgrade: echo-protocol\r\n" + + "Connection: Upgrade\r\n" + + "\r\n"; + + out.write(request.getBytes(StandardCharsets.UTF_8)); + out.flush(); + + // Read response + BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)); + String statusLine = reader.readLine(); + System.out.println("Status: " + statusLine); + + if (!statusLine.contains("101")) { + throw new RuntimeException("Expected 101 Switching Protocols, got: " + statusLine); + } + + // Read headers + String line; + boolean upgradeHeaderFound = false; + while ((line = reader.readLine()) != null && !line.isEmpty()) { + System.out.println("Header: " + line); + if (line.toLowerCase().contains("upgrade: echo-protocol")) { + upgradeHeaderFound = true; + } + } + + if (!upgradeHeaderFound) { + throw new RuntimeException("Upgrade header not found in response"); + } + + // Wait for upgrade handler to be invoked + if (!upgradeLatch.await(5, TimeUnit.SECONDS)) { + throw new RuntimeException("Upgrade handler not invoked"); + } + + // Now the protocol is upgraded, send a message + PrintWriter writer = new PrintWriter(out, true); + writer.println("Hello from client"); + + // Wait for message to be received + if (!messageLatch.await(5, TimeUnit.SECONDS)) { + throw new RuntimeException("Message not received by upgrade handler"); + } + + if (!"Hello from client".equals(receivedMessage)) { + throw new RuntimeException("Unexpected message received: " + receivedMessage); + } + + // Read echo response + String echo = reader.readLine(); + System.out.println("Echo response: " + echo); + + if (!echo.equals("ECHO: Hello from client")) { + throw new RuntimeException("Unexpected echo: " + echo); + } + } + } +}