undefect. CWE-407 — 63 sites patched across 27 ecosystems

Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com

Patches, unit tests, benchmarks, whitepaper, and outreach briefs.
Public domain — no copyright claimed. Use freely.
This commit is contained in:
russell@unturf.com 2026-03-26 17:11:57 -04:00
commit 0a580b313d
70422 changed files with 17213626 additions and 0 deletions

View file

@ -0,0 +1,448 @@
/*
* Copyright (c) 2018, 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.
*/
/*
* @test
* @build DummyWebSocketServer
* @run junit/othervm
* -Djdk.internal.httpclient.websocket.debug=true
* Abort
*/
import java.io.IOException;
import java.net.ProtocolException;
import java.net.http.WebSocket;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static java.net.http.HttpClient.newHttpClient;
import static java.net.http.WebSocket.NORMAL_CLOSURE;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import org.junit.jupiter.api.Test;
public class Abort {
private static final Class<NullPointerException> NPE = NullPointerException.class;
private static final Class<IllegalArgumentException> IAE = IllegalArgumentException.class;
private static final Class<IOException> IOE = IOException.class;
@Test
public void onOpenThenAbort() throws Exception {
int[] bytes = new int[]{
0x88, 0x00, // opcode=close
};
try (var server = Support.serverWithCannedData(bytes)) {
server.open();
// messages are available
MockListener listener = new MockListener() {
@Override
protected void onOpen0(WebSocket webSocket) {
// unbounded request
webSocket.request(Long.MAX_VALUE);
webSocket.abort();
}
};
var webSocket = newHttpClient().newWebSocketBuilder()
.buildAsync(server.getURI(), listener)
.join();
try {
TimeUnit.SECONDS.sleep(5);
List<MockListener.Invocation> inv = listener.invocationsSoFar();
// no more invocations after onOpen as WebSocket was aborted
assertEquals(List.of(MockListener.Invocation.onOpen(webSocket)), inv);
} finally {
webSocket.abort();
}
}
}
@Test
public void onOpenThenOnTextThenAbort() throws Exception {
int[] bytes = new int[]{
0x81, 0x00, // opcode=text, fin=true
0x88, 0x00, // opcode=close
};
try (var server = Support.serverWithCannedData(bytes)) {
server.open();
MockListener listener = new MockListener() {
@Override
protected void onOpen0(WebSocket webSocket) {
// unbounded request
webSocket.request(Long.MAX_VALUE);
}
@Override
protected CompletionStage<?> onText0(WebSocket webSocket,
CharSequence message,
boolean last) {
webSocket.abort();
return super.onText0(webSocket, message, last);
}
};
var webSocket = newHttpClient().newWebSocketBuilder()
.buildAsync(server.getURI(), listener)
.join();
try {
TimeUnit.SECONDS.sleep(5);
List<MockListener.Invocation> inv = listener.invocationsSoFar();
// no more invocations after onOpen, onBinary as WebSocket was aborted
List<MockListener.Invocation> expected = List.of(
MockListener.Invocation.onOpen(webSocket),
MockListener.Invocation.onText(webSocket, "", true));
assertEquals(expected, inv);
} finally {
webSocket.abort();
}
}
}
@Test
public void onOpenThenOnBinaryThenAbort() throws Exception {
int[] bytes = new int[]{
0x82, 0x00, // opcode=binary, fin=true
0x88, 0x00, // opcode=close
};
try (var server = Support.serverWithCannedData(bytes)) {
server.open();
MockListener listener = new MockListener() {
@Override
protected void onOpen0(WebSocket webSocket) {
// unbounded request
webSocket.request(Long.MAX_VALUE);
}
@Override
protected CompletionStage<?> onBinary0(WebSocket webSocket,
ByteBuffer message,
boolean last) {
webSocket.abort();
return super.onBinary0(webSocket, message, last);
}
};
var webSocket = newHttpClient().newWebSocketBuilder()
.buildAsync(server.getURI(), listener)
.join();
try {
TimeUnit.SECONDS.sleep(5);
List<MockListener.Invocation> inv = listener.invocationsSoFar();
// no more invocations after onOpen, onBinary as WebSocket was aborted
List<MockListener.Invocation> expected = List.of(
MockListener.Invocation.onOpen(webSocket),
MockListener.Invocation.onBinary(webSocket, ByteBuffer.allocate(0), true));
assertEquals(expected, inv);
} finally {
webSocket.abort();
}
}
}
@Test
public void onOpenThenOnPingThenAbort() throws Exception {
int[] bytes = {
0x89, 0x00, // opcode=ping
0x88, 0x00, // opcode=close
};
try (var server = Support.serverWithCannedData(bytes)) {
server.open();
MockListener listener = new MockListener() {
@Override
protected void onOpen0(WebSocket webSocket) {
// unbounded request
webSocket.request(Long.MAX_VALUE);
}
@Override
protected CompletionStage<?> onPing0(WebSocket webSocket,
ByteBuffer message) {
webSocket.abort();
return super.onPing0(webSocket, message);
}
};
var webSocket = newHttpClient().newWebSocketBuilder()
.buildAsync(server.getURI(), listener)
.join();
try {
TimeUnit.SECONDS.sleep(5);
List<MockListener.Invocation> inv = listener.invocationsSoFar();
// no more invocations after onOpen, onPing as WebSocket was aborted
List<MockListener.Invocation> expected = List.of(
MockListener.Invocation.onOpen(webSocket),
MockListener.Invocation.onPing(webSocket, ByteBuffer.allocate(0)));
assertEquals(expected, inv);
} finally {
webSocket.abort();
}
}
}
@Test
public void onOpenThenOnPongThenAbort() throws Exception {
int[] bytes = {
0x8a, 0x00, // opcode=pong
0x88, 0x00, // opcode=close
};
try (var server = Support.serverWithCannedData(bytes)) {
server.open();
MockListener listener = new MockListener() {
@Override
protected void onOpen0(WebSocket webSocket) {
// unbounded request
webSocket.request(Long.MAX_VALUE);
}
@Override
protected CompletionStage<?> onPong0(WebSocket webSocket,
ByteBuffer message) {
webSocket.abort();
return super.onPong0(webSocket, message);
}
};
var webSocket = newHttpClient().newWebSocketBuilder()
.buildAsync(server.getURI(), listener)
.join();
try {
TimeUnit.SECONDS.sleep(5);
List<MockListener.Invocation> inv = listener.invocationsSoFar();
// no more invocations after onOpen, onPong as WebSocket was aborted
List<MockListener.Invocation> expected = List.of(
MockListener.Invocation.onOpen(webSocket),
MockListener.Invocation.onPong(webSocket, ByteBuffer.allocate(0)));
assertEquals(expected, inv);
} finally {
webSocket.abort();
}
}
}
@Test
public void onOpenThenOnCloseThenAbort() throws Exception {
int[] bytes = {
0x88, 0x00, // opcode=close
0x8a, 0x00, // opcode=pong
};
try (var server = Support.serverWithCannedData(bytes)) {
server.open();
MockListener listener = new MockListener() {
@Override
protected void onOpen0(WebSocket webSocket) {
// unbounded request
webSocket.request(Long.MAX_VALUE);
}
@Override
protected CompletionStage<?> onClose0(WebSocket webSocket,
int statusCode,
String reason) {
webSocket.abort();
return super.onClose0(webSocket, statusCode, reason);
}
};
var webSocket = newHttpClient().newWebSocketBuilder()
.buildAsync(server.getURI(), listener)
.join();
try {
TimeUnit.SECONDS.sleep(5);
List<MockListener.Invocation> inv = listener.invocationsSoFar();
// no more invocations after onOpen, onClose
List<MockListener.Invocation> expected = List.of(
MockListener.Invocation.onOpen(webSocket),
MockListener.Invocation.onClose(webSocket, 1005, ""));
assertEquals(expected, inv);
} finally {
webSocket.abort();
}
}
}
@Test
public void onOpenThenOnErrorThenAbort() throws Exception {
// A header of 128 bytes long Ping (which is a protocol error)
int[] badPingHeader = new int[]{0x89, 0x7e, 0x00, 0x80};
int[] closeMessage = new int[]{0x88, 0x00};
int[] bytes = new int[badPingHeader.length + 128 + closeMessage.length];
System.arraycopy(badPingHeader, 0, bytes, 0, badPingHeader.length);
System.arraycopy(closeMessage, 0, bytes, badPingHeader.length + 128, closeMessage.length);
try (var server = Support.serverWithCannedData(bytes)) {
server.open();
MockListener listener = new MockListener() {
@Override
protected void onOpen0(WebSocket webSocket) {
// unbounded request
webSocket.request(Long.MAX_VALUE);
}
@Override
protected void onError0(WebSocket webSocket, Throwable error) {
webSocket.abort();
super.onError0(webSocket, error);
}
};
var webSocket = newHttpClient().newWebSocketBuilder()
.buildAsync(server.getURI(), listener)
.join();
try {
TimeUnit.SECONDS.sleep(5);
List<MockListener.Invocation> inv = listener.invocationsSoFar();
// no more invocations after onOpen, onError
List<MockListener.Invocation> expected = List.of(
MockListener.Invocation.onOpen(webSocket),
MockListener.Invocation.onError(webSocket, ProtocolException.class));
System.out.println("actual invocations:" + Arrays.toString(inv.toArray()));
assertEquals(expected, inv);
} finally {
webSocket.abort();
}
}
}
@Test
public void immediateAbort() throws Exception {
CompletableFuture<Void> messageReceived = new CompletableFuture<>();
WebSocket.Listener listener = new WebSocket.Listener() {
@Override
public void onOpen(WebSocket webSocket) {
/* no initial request */
}
@Override
public CompletionStage<?> onText(WebSocket webSocket,
CharSequence message,
boolean last) {
messageReceived.complete(null);
return null;
}
@Override
public CompletionStage<?> onBinary(WebSocket webSocket,
ByteBuffer message,
boolean last) {
messageReceived.complete(null);
return null;
}
@Override
public CompletionStage<?> onPing(WebSocket webSocket,
ByteBuffer message) {
messageReceived.complete(null);
return null;
}
@Override
public CompletionStage<?> onPong(WebSocket webSocket,
ByteBuffer message) {
messageReceived.complete(null);
return null;
}
@Override
public CompletionStage<?> onClose(WebSocket webSocket,
int statusCode,
String reason) {
messageReceived.complete(null);
return null;
}
};
int[] bytes = new int[]{
0x82, 0x00, // opcode=binary, fin=true
0x88, 0x00, // opcode=close
};
try (var server = Support.serverWithCannedData(bytes)) {
server.open();
WebSocket ws = newHttpClient()
.newWebSocketBuilder()
.buildAsync(server.getURI(), listener)
.join();
try {
for (int i = 0; i < 3; i++) {
System.out.printf("iteration #%s%n", i);
// after the first abort() each consecutive one must be a no-op,
// moreover, query methods should continue to return consistent
// values
for (int j = 0; j < 3; j++) {
System.out.printf("abort #%s%n", j);
ws.abort();
assertTrue(ws.isInputClosed());
assertTrue(ws.isOutputClosed());
assertEquals("", ws.getSubprotocol());
}
// at this point valid requests MUST be a no-op:
for (int j = 0; j < 3; j++) {
System.out.printf("request #%s%n", j);
ws.request(1);
ws.request(2);
ws.request(8);
ws.request(Integer.MAX_VALUE);
ws.request(Long.MAX_VALUE);
// invalid requests MUST throw IAE:
assertThrows(IAE, () -> ws.request(Integer.MIN_VALUE));
assertThrows(IAE, () -> ws.request(Long.MIN_VALUE));
assertThrows(IAE, () -> ws.request(-1));
assertThrows(IAE, () -> ws.request(0));
}
}
// even though there is a bunch of messages readily available on the
// wire we shouldn't have received any of them as we aborted before
// the first request
try {
messageReceived.get(5, TimeUnit.SECONDS);
fail();
} catch (TimeoutException expected) {
System.out.println("Finished waiting");
}
for (int i = 0; i < 3; i++) {
System.out.printf("send #%s%n", i);
Support.assertFails(IOE, ws.sendText("text!", false));
Support.assertFails(IOE, ws.sendText("text!", true));
Support.assertFails(IOE, ws.sendBinary(ByteBuffer.allocate(16), false));
Support.assertFails(IOE, ws.sendBinary(ByteBuffer.allocate(16), true));
Support.assertFails(IOE, ws.sendPing(ByteBuffer.allocate(16)));
Support.assertFails(IOE, ws.sendPong(ByteBuffer.allocate(16)));
Support.assertFails(IOE, ws.sendClose(NORMAL_CLOSURE, "a reason"));
assertThrows(NPE, () -> ws.sendText(null, false));
assertThrows(NPE, () -> ws.sendText(null, true));
assertThrows(NPE, () -> ws.sendBinary(null, false));
assertThrows(NPE, () -> ws.sendBinary(null, true));
assertThrows(NPE, () -> ws.sendPing(null));
assertThrows(NPE, () -> ws.sendPong(null));
assertThrows(NPE, () -> ws.sendClose(NORMAL_CLOSURE, null));
}
} finally {
ws.abort();
}
}
}
}

View file

@ -0,0 +1,231 @@
/*
* Copyright (c) 2018, 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.
*/
/*
* @test
* @build DummyWebSocketServer
* @run junit/othervm
* -Djdk.internal.httpclient.websocket.debug=true
* AutomaticPong
*/
import jdk.internal.net.http.websocket.Frame;
import java.io.IOException;
import java.net.http.WebSocket;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.List;
import static java.net.http.HttpClient.newHttpClient;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
public class AutomaticPong {
/*
* The sendClose method has been invoked and a Ping comes from the server.
* Naturally, the client cannot reply with a Pong (the output has been
* closed). However, this MUST not be treated as an error.
* At this stage the server either has received or pretty soon will receive
* the Close message sent by the sendClose. Thus, the server will know the
* client cannot send further messages and it's up to the server to decide
* how to react on the corresponding Pong not being received.
*/
@Test
public void sendCloseThenAutomaticPong() throws IOException {
int[] bytes = {
0x89, 0x00, // ping
0x89, 0x06, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x3f, // ping hello?
0x88, 0x00, // close
};
try (var server = Support.serverWithCannedData(bytes)) {
server.open();
MockListener listener = new MockListener() {
@Override
protected void onOpen0(WebSocket webSocket) {
/* request nothing */
}
};
var webSocket = newHttpClient()
.newWebSocketBuilder()
.buildAsync(server.getURI(), listener)
.join();
try {
webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "ok").join();
// now request all messages available
webSocket.request(Long.MAX_VALUE);
List<MockListener.Invocation> actual = listener.invocations();
ByteBuffer hello = ByteBuffer.wrap("hello?".getBytes(StandardCharsets.UTF_8));
ByteBuffer empty = ByteBuffer.allocate(0);
List<MockListener.Invocation> expected = List.of(
MockListener.Invocation.onOpen(webSocket),
MockListener.Invocation.onPing(webSocket, empty),
MockListener.Invocation.onPing(webSocket, hello),
MockListener.Invocation.onClose(webSocket, 1005, "")
);
assertEquals(expected, actual);
} finally {
webSocket.abort();
}
}
}
/*
* The server sends a number of contiguous Ping messages. The client replies
* to these messages automatically. According to RFC 6455 a WebSocket client
* is free to reply only to the most recent Pings.
*
* What is checked here is that:
*
* a) the order of Pong replies corresponds to the Pings received,
* b) the last Pong corresponds to the last Ping
* c) there are no unrelated Pongs
*/
@ParameterizedTest
@MethodSource("nPings")
public void automaticPongs(int nPings) throws Exception {
// big enough to not bother with resize
ByteBuffer buffer = ByteBuffer.allocate(65536);
Frame.HeaderWriter w = new Frame.HeaderWriter();
for (int i = 0; i < nPings; i++) {
w.fin(true)
.opcode(Frame.Opcode.PING)
.noMask()
.payloadLen(4) // the length of the number of the Ping (int)
.write(buffer);
buffer.putInt(i); // the number of the Ping (int)
}
w.fin(true)
.opcode(Frame.Opcode.CLOSE)
.noMask()
.payloadLen(2)
.write(buffer);
buffer.putChar((char) 1000);
buffer.flip();
try (var server = Support.serverWithCannedData(buffer.array())) {
server.open();
MockListener listener = new MockListener();
var webSocket = newHttpClient()
.newWebSocketBuilder()
.buildAsync(server.getURI(), listener)
.join();
try {
List<MockListener.Invocation> inv = listener.invocations();
assertEquals(nPings + 2, inv.size()); // n * onPing + onOpen + onClose
ByteBuffer data = server.read();
Frame.Reader reader = new Frame.Reader();
Frame.Consumer consumer = new Frame.Consumer() {
ByteBuffer number = ByteBuffer.allocate(4);
Frame.Masker masker = new Frame.Masker();
int i = -1;
boolean closed;
@Override
public void fin(boolean value) {
assertTrue(value);
}
@Override
public void rsv1(boolean value) {
assertFalse(value);
}
@Override
public void rsv2(boolean value) {
assertFalse(value);
}
@Override
public void rsv3(boolean value) {
assertFalse(value);
}
@Override
public void opcode(Frame.Opcode value) {
if (value == Frame.Opcode.CLOSE) {
closed = true;
return;
}
assertEquals(Frame.Opcode.PONG, value);
}
@Override
public void mask(boolean value) {
assertTrue(value);
}
@Override
public void payloadLen(long value) {
if (!closed)
assertEquals(4, value);
}
@Override
public void maskingKey(int value) {
masker.setMask(value);
}
@Override
public void payloadData(ByteBuffer src) {
masker.applyMask(src, number);
if (closed) {
return;
}
number.flip();
int n = number.getInt();
System.out.printf("pong number=%s%n", n);
number.clear();
// a Pong with the number less than the maximum of Pongs already
// received MUST never be received
if (i >= n) {
fail(String.format("i=%s, n=%s", i, n));
}
i = n;
}
@Override
public void endFrame() {
}
};
while (data.hasRemaining()) {
reader.readFrame(data, consumer);
}
} finally {
webSocket.abort();
}
}
}
public static Object[][] nPings() {
return new Object[][]{{1}, {2}, {4}, {8}, {9}, {256}};
}
}

View file

@ -0,0 +1,136 @@
/*
* Copyright (c) 2018, 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.
*/
/*
* @test
* @build DummyWebSocketServer
* @run junit/othervm
* -Djdk.internal.httpclient.debug=true
* -Djdk.internal.httpclient.websocket.debug=true
* BlowupOutputQueue
*/
import java.io.IOException;
import java.net.http.WebSocket;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
public class BlowupOutputQueue extends PendingOperations {
CompletableFuture<WebSocket> cfText;
CompletableFuture<WebSocket> cfPing;
CompletableFuture<WebSocket> cfClose;
MockListener listener;
/*
* The idea is to arrange things such that the internal queue will be fully
* utilized and then make sure there won't be any errors caused by that.
*
* First, fill the queue with Text messages. Once done, send a Ping message.
* At this stage, there are at least 2 messages in queue. Now, start
* receiving. Received Ping messages will cause automatic Pong replies. When
* all done, there will be at least 3 messages in the queue. (As at least
* a single Ping has to be replied). Then send a Close message. Now
* there are at least 4 messages in the queue. Finally, receive the last
* message which is a Close message. This will cause an automatic reply with
* a Close message from the client side. All in all there should be at least
* 5 messages in the queue.
*/
@Test
public void full() throws Exception {
int N = 32;
int[] incoming = new int[2 * (N + 1)]; // 2 bytes per message
for (int i = 0; i < incoming.length - 2; i += 2) {
// <PING>
incoming[i + 0] = 0x89;
incoming[i + 1] = 0x00;
}
// <CLOSE>
incoming[incoming.length - 2] = 0x88;
incoming[incoming.length - 1] = 0x00;
repeatable(() -> {
CountDownLatch allButCloseReceived = new CountDownLatch(N);
server = Support.writingServer(incoming);
server.open();
listener = new MockListener() {
@Override
protected void onOpen0(WebSocket webSocket) {
/* do nothing */
}
@Override
protected void replenish(WebSocket webSocket) {
/* do nothing */
}
@Override
protected CompletionStage<?> onPing0(WebSocket webSocket,
ByteBuffer message) {
allButCloseReceived.countDown();
return null;
}
};
webSocket = httpClient().newWebSocketBuilder()
.buildAsync(server.getURI(), listener)
.join();
CharBuffer data = CharBuffer.allocate(65536);
for (int i = 0; ; i++) { // fill up the send buffer
long start = System.currentTimeMillis();
System.out.printf("begin cycle #%s at %s%n", i, start);
cfText = webSocket.sendText(data, true);
try {
cfText.get(waitSec, TimeUnit.SECONDS);
data.clear();
} catch (TimeoutException e) {
break;
} finally {
long stop = System.currentTimeMillis();
System.out.printf("end cycle #%s at %s (%s ms)%n", i, stop, stop - start);
}
}
cfPing = webSocket.sendPing(ByteBuffer.allocate(125));
webSocket.request(N);
allButCloseReceived.await();
webSocket.request(1); // Receive the last message: Close
List<MockListener.Invocation> invocations = listener.invocations();
cfClose = webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
assertFalse(invocations.contains(new MockListener.OnError(webSocket, IOException.class)));
assertFalse(cfText.isDone());
assertFalse(cfPing.isDone());
assertFalse(cfClose.isDone());
return null;
}, () -> cfText.isDone());
}
}

View file

@ -0,0 +1,62 @@
/*
* Copyright (c) 2017, 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 8164625
* @summary Verifies HttpClient yields the connection to the WebSocket
* @build DummyWebSocketServer
* @run main/othervm -Djdk.httpclient.HttpClient.log=trace ConnectionHandoverTest
*/
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.WebSocket;
public class ConnectionHandoverTest {
/*
* An I/O channel associated with the connection is closed by WebSocket.abort().
* If this connection is returned to the connection pool, then the second
* attempt to use it would fail with a ClosedChannelException.
*
* The assumption is that since the WebSocket client is connecting to the
* same URI, the pooled connection is to be used.
*/
public static void main(String[] args) throws IOException {
try (DummyWebSocketServer server = new DummyWebSocketServer()) {
server.open();
URI uri = server.getURI();
WebSocket.Builder webSocketBuilder =
HttpClient.newHttpClient().newWebSocketBuilder();
WebSocket ws1 = webSocketBuilder
.buildAsync(uri, new WebSocket.Listener() { }).join();
ws1.abort();
WebSocket ws2 = webSocketBuilder
.buildAsync(uri, new WebSocket.Listener() { }).join(); // Exception here if the connection was pooled
ws2.abort();
}
}
}

View file

@ -0,0 +1,610 @@
/*
* Copyright (c) 2020, 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 javax.net.ServerSocketFactory;
import javax.net.ssl.SSLServerSocketFactory;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.ServerSocket;
import java.net.SocketAddress;
import java.net.SocketOption;
import java.net.StandardSocketOptions;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.CharacterCodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiFunction;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static java.lang.String.format;
import static java.lang.System.err;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.asList;
import static java.util.Objects.requireNonNull;
/**
* Dummy WebSocket Server, which supports TLS.
* By default the dummy webserver uses a plain TCP connection,
* but it can use a TLS connection if secure() is called before
* open(). It will use the default SSL context.
*
* Performs simpler version of the WebSocket Opening Handshake over HTTP (i.e.
* no proxying, cookies, etc.) Supports sequential connections, one at a time,
* i.e. in order for a client to connect to the server the previous client must
* disconnect first.
*
* Expected client request:
*
* GET /chat HTTP/1.1
* Host: server.example.com
* Upgrade: websocket
* Connection: Upgrade
* Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
* Origin: http://example.com
* Sec-WebSocket-Protocol: chat, superchat
* Sec-WebSocket-Version: 13
*
* This server response:
*
* HTTP/1.1 101 Switching Protocols
* Upgrade: websocket
* Connection: Upgrade
* Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
* Sec-WebSocket-Protocol: chat
*/
public class DummySecureWebSocketServer implements Closeable {
/**
* Emulates some of the SocketChannel APIs over a Socket
* instance.
*/
public static class WebSocketChannel implements AutoCloseable {
interface Reader {
int read(ByteBuffer buf) throws IOException;
}
interface Writer {
void write(ByteBuffer buf) throws IOException;
}
interface Config {
<T> void setOption(SocketOption<T> option, T value) throws IOException;
}
interface Closer {
void close() throws IOException;
}
final AutoCloseable channel;
final Reader reader;
final Writer writer;
final Config config;
final Closer closer;
WebSocketChannel(AutoCloseable channel, Reader reader, Writer writer, Config config, Closer closer) {
this.channel = channel;
this.reader = reader;
this.writer = writer;
this.config = config;
this.closer = closer;
}
public void close() throws IOException {
closer.close();
}
public String toString() {
return channel.toString();
}
public int read(ByteBuffer bb) throws IOException {
return reader.read(bb);
}
public void write(ByteBuffer bb) throws IOException {
writer.write(bb);
}
public <T> void setOption(SocketOption<T> option, T value) throws IOException {
config.setOption(option, value);
}
public static WebSocketChannel of(Socket s) {
Reader reader = (bb) -> DummySecureWebSocketServer.read(s.getInputStream(), bb);
Writer writer = (bb) -> DummySecureWebSocketServer.write(s.getOutputStream(), bb);
return new WebSocketChannel(s, reader, writer, s::setOption, s::close);
}
}
/**
* Emulates some of the ServerSocketChannel APIs over a ServerSocket
* instance.
*/
public static class WebServerSocketChannel implements AutoCloseable {
interface Accepter {
WebSocketChannel accept() throws IOException;
}
interface Binder {
void bind(SocketAddress address) throws IOException;
}
interface Config {
<T> void setOption(SocketOption<T> option, T value) throws IOException;
}
interface Closer {
void close() throws IOException;
}
interface Addressable {
SocketAddress getLocalAddress() throws IOException;
}
final AutoCloseable server;
final Accepter accepter;
final Binder binder;
final Addressable address;
final Config config;
final Closer closer;
WebServerSocketChannel(AutoCloseable server,
Accepter accepter,
Binder binder,
Addressable address,
Config config,
Closer closer) {
this.server = server;
this.accepter = accepter;
this.binder = binder;
this.address = address;
this.config = config;
this.closer = closer;
}
public void close() throws IOException {
closer.close();
}
public String toString() {
return server.toString();
}
public WebSocketChannel accept() throws IOException {
return accepter.accept();
}
public void bind(SocketAddress address) throws IOException {
binder.bind(address);
}
public <T> void setOption(SocketOption<T> option, T value) throws IOException {
config.setOption(option, value);
}
public SocketAddress getLocalAddress() throws IOException {
return address.getLocalAddress();
}
public static WebServerSocketChannel of(ServerSocket ss) {
Accepter a = () -> WebSocketChannel.of(ss.accept());
return new WebServerSocketChannel(ss, a, ss::bind, ss::getLocalSocketAddress, ss::setOption, ss::close);
}
}
// Creates a secure WebServerSocketChannel
static WebServerSocketChannel openWSS() throws IOException {
return WebServerSocketChannel.of(SSLServerSocketFactory.getDefault().createServerSocket());
}
// Creates a plain WebServerSocketChannel
static WebServerSocketChannel openWS() throws IOException {
return WebServerSocketChannel.of(ServerSocketFactory.getDefault().createServerSocket());
}
static int read(InputStream str, ByteBuffer buffer) throws IOException {
int len = Math.min(buffer.remaining(), 1024);
if (len <= 0) return 0;
byte[] bytes = new byte[len];
int res = 0;
if (buffer.hasRemaining()) {
len = Math.min(len, buffer.remaining());
int n = str.read(bytes, 0, len);
if (n > 0) {
buffer.put(bytes, 0, n);
res += n;
} else if (res > 0) {
return res;
} else {
return n;
}
}
return res;
}
static void write(OutputStream str, ByteBuffer buffer) throws IOException {
int len = Math.min(buffer.remaining(), 1024);
if (len <= 0) return;
byte[] bytes = new byte[len];
int res = 0;
int pos = buffer.position();
while (buffer.hasRemaining()) {
len = Math.min(len, buffer.remaining());
buffer.get(bytes, 0, len);
str.write(bytes, 0, len);
}
}
private final AtomicBoolean started = new AtomicBoolean();
private final Thread thread;
private volatile WebServerSocketChannel ss;
private volatile InetSocketAddress address;
private volatile boolean secure;
private ByteBuffer read = ByteBuffer.allocate(16384);
private final CountDownLatch readReady = new CountDownLatch(1);
private volatile boolean done;
private static class Credentials {
private final String name;
private final String password;
private Credentials(String name, String password) {
this.name = name;
this.password = password;
}
public String name() { return name; }
public String password() { return password; }
}
public DummySecureWebSocketServer() {
this(defaultMapping(), null, null);
}
public DummySecureWebSocketServer(String username, String password) {
this(defaultMapping(), username, password);
}
public DummySecureWebSocketServer(BiFunction<List<String>,Credentials,List<String>> mapping,
String username,
String password) {
requireNonNull(mapping);
Credentials credentials = username != null ?
new Credentials(username, password) : null;
thread = new Thread(() -> {
try {
while (!Thread.currentThread().isInterrupted() && !done) {
err.println("Accepting next connection at: " + ss);
WebSocketChannel channel = ss.accept();
err.println("Accepted: " + channel);
try {
channel.setOption(StandardSocketOptions.TCP_NODELAY, true);
while (!done) {
StringBuilder request = new StringBuilder();
if (!readRequest(channel, request)) {
throw new IOException("Bad request:[" + request + "]");
}
List<String> strings = asList(request.toString().split("\r\n"));
List<String> response = mapping.apply(strings, credentials);
writeResponse(channel, response);
if (response.get(0).startsWith("HTTP/1.1 401")) {
err.println("Sent 401 Authentication response " + channel);
continue;
} else {
serve(channel);
break;
}
}
} catch (IOException e) {
if (!done) {
err.println("Error in connection: " + channel + ", " + e);
}
} finally {
err.println("Closed: " + channel);
close(channel);
readReady.countDown();
}
}
} catch (ClosedByInterruptException ignored) {
} catch (Throwable e) {
if (!done) {
e.printStackTrace(err);
}
} finally {
done = true;
close(ss);
err.println("Stopped at: " + getURI());
}
});
thread.setName("DummySecureWebSocketServer");
thread.setDaemon(false);
}
// must be called before open()
public DummySecureWebSocketServer secure() {
secure = true;
return this;
}
protected void read(WebSocketChannel ch) throws IOException {
// Read until the thread is interrupted or an error occurred
// or the input is shutdown
ByteBuffer b = ByteBuffer.allocate(65536);
while (ch.read(b) != -1) {
b.flip();
if (read.remaining() < b.remaining()) {
int required = read.capacity() - read.remaining() + b.remaining();
int log2required = 32 - Integer.numberOfLeadingZeros(required - 1);
ByteBuffer newBuffer = ByteBuffer.allocate(1 << log2required);
newBuffer.put(read.flip());
read = newBuffer;
}
read.put(b);
b.clear();
}
}
protected void write(WebSocketChannel ch) throws IOException { }
protected final void serve(WebSocketChannel channel)
throws InterruptedException
{
Thread reader = new Thread(() -> {
try {
read(channel);
} catch (IOException ignored) { }
});
Thread writer = new Thread(() -> {
try {
write(channel);
} catch (IOException ignored) { }
});
reader.start();
writer.start();
try {
while (!done) {
try {
reader.join(500);
} catch (InterruptedException x) {
if (done) {
close(channel);
break;
}
}
}
} finally {
reader.interrupt();
try {
while (!done) {
try {
writer.join(500);
} catch (InterruptedException x) {
if (done) break;
}
}
} finally {
writer.interrupt();
}
}
}
public ByteBuffer read() throws InterruptedException {
readReady.await();
return read.duplicate().asReadOnlyBuffer().flip();
}
public void open() throws IOException {
err.println("Starting");
if (!started.compareAndSet(false, true)) {
throw new IllegalStateException("Already started");
}
ss = secure ? openWSS() : openWS();
try {
ss.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0));
address = (InetSocketAddress) ss.getLocalAddress();
thread.start();
} catch (IOException e) {
done = true;
close(ss);
throw e;
}
err.println("Started at: " + getURI());
}
@Override
public void close() {
err.println("Stopping: " + getURI());
done = true;
thread.interrupt();
close(ss);
}
URI getURI() {
if (!started.get()) {
throw new IllegalStateException("Not yet started");
}
if (!secure) {
return URI.create("ws://localhost:" + address.getPort());
} else {
return URI.create("wss://localhost:" + address.getPort());
}
}
private boolean readRequest(WebSocketChannel channel, StringBuilder request)
throws IOException
{
ByteBuffer buffer = ByteBuffer.allocate(512);
while (channel.read(buffer) != -1) {
// read the complete HTTP request headers, there should be no body
CharBuffer decoded;
buffer.flip();
try {
decoded = ISO_8859_1.newDecoder().decode(buffer);
} catch (CharacterCodingException e) {
throw new UncheckedIOException(e);
}
request.append(decoded);
if (Pattern.compile("\r\n\r\n").matcher(request).find())
return true;
buffer.clear();
}
return false;
}
private void writeResponse(WebSocketChannel channel, List<String> response)
throws IOException
{
String s = response.stream().collect(Collectors.joining("\r\n"))
+ "\r\n\r\n";
ByteBuffer encoded;
try {
encoded = ISO_8859_1.newEncoder().encode(CharBuffer.wrap(s));
} catch (CharacterCodingException e) {
throw new UncheckedIOException(e);
}
while (encoded.hasRemaining()) {
channel.write(encoded);
}
}
private static BiFunction<List<String>,Credentials,List<String>> defaultMapping() {
return (request, credentials) -> {
List<String> response = new LinkedList<>();
Iterator<String> iterator = request.iterator();
if (!iterator.hasNext()) {
throw new IllegalStateException("The request is empty");
}
String statusLine = iterator.next();
if (!(statusLine.startsWith("GET /") && statusLine.endsWith(" HTTP/1.1"))) {
throw new IllegalStateException
("Unexpected status line: " + request.get(0));
}
response.add("HTTP/1.1 101 Switching Protocols");
Map<String, List<String>> requestHeaders = new HashMap<>();
while (iterator.hasNext()) {
String header = iterator.next();
String[] split = header.split(": ");
if (split.length != 2) {
throw new IllegalStateException
("Unexpected header: " + header
+ ", split=" + Arrays.toString(split));
}
requestHeaders.computeIfAbsent(split[0], k -> new ArrayList<>()).add(split[1]);
}
if (requestHeaders.containsKey("Sec-WebSocket-Protocol")) {
throw new IllegalStateException("Subprotocols are not expected");
}
if (requestHeaders.containsKey("Sec-WebSocket-Extensions")) {
throw new IllegalStateException("Extensions are not expected");
}
expectHeader(requestHeaders, "Connection", "Upgrade");
response.add("Connection: Upgrade");
expectHeader(requestHeaders, "Upgrade", "websocket");
response.add("Upgrade: websocket");
expectHeader(requestHeaders, "Sec-WebSocket-Version", "13");
List<String> key = requestHeaders.get("Sec-WebSocket-Key");
if (key == null || key.isEmpty()) {
throw new IllegalStateException("Sec-WebSocket-Key is missing");
}
if (key.size() != 1) {
throw new IllegalStateException("Sec-WebSocket-Key has too many values : " + key);
}
MessageDigest sha1 = null;
try {
sha1 = MessageDigest.getInstance("SHA-1");
} catch (NoSuchAlgorithmException e) {
throw new InternalError(e);
}
String x = key.get(0) + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
sha1.update(x.getBytes(ISO_8859_1));
String v = Base64.getEncoder().encodeToString(sha1.digest());
response.add("Sec-WebSocket-Accept: " + v);
// check authorization credentials, if required by the server
if (credentials != null && !authorized(credentials, requestHeaders)) {
response.clear();
response.add("HTTP/1.1 401 Unauthorized");
response.add("Content-Length: 0");
response.add("WWW-Authenticate: Basic realm=\"dummy server realm\"");
}
return response;
};
}
// Checks credentials in the request against those allowable by the server.
private static boolean authorized(Credentials credentials,
Map<String,List<String>> requestHeaders) {
List<String> authorization = requestHeaders.get("Authorization");
if (authorization == null)
return false;
if (authorization.size() != 1) {
throw new IllegalStateException("Authorization unexpected count:" + authorization);
}
String header = authorization.get(0);
if (!header.startsWith("Basic "))
throw new IllegalStateException("Authorization not Basic: " + header);
header = header.substring("Basic ".length());
String values = new String(Base64.getDecoder().decode(header), UTF_8);
int sep = values.indexOf(':');
if (sep < 1) {
throw new IllegalStateException("Authorization not colon: " + values);
}
String name = values.substring(0, sep);
String password = values.substring(sep + 1);
if (name.equals(credentials.name()) && password.equals(credentials.password()))
return true;
return false;
}
protected static String expectHeader(Map<String, List<String>> headers,
String name,
String value) {
List<String> v = headers.get(name);
if (v == null) {
throw new IllegalStateException(
format("Expected '%s' header, not present in %s",
name, headers));
}
if (!v.contains(value)) {
throw new IllegalStateException(
format("Expected '%s: %s', actual: '%s: %s'",
name, value, name, v)
);
}
return value;
}
private static void close(AutoCloseable... acs) {
for (AutoCloseable ac : acs) {
try {
ac.close();
} catch (Exception ignored) { }
}
}
}

View file

@ -0,0 +1,521 @@
/*
* Copyright (c) 2016, 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.
*/
import jdk.internal.net.http.websocket.Frame;
import java.io.Closeable;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.CharacterCodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiFunction;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static java.lang.String.format;
import static java.lang.System.err;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.asList;
import static java.util.Objects.requireNonNull;
/**
* Dummy WebSocket Server.
*
* Performs simpler version of the WebSocket Opening Handshake over HTTP (i.e.
* no proxying, cookies, etc.) Supports sequential connections, one at a time,
* i.e. in order for a client to connect to the server the previous client must
* disconnect first.
*
* Expected client request:
*
* GET /chat HTTP/1.1
* Host: server.example.com
* Upgrade: websocket
* Connection: Upgrade
* Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
* Origin: http://example.com
* Sec-WebSocket-Protocol: chat, superchat
* Sec-WebSocket-Version: 13
*
* This server response:
*
* HTTP/1.1 101 Switching Protocols
* Upgrade: websocket
* Connection: Upgrade
* Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
* Sec-WebSocket-Protocol: chat
*/
public class DummyWebSocketServer implements Closeable {
private final AtomicBoolean started = new AtomicBoolean();
private final Thread thread;
private volatile ServerSocketChannel ssc;
private volatile InetSocketAddress address;
private ByteBuffer read = ByteBuffer.allocate(16384);
private final CountDownLatch readReady = new CountDownLatch(1);
private volatile int receiveBufferSize;
private static class Credentials {
private final String name;
private final String password;
private Credentials(String name, String password) {
this.name = name;
this.password = password;
}
public String name() { return name; }
public String password() { return password; }
}
public DummyWebSocketServer() {
this(defaultMapping(), null, null);
}
public DummyWebSocketServer(String username, String password) {
this(defaultMapping(), username, password);
}
public DummyWebSocketServer(BiFunction<List<String>,Credentials,List<String>> mapping,
String username,
String password) {
requireNonNull(mapping);
Credentials credentials = username != null ?
new Credentials(username, password) : null;
thread = new Thread(() -> {
try {
while (!Thread.currentThread().isInterrupted()) {
err.println("Accepting next connection at: " + ssc);
SocketChannel channel = ssc.accept();
err.println("Accepted: " + channel);
try {
channel.setOption(StandardSocketOptions.TCP_NODELAY, true);
channel.configureBlocking(true);
while (true) {
StringBuilder request = new StringBuilder();
if (!readRequest(channel, request)) {
throw new IOException("Bad request:[" + request + "]");
}
List<String> strings = asList(request.toString().split("\r\n"));
List<String> response = mapping.apply(strings, credentials);
writeResponse(channel, response);
if (response.get(0).startsWith("HTTP/1.1 401")) {
err.println("Sent 401 Authentication response " + channel);
continue;
} else {
serve(channel);
break;
}
}
} catch (IOException e) {
err.println("Error in connection: " + channel + ", " + e);
} finally {
err.println("Closed: " + channel);
closeChannel(channel);
readReady.countDown();
}
}
} catch (ClosedByInterruptException ignored) {
} catch (Exception e) {
e.printStackTrace(err);
} finally {
close(ssc);
err.println("Stopped at: " + getURI());
}
});
thread.setName("DummyWebSocketServer");
thread.setDaemon(false);
}
protected void read(SocketChannel ch) throws IOException {
// Read until the thread is interrupted or an error occurred
// or the input is shutdown
ByteBuffer b = ByteBuffer.allocate(65536);
while (ch.read(b) != -1) {
b.flip();
if (read.remaining() < b.remaining()) {
int required = read.capacity() - read.remaining() + b.remaining();
int log2required = 32 - Integer.numberOfLeadingZeros(required - 1);
ByteBuffer newBuffer = ByteBuffer.allocate(1 << log2required);
newBuffer.put(read.flip());
read = newBuffer;
}
read.put(b);
b.clear();
}
}
protected void closeChannel(SocketChannel channel) {
close(channel);
}
protected void write(SocketChannel ch) throws IOException { }
protected final void serve(SocketChannel channel)
throws InterruptedException
{
Thread reader = new Thread(() -> {
try {
read(channel);
} catch (IOException ignored) { }
});
Thread writer = new Thread(() -> {
try {
write(channel);
} catch (IOException ignored) { }
});
reader.start();
writer.start();
try {
reader.join();
} finally {
reader.interrupt();
try {
writer.join();
} finally {
writer.interrupt();
}
}
}
public List<DecodedFrame> readFrames() throws InterruptedException {
ByteBuffer buffer = read();
Frame.Reader reader = new Frame.Reader();
DecodedFrameCollector consumer = new DecodedFrameCollector();
while (buffer.hasRemaining()) {
reader.readFrame(buffer, consumer);
}
return consumer.frames;
}
private static final class DecodedFrameCollector implements Frame.Consumer {
private final Frame.Masker masker = new Frame.Masker();
private final List<DecodedFrame> frames = new ArrayList<>();
private ByteBuffer data;
private Frame.Opcode opcode;
private boolean last;
@Override
public void fin(boolean value) {
last = value;
}
@Override
public void rsv1(boolean value) {
if (value) {
throw new AssertionError();
}
}
@Override
public void rsv2(boolean value) {
if (value) {
throw new AssertionError();
}
}
@Override
public void rsv3(boolean value) {
if (value) {
throw new AssertionError();
}
}
@Override
public void opcode(Frame.Opcode value) {
opcode = value;
}
@Override
public void mask(boolean value) {
if (!value) { // Frames from the client MUST be masked
throw new AssertionError();
}
}
@Override
public void payloadLen(long value) {
data = ByteBuffer.allocate((int) value);
}
@Override
public void maskingKey(int value) {
masker.setMask(value);
}
@Override
public void payloadData(ByteBuffer data) {
masker.applyMask(data, this.data);
}
@Override
public void endFrame() {
frames.add(new DecodedFrame(opcode, this.data.flip(), last));
}
}
public record DecodedFrame(Frame.Opcode opcode, ByteBuffer data, boolean last) {}
public ByteBuffer read() throws InterruptedException {
readReady.await();
return read.duplicate().asReadOnlyBuffer().flip();
}
public void setReceiveBufferSize(int bufsize) {
assert ssc == null : "Must configure before calling open()";
this.receiveBufferSize = bufsize;
}
public void open() throws IOException {
err.println("Starting");
if (!started.compareAndSet(false, true)) {
throw new IllegalStateException("Already started");
}
ssc = ServerSocketChannel.open();
try {
ssc.configureBlocking(true);
var bufsize = receiveBufferSize;
if (bufsize > 0) {
err.printf("Configuring receive buffer size to %d%n", bufsize);
try {
ssc.setOption(StandardSocketOptions.SO_RCVBUF, bufsize);
} catch (IOException x) {
err.printf("Failed to configure receive buffer size to %d%n", bufsize);
}
}
ssc.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0));
address = (InetSocketAddress) ssc.getLocalAddress();
thread.start();
} catch (IOException e) {
close(ssc);
throw e;
}
err.println("Started at: " + getURI());
}
@Override
public void close() {
err.println("Stopping: " + getURI());
thread.interrupt();
close(ssc);
}
URI getURI() {
if (!started.get()) {
throw new IllegalStateException("Not yet started");
}
String ip = address.getAddress().isAnyLocalAddress()
? InetAddress.getLoopbackAddress().getHostAddress()
: address.getAddress().getHostAddress();
if (ip.indexOf(':') >= 0) {
ip = String.format("[%s]", ip);
}
return URI.create("ws://" + ip + ":" + address.getPort());
}
private boolean readRequest(SocketChannel channel, StringBuilder request)
throws IOException
{
ByteBuffer buffer = ByteBuffer.allocate(512);
while (channel.read(buffer) != -1) {
// read the complete HTTP request headers, there should be no body
CharBuffer decoded;
buffer.flip();
try {
decoded = ISO_8859_1.newDecoder().decode(buffer);
} catch (CharacterCodingException e) {
throw new UncheckedIOException(e);
}
request.append(decoded);
if (Pattern.compile("\r\n\r\n").matcher(request).find())
return true;
buffer.clear();
}
return false;
}
private void writeResponse(SocketChannel channel, List<String> response)
throws IOException
{
String s = response.stream().collect(Collectors.joining("\r\n"))
+ "\r\n\r\n";
ByteBuffer encoded;
try {
encoded = ISO_8859_1.newEncoder().encode(CharBuffer.wrap(s));
} catch (CharacterCodingException e) {
throw new UncheckedIOException(e);
}
while (encoded.hasRemaining()) {
channel.write(encoded);
}
}
private static BiFunction<List<String>,Credentials,List<String>> defaultMapping() {
return (request, credentials) -> {
List<String> response = new LinkedList<>();
Iterator<String> iterator = request.iterator();
if (!iterator.hasNext()) {
throw new IllegalStateException("The request is empty");
}
String statusLine = iterator.next();
if (!(statusLine.startsWith("GET /") && statusLine.endsWith(" HTTP/1.1"))) {
throw new IllegalStateException
("Unexpected status line: " + request.get(0));
}
response.add("HTTP/1.1 101 Switching Protocols");
Map<String, List<String>> requestHeaders = new HashMap<>();
while (iterator.hasNext()) {
String header = iterator.next();
String[] split = header.split(": ");
if (split.length != 2) {
throw new IllegalStateException
("Unexpected header: " + header
+ ", split=" + Arrays.toString(split));
}
requestHeaders.computeIfAbsent(split[0], k -> new ArrayList<>()).add(split[1]);
}
if (requestHeaders.containsKey("Sec-WebSocket-Protocol")) {
throw new IllegalStateException("Subprotocols are not expected");
}
if (requestHeaders.containsKey("Sec-WebSocket-Extensions")) {
throw new IllegalStateException("Extensions are not expected");
}
expectHeader(requestHeaders, "Connection", "Upgrade");
response.add("Connection: Upgrade");
expectHeader(requestHeaders, "Upgrade", "websocket");
response.add("Upgrade: websocket");
expectHeader(requestHeaders, "Sec-WebSocket-Version", "13");
List<String> key = requestHeaders.get("Sec-WebSocket-Key");
if (key == null || key.isEmpty()) {
throw new IllegalStateException("Sec-WebSocket-Key is missing");
}
if (key.size() != 1) {
throw new IllegalStateException("Sec-WebSocket-Key has too many values : " + key);
}
MessageDigest sha1 = null;
try {
sha1 = MessageDigest.getInstance("SHA-1");
} catch (NoSuchAlgorithmException e) {
throw new InternalError(e);
}
String x = key.get(0) + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
sha1.update(x.getBytes(ISO_8859_1));
String v = Base64.getEncoder().encodeToString(sha1.digest());
response.add("Sec-WebSocket-Accept: " + v);
// check authorization credentials, if required by the server
if (credentials != null && !authorized(credentials, requestHeaders)) {
response.clear();
response.add("HTTP/1.1 401 Unauthorized");
response.add("Content-Length: 0");
response.add("WWW-Authenticate: Basic realm=\"dummy server realm\"");
}
return response;
};
}
// Checks credentials in the request against those allowable by the server.
private static boolean authorized(Credentials credentials,
Map<String,List<String>> requestHeaders) {
List<String> authorization = requestHeaders.get("Authorization");
if (authorization == null)
return false;
if (authorization.size() != 1) {
throw new IllegalStateException("Authorization unexpected count:" + authorization);
}
String header = authorization.get(0);
if (!header.startsWith("Basic "))
throw new IllegalStateException("Authorization not Basic: " + header);
header = header.substring("Basic ".length());
String values = new String(Base64.getDecoder().decode(header), UTF_8);
int sep = values.indexOf(':');
if (sep < 1) {
throw new IllegalStateException("Authorization not colon: " + values);
}
String name = values.substring(0, sep);
String password = values.substring(sep + 1);
if (name.equals(credentials.name()) && password.equals(credentials.password()))
return true;
return false;
}
protected static String expectHeader(Map<String, List<String>> headers,
String name,
String value) {
List<String> v = headers.get(name);
if (v == null) {
throw new IllegalStateException(
format("Expected '%s' header, not present in %s",
name, headers));
}
if (!v.contains(value)) {
throw new IllegalStateException(
format("Expected '%s: %s', actual: '%s: %s'",
name, value, name, v)
);
}
return value;
}
private static void close(AutoCloseable... acs) {
for (AutoCloseable ac : acs) {
try {
ac.close();
} catch (Exception ignored) { }
}
}
}

View file

@ -0,0 +1,202 @@
/*
* Copyright (c) 2020, 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.
*/
/*
* @test
* @bug 8245245
* @summary Test for Websocket URI encoding during HandShake
* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.TestServerConfigurator
* @modules java.net.http/jdk.internal.net.http.common
* jdk.httpserver
* @run junit/othervm -Djdk.internal.httpclient.debug=true HandshakeUrlEncodingTest
*/
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpsServer;
import com.sun.net.httpserver.HttpExchange;
import jdk.httpclient.test.lib.common.TestServerConfigurator;
import jdk.test.lib.net.SimpleSSLContext;
import jdk.test.lib.net.URIBuilder;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.WebSocket;
import java.net.http.WebSocketHandshakeException;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static java.net.http.HttpClient.Builder.NO_PROXY;
import static java.lang.System.out;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;
public class HandshakeUrlEncodingTest {
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
private static HttpServer httpTestServer;
private static HttpsServer httpsTestServer;
private static String httpURI;
private static String httpsURI;
static String queryPart;
static final int ITERATION_COUNT = 10;
// a shared executor helps reduce the amount of threads created by the test
static final ExecutorService executor = Executors.newCachedThreadPool();
public static Object[][] variants() {
return new Object[][]{
{ httpURI, false },
{ httpsURI, false },
{ httpURI, true },
{ httpsURI, true }
};
}
HttpClient newHttpClient() {
return HttpClient.newBuilder()
.proxy(NO_PROXY)
.executor(executor)
.sslContext(sslContext)
.build();
}
@ParameterizedTest
@MethodSource("variants")
public void test(String uri, boolean sameClient) {
HttpClient client = null;
out.println("The url is " + uri);
for (int i = 0; i < ITERATION_COUNT; i++) {
System.out.printf("iteration %s%n", i);
if (!sameClient || client == null)
client = newHttpClient();
try {
client.newWebSocketBuilder()
.buildAsync(URI.create(uri), new WebSocket.Listener() { })
.join();
fail("Expected to throw");
} catch (CompletionException ce) {
final Throwable t = getCompletionCause(ce);
if (!(t instanceof WebSocketHandshakeException)) {
throw new AssertionError("Unexpected exception", t);
}
final WebSocketHandshakeException wse = (WebSocketHandshakeException) t;
assertNotNull(wse.getResponse());
assertNotNull(wse.getResponse().uri());
final String rawQuery = wse.getResponse().uri().getRawQuery();
final String expectedRawQuery = "&raw=abc+def/ghi=xyz&encoded=abc%2Bdef%2Fghi%3Dxyz";
assertEquals(expectedRawQuery, rawQuery);
final String body = (String) wse.getResponse().body();
final String expectedBody = "/?" + expectedRawQuery;
assertEquals(expectedBody, body);
out.println("Status code is " + wse.getResponse().statusCode());
out.println("Response is " + wse.getResponse());
assertEquals(400, wse.getResponse().statusCode());
}
}
}
@BeforeAll
public static void setup() throws Exception {
InetSocketAddress sa = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
queryPart = "?&raw=abc+def/ghi=xyz&encoded=abc%2Bdef%2Fghi%3Dxyz";
httpTestServer = HttpServer.create(sa, 10);
httpURI = URIBuilder.newBuilder()
.scheme("ws")
.host("localhost")
.port(httpTestServer.getAddress().getPort())
.path("/")
.build()
.toString() + queryPart;
httpTestServer.createContext("/", new UrlHandler());
httpsTestServer = HttpsServer.create(sa, 10);
httpsTestServer.setHttpsConfigurator(new TestServerConfigurator(sa.getAddress(), sslContext));
httpsURI = URIBuilder.newBuilder()
.scheme("wss")
.host("localhost")
.port(httpsTestServer.getAddress().getPort())
.path("/")
.build()
.toString() + queryPart;
httpsTestServer.createContext("/", new UrlHandler());
httpTestServer.start();
httpsTestServer.start();
}
@AfterAll
public static void teardown() {
httpTestServer.stop(0);
httpsTestServer.stop(0);
executor.shutdownNow();
}
private static Throwable getCompletionCause(Throwable x) {
if (!(x instanceof CompletionException)
&& !(x instanceof ExecutionException)) return x;
final Throwable cause = x.getCause();
if (cause == null) {
throw new InternalError("Unexpected null cause", x);
}
return cause;
}
static class UrlHandler implements HttpHandler {
@Override
public void handle(HttpExchange e) throws IOException {
try(InputStream is = e.getRequestBody();
OutputStream os = e.getResponseBody()) {
String testUri = "/?&raw=abc+def/ghi=xyz&encoded=abc%2Bdef%2Fghi%3Dxyz";
URI uri = e.getRequestURI();
byte[] bytes = is.readAllBytes();
if (uri.toString().equals(testUri)) {
bytes = testUri.getBytes();
}
e.sendResponseHeaders(400, bytes.length);
os.write(bytes);
}
}
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2016, 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.
*/
/*
* @test
* @bug 8159053
* @modules java.net.http/jdk.internal.net.http.websocket:open
* @run junit/othervm
* --add-reads java.net.http=ALL-UNNAMED
* java.net.http/jdk.internal.net.http.websocket.HeaderWriterTest
*/
public final class HeaderWriterDriver { }

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2016, 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.
*/
/*
* @test
* @bug 8159053
* @modules java.net.http/jdk.internal.net.http.websocket:open
* @run junit/othervm
* --add-reads java.net.http=ALL-UNNAMED
* java.net.http/jdk.internal.net.http.websocket.MaskerTest
*/
public final class MaskerDriver { }

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2018, 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.
*/
/*
* @test
* @bug 8159053
* @modules java.net.http/jdk.internal.net.http.websocket:open
* @run junit/othervm
* --add-reads java.net.http=ALL-UNNAMED
* java.net.http/jdk.internal.net.http.websocket.MessageQueueTest
*/
public final class MessageQueueDriver { }

View file

@ -0,0 +1,490 @@
/*
* Copyright (c) 2017, 2018, 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.net.http.WebSocket;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.function.Predicate;
public class MockListener implements WebSocket.Listener {
private final long bufferSize;
private long count;
private final List<Invocation> invocations = new ArrayList<>();
private final CompletableFuture<?> lastCall = new CompletableFuture<>();
private final Predicate<? super Invocation> collectUntil;
public MockListener() {
this(1, MockListener::closeOrError);
}
public MockListener(long bufferSize) {
this(bufferSize, MockListener::closeOrError);
}
public MockListener(Predicate<? super Invocation> collectUntil) {
this(1, collectUntil);
}
/*
* Typical buffer sizes: 1, n, Long.MAX_VALUE
*/
public MockListener(long bufferSize,
Predicate<? super Invocation> collectUntil) {
if (bufferSize < 1) {
throw new IllegalArgumentException();
}
Objects.requireNonNull(collectUntil);
this.bufferSize = bufferSize;
this.collectUntil = collectUntil;
}
private static boolean closeOrError(Invocation i) {
return i instanceof OnClose || i instanceof OnError;
}
@Override
public void onOpen(WebSocket webSocket) {
System.out.printf("onOpen(%s)%n", webSocket);
OnOpen inv = new OnOpen(webSocket);
synchronized (invocations) {
invocations.add(inv);
}
if (collectUntil.test(inv)) {
lastCall.complete(null);
}
onOpen0(webSocket);
}
protected void onOpen0(WebSocket webSocket) {
count = bufferSize - bufferSize / 2;
System.out.printf("request(%d)%n", bufferSize);
webSocket.request(bufferSize);
}
@Override
public CompletionStage<?> onText(WebSocket webSocket,
CharSequence message,
boolean last) {
System.out.printf("onText(%s, message.length=%s, %s)%n", webSocket, message.length(), last);
OnText inv = new OnText(webSocket, message.toString(), last);
synchronized (invocations) {
invocations.add(inv);
}
if (collectUntil.test(inv)) {
lastCall.complete(null);
}
return onText0(webSocket, message, last);
}
protected CompletionStage<?> onText0(WebSocket webSocket,
CharSequence message,
boolean last) {
replenish(webSocket);
return null;
}
@Override
public CompletionStage<?> onBinary(WebSocket webSocket,
ByteBuffer message,
boolean last) {
System.out.printf("onBinary(%s, %s, %s)%n", webSocket, message, last);
OnBinary inv = new OnBinary(webSocket, fullCopy(message), last);
synchronized (invocations) {
invocations.add(inv);
}
if (collectUntil.test(inv)) {
lastCall.complete(null);
}
return onBinary0(webSocket, message, last);
}
protected CompletionStage<?> onBinary0(WebSocket webSocket,
ByteBuffer message,
boolean last) {
replenish(webSocket);
return null;
}
@Override
public CompletionStage<?> onPing(WebSocket webSocket, ByteBuffer message) {
System.out.printf("onPing(%s, %s)%n", webSocket, message);
OnPing inv = new OnPing(webSocket, fullCopy(message));
synchronized (invocations) {
invocations.add(inv);
}
if (collectUntil.test(inv)) {
lastCall.complete(null);
}
return onPing0(webSocket, message);
}
protected CompletionStage<?> onPing0(WebSocket webSocket, ByteBuffer message) {
replenish(webSocket);
return null;
}
@Override
public CompletionStage<?> onPong(WebSocket webSocket, ByteBuffer message) {
System.out.printf("onPong(%s, %s)%n", webSocket, message);
OnPong inv = new OnPong(webSocket, fullCopy(message));
synchronized (invocations) {
invocations.add(inv);
}
if (collectUntil.test(inv)) {
lastCall.complete(null);
}
return onPong0(webSocket, message);
}
protected CompletionStage<?> onPong0(WebSocket webSocket, ByteBuffer message) {
replenish(webSocket);
return null;
}
@Override
public CompletionStage<?> onClose(WebSocket webSocket,
int statusCode,
String reason) {
System.out.printf("onClose(%s, %s, %s)%n", webSocket, statusCode, reason);
OnClose inv = new OnClose(webSocket, statusCode, reason);
synchronized (invocations) {
invocations.add(inv);
}
if (collectUntil.test(inv)) {
lastCall.complete(null);
}
return onClose0(webSocket, statusCode, reason);
}
protected CompletionStage<?> onClose0(WebSocket webSocket,
int statusCode,
String reason) {
return null;
}
@Override
public void onError(WebSocket webSocket, Throwable error) {
System.out.printf("onError(%s, %s)%n", webSocket, error);
error.printStackTrace(System.out);
OnError inv = new OnError(webSocket, error == null ? null : error.getClass());
synchronized (invocations) {
invocations.add(inv);
}
if (collectUntil.test(inv)) {
lastCall.complete(null);
}
onError0(webSocket, error);
}
protected void onError0(WebSocket webSocket, Throwable error) { }
public List<Invocation> invocationsSoFar() {
synchronized (invocations) {
return new ArrayList<>(invocations);
}
}
public List<Invocation> invocations() {
lastCall.join();
synchronized (invocations) {
return new ArrayList<>(invocations);
}
}
protected void replenish(WebSocket webSocket) {
if (--count <= 0) {
count = bufferSize - bufferSize / 2;
webSocket.request(count);
System.out.printf("request(%d)%n", count);
}
}
public abstract static class Invocation {
public static OnOpen onOpen(WebSocket webSocket) {
return new OnOpen(webSocket);
}
public static OnText onText(WebSocket webSocket,
String text,
boolean last) {
return new OnText(webSocket, text, last);
}
public static OnBinary onBinary(WebSocket webSocket,
ByteBuffer data,
boolean last) {
return new OnBinary(webSocket, data, last);
}
public static OnPing onPing(WebSocket webSocket,
ByteBuffer data) {
return new OnPing(webSocket, data);
}
public static OnPong onPong(WebSocket webSocket,
ByteBuffer data) {
return new OnPong(webSocket, data);
}
public static OnClose onClose(WebSocket webSocket,
int statusCode,
String reason) {
return new OnClose(webSocket, statusCode, reason);
}
public static OnError onError(WebSocket webSocket,
Class<? extends Throwable> clazz) {
return new OnError(webSocket, clazz);
}
final WebSocket webSocket;
private Invocation(WebSocket webSocket) {
this.webSocket = webSocket;
}
}
public static final class OnOpen extends Invocation {
public OnOpen(WebSocket webSocket) {
super(webSocket);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Invocation that = (Invocation) o;
return Objects.equals(webSocket, that.webSocket);
}
@Override
public int hashCode() {
return Objects.hashCode(webSocket);
}
@Override
public String toString() {
return String.format("onOpen(%s)", webSocket);
}
}
public static final class OnText extends Invocation {
final String text;
final boolean last;
public OnText(WebSocket webSocket, String text, boolean last) {
super(webSocket);
this.text = text;
this.last = last;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OnText onText = (OnText) o;
return Objects.equals(text, onText.text) &&
last == onText.last &&
Objects.equals(webSocket, onText.webSocket);
}
@Override
public int hashCode() {
return Objects.hash(text, last, webSocket);
}
@Override
public String toString() {
return String.format("onText(%s, message.length=%s, %s)", webSocket, text.length(), last);
}
}
public static final class OnBinary extends Invocation {
final ByteBuffer data;
final boolean last;
public OnBinary(WebSocket webSocket, ByteBuffer data, boolean last) {
super(webSocket);
this.data = data;
this.last = last;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OnBinary onBinary = (OnBinary) o;
return Objects.equals(data, onBinary.data) &&
last == onBinary.last &&
Objects.equals(webSocket, onBinary.webSocket);
}
@Override
public int hashCode() {
return Objects.hash(data, last, webSocket);
}
@Override
public String toString() {
return String.format("onBinary(%s, %s, %s)", webSocket, data, last);
}
}
public static final class OnPing extends Invocation {
final ByteBuffer data;
public OnPing(WebSocket webSocket, ByteBuffer data) {
super(webSocket);
this.data = data;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OnPing onPing = (OnPing) o;
return Objects.equals(data, onPing.data) &&
Objects.equals(webSocket, onPing.webSocket);
}
@Override
public int hashCode() {
return Objects.hash(data, webSocket);
}
@Override
public String toString() {
return String.format("onPing(%s, %s)", webSocket, data);
}
}
public static final class OnPong extends Invocation {
final ByteBuffer data;
public OnPong(WebSocket webSocket, ByteBuffer data) {
super(webSocket);
this.data = data;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OnPong onPong = (OnPong) o;
return Objects.equals(data, onPong.data) &&
Objects.equals(webSocket, onPong.webSocket);
}
@Override
public int hashCode() {
return Objects.hash(data, webSocket);
}
@Override
public String toString() {
return String.format("onPong(%s, %s)", webSocket, data);
}
}
public static final class OnClose extends Invocation {
final int statusCode;
final String reason;
public OnClose(WebSocket webSocket, int statusCode, String reason) {
super(webSocket);
this.statusCode = statusCode;
this.reason = reason;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OnClose onClose = (OnClose) o;
return statusCode == onClose.statusCode &&
Objects.equals(reason, onClose.reason) &&
Objects.equals(webSocket, onClose.webSocket);
}
@Override
public int hashCode() {
return Objects.hash(statusCode, reason, webSocket);
}
@Override
public String toString() {
return String.format("onClose(%s, %s, %s)", webSocket, statusCode, reason);
}
}
public static final class OnError extends Invocation {
final Class<? extends Throwable> clazz;
public OnError(WebSocket webSocket, Class<? extends Throwable> clazz) {
super(webSocket);
this.clazz = clazz;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OnError onError = (OnError) o;
return Objects.equals(clazz, onError.clazz) &&
Objects.equals(webSocket, onError.webSocket);
}
@Override
public int hashCode() {
return Objects.hash(clazz, webSocket);
}
@Override
public String toString() {
return String.format("onError(%s, %s)", webSocket, clazz);
}
}
private static ByteBuffer fullCopy(ByteBuffer src) {
ByteBuffer copy = ByteBuffer.allocate(src.capacity());
int p = src.position();
int l = src.limit();
src.clear();
copy.put(src).position(p).limit(l);
src.position(p).limit(l);
return copy;
}
}

View file

@ -0,0 +1,91 @@
/*
* Copyright (c) 2018, 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.
*/
/*
* @test
* @build DummyWebSocketServer
* @run junit/othervm
* -Djdk.httpclient.sendBufferSize=8192
* -Djdk.internal.httpclient.debug=true
* -Djdk.internal.httpclient.websocket.debug=true
* PendingBinaryPingClose
*/
import java.net.http.WebSocket;
import java.nio.ByteBuffer;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
public class PendingBinaryPingClose extends PendingOperations {
CompletableFuture<WebSocket> cfBinary;
CompletableFuture<WebSocket> cfPing;
CompletableFuture<WebSocket> cfClose;
@ParameterizedTest
@MethodSource("booleans")
public void pendingBinaryPingClose(boolean last) throws Exception {
repeatable(() -> {
server = Support.notReadingServer();
server.setReceiveBufferSize(1024);
server.open();
webSocket = httpClient().newWebSocketBuilder()
.buildAsync(server.getURI(), new WebSocket.Listener() { })
.join();
ByteBuffer data = ByteBuffer.allocate(65536);
for (int i = 0; ; i++) { // fill up the send buffer
long start = System.currentTimeMillis();
System.out.printf("begin cycle #%s at %s%n", i, start);
cfBinary = webSocket.sendBinary(data, last);
try {
cfBinary.get(waitSec, TimeUnit.SECONDS);
data.clear();
} catch (TimeoutException e) {
break;
} finally {
long stop = System.currentTimeMillis();
System.out.printf("end cycle #%s at %s (%s ms)%n", i, stop, stop - start);
}
}
assertFails(ISE, webSocket.sendText("", true));
assertFails(ISE, webSocket.sendText("", false));
assertFails(ISE, webSocket.sendBinary(ByteBuffer.allocate(0), true));
assertFails(ISE, webSocket.sendBinary(ByteBuffer.allocate(0), false));
cfPing = webSocket.sendPing(ByteBuffer.allocate(125));
assertFails(ISE, webSocket.sendPing(ByteBuffer.allocate(125)));
assertFails(ISE, webSocket.sendPong(ByteBuffer.allocate(125)));
cfClose = webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
assertAllHang(cfPing, cfClose);
assertNotDone(cfBinary);
webSocket.abort();
assertFails(IOE, cfBinary);
assertFails(IOE, cfPing);
assertFails(IOE, cfClose);
return null;
}, () -> cfBinary.isDone());
}
}

View file

@ -0,0 +1,91 @@
/*
* Copyright (c) 2018, 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.
*/
/*
* @test
* @build DummyWebSocketServer
* @run junit/othervm
* -Djdk.httpclient.sendBufferSize=8192
* -Djdk.internal.httpclient.debug=true
* -Djdk.internal.httpclient.websocket.debug=true
* PendingBinaryPongClose
*/
import java.net.http.WebSocket;
import java.nio.ByteBuffer;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
public class PendingBinaryPongClose extends PendingOperations {
CompletableFuture<WebSocket> cfBinary;
CompletableFuture<WebSocket> cfPong;
CompletableFuture<WebSocket> cfClose;
@ParameterizedTest
@MethodSource("booleans")
public void pendingBinaryPongClose(boolean last) throws Exception {
repeatable(() -> {
server = Support.notReadingServer();
server.setReceiveBufferSize(1024);
server.open();
webSocket = httpClient().newWebSocketBuilder()
.buildAsync(server.getURI(), new WebSocket.Listener() { })
.join();
ByteBuffer data = ByteBuffer.allocate(65536);
for (int i = 0; ; i++) { // fill up the send buffer
long start = System.currentTimeMillis();
System.out.printf("begin cycle #%s at %s%n", i, start);
cfBinary = webSocket.sendBinary(data, last);
try {
cfBinary.get(waitSec, TimeUnit.SECONDS);
data.clear();
} catch (TimeoutException e) {
break;
} finally {
long stop = System.currentTimeMillis();
System.out.printf("end cycle #%s at %s (%s ms)%n", i, stop, stop - start);
}
}
assertFails(ISE, webSocket.sendText("", true));
assertFails(ISE, webSocket.sendText("", false));
assertFails(ISE, webSocket.sendBinary(ByteBuffer.allocate(0), true));
assertFails(ISE, webSocket.sendBinary(ByteBuffer.allocate(0), false));
cfPong = webSocket.sendPong(ByteBuffer.allocate(125));
assertFails(ISE, webSocket.sendPing(ByteBuffer.allocate(125)));
assertFails(ISE, webSocket.sendPong(ByteBuffer.allocate(125)));
cfClose = webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
assertAllHang(cfPong, cfClose);
assertNotDone(cfBinary);
webSocket.abort();
assertFails(IOE, cfBinary);
assertFails(IOE, cfPong);
assertFails(IOE, cfClose);
return null;
}, () -> cfBinary.isDone());
}
}

View file

@ -0,0 +1,142 @@
/*
* Copyright (c) 2018, 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.net.http.HttpClient;
import java.net.http.WebSocket;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.function.BooleanSupplier;
import static java.net.http.HttpClient.Builder.NO_PROXY;
import static java.net.http.HttpClient.newBuilder;
import org.junit.jupiter.api.AfterEach;
/* Common infrastructure for tests that check pending operations */
public class PendingOperations {
static final Class<IllegalStateException> ISE = IllegalStateException.class;
static final Class<IOException> IOE = IOException.class;
// Time after which we deem that the local send buffer and remote
// receive buffer must be full.
// At the time of writing, using anything <= 5s on Mac will make the
// tests fail intermittently.
static final long MAX_WAIT_SEC = 10; // seconds.
long waitSec;
DummyWebSocketServer server;
WebSocket webSocket;
protected HttpClient httpClient() {
return newBuilder().proxy(NO_PROXY).build();
}
@AfterEach
public void cleanup() {
// make sure we have a trace both on System.out and System.err
// to help with diagnosis.
System.out.println("cleanup: Closing server");
System.err.println("cleanup: Closing server");
server.close();
webSocket.abort();
}
/* shortcut */
static void assertAllHang(CompletableFuture<?> cf1,
CompletableFuture<?> cf2) {
assertHangs(CompletableFuture.anyOf(cf1, cf2));
}
/* shortcut */
static void assertHangs(CompletionStage<?> stage) {
Support.assertHangs(stage);
}
/* shortcut */
static void assertFails(Class<? extends Throwable> clazz,
CompletionStage<?> stage) {
Support.assertCompletesExceptionally(clazz, stage);
}
static void assertNotDone(CompletableFuture<?> future) {
Support.assertNotDone(future);
}
public static Object[][] booleans() {
return new Object[][]{{Boolean.TRUE}, {Boolean.FALSE}};
}
static boolean isMacOS() {
return System.getProperty("os.name").contains("OS X");
}
static boolean isWindows() {
return System.getProperty("os.name").toLowerCase().startsWith("win");
}
private static final int ITERATIONS = 3;
void repeatable(Callable<Void> callable,
BooleanSupplier repeatCondition)
throws Exception
{
int iterations = 0;
do {
iterations++;
if (iterations == 1) {
waitSec = initialWaitSec();
} else {
waitSec = MAX_WAIT_SEC;
}
System.out.println("--- iteration " + iterations + " ---");
try {
callable.call();
break;
} catch (AssertionError e) {
var isMac = isMacOS();
var isWindows = isWindows();
var repeat = repeatCondition.getAsBoolean();
System.out.printf("repeatable: isMac=%s, isWindows=%s, repeat=%s, iterations=%d%n",
isMac, isWindows, repeat, iterations);
if ((isMac || isWindows) && repeat) {
// ## This is loathsome, but necessary because of observed
// ## automagic socket buffer resizing on recent macOS platforms
try { cleanup(); } catch (Throwable x) {}
continue;
} else {
throw e;
}
} finally {
// gives some time to gc to cleanup any resource that might
// be eligible for garbage collection
System.gc();
Thread.sleep(100);
}
} while (iterations <= ITERATIONS);
}
long initialWaitSec() {
return 1;
}
}

View file

@ -0,0 +1,95 @@
/*
* Copyright (c) 2018, 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.
*/
/*
* @test
* @build DummyWebSocketServer
* @run junit/othervm
* -Djdk.httpclient.sendBufferSize=8192
* PendingPingBinaryClose
*/
// This test produce huge logs (14Mb+) so disable logging by default
// * -Djdk.internal.httpclient.debug=true
// * -Djdk.internal.httpclient.websocket.debug=true
import java.net.http.WebSocket;
import java.nio.ByteBuffer;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
public class PendingPingBinaryClose extends PendingOperations {
CompletableFuture<WebSocket> cfBinary;
CompletableFuture<WebSocket> cfPing;
CompletableFuture<WebSocket> cfClose;
@ParameterizedTest
@MethodSource("booleans")
public void pendingPingBinaryClose(boolean last) throws Exception {
repeatable( () -> {
server = Support.notReadingServer();
server.setReceiveBufferSize(1024);
server.open();
webSocket = httpClient().newWebSocketBuilder()
.buildAsync(server.getURI(), new WebSocket.Listener() { })
.join();
ByteBuffer data = ByteBuffer.allocate(125);
for (int i = 0; ; i++) { // fill up the send buffer
long start = System.currentTimeMillis();
System.out.printf("begin cycle #%s at %s%n", i, start);
cfPing = webSocket.sendPing(data);
try {
cfPing.get(waitSec, TimeUnit.SECONDS);
data.clear();
} catch (TimeoutException e) {
break;
} finally {
long stop = System.currentTimeMillis();
System.out.printf("end cycle #%s at %s (%s ms)%n", i, stop, stop - start);
}
}
assertFails(ISE, webSocket.sendPing(ByteBuffer.allocate(125)));
assertFails(ISE, webSocket.sendPong(ByteBuffer.allocate(125)));
cfBinary = webSocket.sendBinary(ByteBuffer.allocate(4), last);
cfClose = webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
assertAllHang(cfBinary, cfClose);
assertNotDone(cfPing);
webSocket.abort();
assertFails(IOE, cfPing);
assertFails(IOE, cfBinary);
assertFails(IOE, cfClose);
return null;
}, () -> cfPing.isDone());
}
@Override
long initialWaitSec() {
// Some Windows machines increase buffer size after 1-2 seconds
return isWindows() ? 3 : 1;
}
}

View file

@ -0,0 +1,109 @@
/*
* Copyright (c) 2018, 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.
*/
/*
* @test
* @build DummyWebSocketServer
* @run junit/othervm
* -Djdk.httpclient.sendBufferSize=8192
* PendingPingTextClose
*/
// This test produce huge logs (14Mb+) so disable logging by default
// * -Djdk.internal.httpclient.debug=true
// * -Djdk.internal.httpclient.websocket.debug=true
import java.net.http.WebSocket;
import java.nio.ByteBuffer;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
public class PendingPingTextClose extends PendingOperations {
static boolean debug = false; // avoid too verbose output
CompletableFuture<WebSocket> cfText;
CompletableFuture<WebSocket> cfPing;
CompletableFuture<WebSocket> cfClose;
@ParameterizedTest
@MethodSource("booleans")
public void pendingPingTextClose(boolean last) throws Exception {
try {
repeatable(() -> {
server = Support.notReadingServer();
server.setReceiveBufferSize(1024);
server.open();
webSocket = httpClient().newWebSocketBuilder()
.buildAsync(server.getURI(), new WebSocket.Listener() { })
.join();
ByteBuffer data = ByteBuffer.allocate(125);
boolean done = false;
for (int i = 0; ; i++) { // fill up the send buffer
long start = System.currentTimeMillis();
if (debug) System.out.printf("begin cycle #%s at %s%n", i, start);
cfPing = webSocket.sendPing(data);
try {
cfPing.get(waitSec, TimeUnit.SECONDS);
data.clear();
} catch (TimeoutException e) {
done = true;
System.out.printf("Got expected timeout after %d iterations%n", i);
break;
} finally {
long stop = System.currentTimeMillis();
if (debug || done || (stop - start) > (waitSec * 1000L)/2L)
System.out.printf("end cycle #%s at %s (%s ms)%n", i, stop, stop - start);
}
}
assertFails(ISE, webSocket.sendPing(ByteBuffer.allocate(125)));
assertFails(ISE, webSocket.sendPong(ByteBuffer.allocate(125)));
cfText = webSocket.sendText("hello", last);
cfClose = webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
System.out.println("asserting that sendText and sendClose hang");
assertAllHang(cfText, cfClose);
System.out.println("asserting that cfPing is not completed");
assertNotDone(cfPing);
System.out.println("finishing");
webSocket.abort();
assertFails(IOE, cfPing);
assertFails(IOE, cfText);
assertFails(IOE, cfClose);
return null;
}, () -> cfPing.isDone()); // can't use method ref: cfPing not initialized
} catch (Throwable t) {
System.err.printf("pendingPingTextClose(%s) failed: %s%n", last, t);
t.printStackTrace();
throw t;
}
}
@Override
long initialWaitSec() {
// Some Windows machines increase buffer size after 1-2 seconds
return isWindows() ? 3 : 1;
}
}

View file

@ -0,0 +1,95 @@
/*
* Copyright (c) 2018, 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.
*/
/*
* @test
* @build DummyWebSocketServer
* @run junit/othervm
* -Djdk.httpclient.sendBufferSize=8192
* PendingPongBinaryClose
*/
// This test produce huge logs (14Mb+) so disable logging by default
// * -Djdk.internal.httpclient.debug=true
// * -Djdk.internal.httpclient.websocket.debug=true
import java.net.http.WebSocket;
import java.nio.ByteBuffer;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
public class PendingPongBinaryClose extends PendingOperations {
CompletableFuture<WebSocket> cfBinary;
CompletableFuture<WebSocket> cfPong;
CompletableFuture<WebSocket> cfClose;
@ParameterizedTest
@MethodSource("booleans")
public void pendingPongBinaryClose(boolean last) throws Exception {
repeatable( () -> {
server = Support.notReadingServer();
server.setReceiveBufferSize(1024);
server.open();
webSocket = httpClient().newWebSocketBuilder()
.buildAsync(server.getURI(), new WebSocket.Listener() { })
.join();
ByteBuffer data = ByteBuffer.allocate(125);
for (int i = 0; ; i++) { // fill up the send buffer
long start = System.currentTimeMillis();
System.out.printf("begin cycle #%s at %s%n", i, start);
cfPong = webSocket.sendPong(data);
try {
cfPong.get(waitSec, TimeUnit.SECONDS);
data.clear();
} catch (TimeoutException e) {
break;
} finally {
long stop = System.currentTimeMillis();
System.out.printf("end cycle #%s at %s (%s ms)%n", i, stop, stop - start);
}
}
assertFails(ISE, webSocket.sendPing(ByteBuffer.allocate(125)));
assertFails(ISE, webSocket.sendPong(ByteBuffer.allocate(125)));
cfBinary = webSocket.sendBinary(ByteBuffer.allocate(4), last);
cfClose = webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
assertAllHang(cfBinary, cfClose);
assertNotDone(cfPong);
webSocket.abort();
assertFails(IOE, cfPong);
assertFails(IOE, cfBinary);
assertFails(IOE, cfClose);
return null;
}, () -> cfPong.isDone());
}
@Override
long initialWaitSec() {
// Some Windows machines increase buffer size after 1-2 seconds
return isWindows() ? 3 : 1;
}
}

View file

@ -0,0 +1,95 @@
/*
* Copyright (c) 2018, 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.
*/
/*
* @test
* @build DummyWebSocketServer
* @run junit/othervm
* -Djdk.httpclient.sendBufferSize=8192
* PendingPongTextClose
*/
// This test produce huge logs (14Mb+) so disable logging by default
// * -Djdk.internal.httpclient.debug=true
// * -Djdk.internal.httpclient.websocket.debug=true
import java.net.http.WebSocket;
import java.nio.ByteBuffer;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
public class PendingPongTextClose extends PendingOperations {
CompletableFuture<WebSocket> cfText;
CompletableFuture<WebSocket> cfPong;
CompletableFuture<WebSocket> cfClose;
@ParameterizedTest
@MethodSource("booleans")
public void pendingPongTextClose(boolean last) throws Exception {
repeatable( () -> {
server = Support.notReadingServer();
server.setReceiveBufferSize(1024);
server.open();
webSocket = httpClient().newWebSocketBuilder()
.buildAsync(server.getURI(), new WebSocket.Listener() { })
.join();
ByteBuffer data = ByteBuffer.allocate(125);
for (int i = 0; ; i++) { // fill up the send buffer
long start = System.currentTimeMillis();
System.out.printf("begin cycle #%s at %s%n", i, start);
cfPong = webSocket.sendPong(data);
try {
cfPong.get(waitSec, TimeUnit.SECONDS);
data.clear();
} catch (TimeoutException e) {
break;
} finally {
long stop = System.currentTimeMillis();
System.out.printf("end cycle #%s at %s (%s ms)%n", i, stop, stop - start);
}
}
assertFails(ISE, webSocket.sendPing(ByteBuffer.allocate(125)));
assertFails(ISE, webSocket.sendPong(ByteBuffer.allocate(125)));
cfText = webSocket.sendText("hello", last);
cfClose = webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
assertAllHang(cfText, cfClose);
assertNotDone(cfPong);
webSocket.abort();
assertFails(IOE, cfPong);
assertFails(IOE, cfText);
assertFails(IOE, cfClose);
return null;
}, () -> cfPong.isDone());
}
@Override
long initialWaitSec() {
// Some Windows machines increase buffer size after 1-2 seconds
return isWindows() ? 3 : 1;
}
}

View file

@ -0,0 +1,93 @@
/*
* Copyright (c) 2018, 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.
*/
/*
* @test
* @build DummyWebSocketServer
* @run junit/othervm
* -Djdk.internal.httpclient.debug=true
* -Djdk.internal.httpclient.websocket.debug=true
* -Djdk.httpclient.sendBufferSize=8192
* PendingTextPingClose
*/
import java.net.http.WebSocket;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
public class PendingTextPingClose extends PendingOperations {
CompletableFuture<WebSocket> cfText;
CompletableFuture<WebSocket> cfPing;
CompletableFuture<WebSocket> cfClose;
@ParameterizedTest
@MethodSource("booleans")
public void pendingTextPingClose(boolean last) throws Exception {
repeatable(() -> {
server = Support.notReadingServer();
server.setReceiveBufferSize(1024);
server.open();
webSocket = httpClient().newWebSocketBuilder()
.buildAsync(server.getURI(), new WebSocket.Listener() { })
.join();
CharBuffer data = CharBuffer.allocate(65536);
for (int i = 0; ; i++) { // fill up the send buffer
long start = System.currentTimeMillis();
System.out.printf("begin cycle #%s at %s%n", i, start);
cfText = webSocket.sendText(data, last);
try {
if (!cfText.isDone()) System.gc();
cfText.get(waitSec, TimeUnit.SECONDS);
data.clear();
} catch (TimeoutException e) {
break;
} finally {
long stop = System.currentTimeMillis();
System.out.printf("end cycle #%s at %s (%s ms)%n", i, stop, stop - start);
}
}
assertFails(ISE, webSocket.sendText("", true));
assertFails(ISE, webSocket.sendText("", false));
assertFails(ISE, webSocket.sendBinary(ByteBuffer.allocate(0), true));
assertFails(ISE, webSocket.sendBinary(ByteBuffer.allocate(0), false));
cfPing = webSocket.sendPing(ByteBuffer.allocate(125));
assertFails(ISE, webSocket.sendPing(ByteBuffer.allocate(125)));
assertFails(ISE, webSocket.sendPong(ByteBuffer.allocate(125)));
cfClose = webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
assertAllHang(cfPing, cfClose);
assertNotDone(cfText);
webSocket.abort();
assertFails(IOE, cfText);
assertFails(IOE, cfPing);
assertFails(IOE, cfClose);
return null;
}, () -> cfText.isDone());
}
}

View file

@ -0,0 +1,92 @@
/*
* Copyright (c) 2018, 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.
*/
/*
* @test
* @build DummyWebSocketServer
* @run junit/othervm
* -Djdk.internal.httpclient.debug=true
* -Djdk.internal.httpclient.websocket.debug=true
* -Djdk.httpclient.sendBufferSize=8192
* PendingTextPongClose
*/
import java.net.http.WebSocket;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
public class PendingTextPongClose extends PendingOperations {
CompletableFuture<WebSocket> cfText;
CompletableFuture<WebSocket> cfPong;
CompletableFuture<WebSocket> cfClose;
@ParameterizedTest
@MethodSource("booleans")
public void pendingTextPongClose(boolean last) throws Exception {
repeatable(() -> {
server = Support.notReadingServer();
server.setReceiveBufferSize(1024);
server.open();
webSocket = httpClient().newWebSocketBuilder()
.buildAsync(server.getURI(), new WebSocket.Listener() { })
.join();
CharBuffer data = CharBuffer.allocate(65536);
for (int i = 0; ; i++) { // fill up the send buffer
long start = System.currentTimeMillis();
System.out.printf("begin cycle #%s at %s%n", i, start);
cfText = webSocket.sendText(data, last);
try {
cfText.get(waitSec, TimeUnit.SECONDS);
data.clear();
} catch (TimeoutException e) {
break;
} finally {
long stop = System.currentTimeMillis();
System.out.printf("end cycle #%s at %s (%s ms)%n", i, stop, stop - start);
}
}
assertFails(ISE, webSocket.sendText("", true));
assertFails(ISE, webSocket.sendText("", false));
assertFails(ISE, webSocket.sendBinary(ByteBuffer.allocate(0), true));
assertFails(ISE, webSocket.sendBinary(ByteBuffer.allocate(0), false));
cfPong = webSocket.sendPong(ByteBuffer.allocate(125));
assertFails(ISE, webSocket.sendPing(ByteBuffer.allocate(125)));
assertFails(ISE, webSocket.sendPong(ByteBuffer.allocate(125)));
cfClose = webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "ok");
assertAllHang(cfPong, cfClose);
assertNotDone(cfText);
webSocket.abort();
assertFails(IOE, cfText);
assertFails(IOE, cfPong);
assertFails(IOE, cfClose);
return null;
}, () -> cfText.isDone());
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2016, 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.
*/
/*
* @test
* @bug 8159053
* @modules java.net.http/jdk.internal.net.http.websocket:open
* @run junit/othervm/timeout=240
* --add-reads java.net.http=ALL-UNNAMED
* java.net.http/jdk.internal.net.http.websocket.ReaderTest
*/
public final class ReaderDriver { }

View file

@ -0,0 +1,163 @@
/*
* Copyright (c) 2020, 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.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
/**
* Helper class to create instances of DummySecureWebSocketServer which
* can support both plain and secure connections.
* The caller should invoke DummySecureWebSocketServer::secure before
* DummySecureWebSocketServer::open in order to enable secure connection.
* When secure, the DummySecureWebSocketServer currently only support using the
* default SSLEngine through the default SSLSocketServerFacrtory.
*/
public class SecureSupport {
private SecureSupport() { }
public static DummySecureWebSocketServer serverWithCannedData(int... data) {
return serverWithCannedDataAndAuthentication(null, null, data);
}
public static DummySecureWebSocketServer serverWithCannedDataAndAuthentication(
String username,
String password,
int... data)
{
byte[] copy = new byte[data.length];
for (int i = 0; i < data.length; i++) {
copy[i] = (byte) data[i];
}
return serverWithCannedDataAndAuthentication(username, password, copy);
}
public static DummySecureWebSocketServer serverWithCannedData(byte... data) {
return serverWithCannedDataAndAuthentication(null, null, data);
}
public static DummySecureWebSocketServer serverWithCannedDataAndAuthentication(
String username,
String password,
byte... data)
{
byte[] copy = Arrays.copyOf(data, data.length);
return new DummySecureWebSocketServer(username, password) {
@Override
protected void write(WebSocketChannel ch) throws IOException {
int off = 0; int n = 1; // 1 byte at a time
while (off + n < copy.length + n) {
int len = Math.min(copy.length - off, n);
ByteBuffer bytes = ByteBuffer.wrap(copy, off, len);
off += len;
ch.write(bytes);
}
super.write(ch);
}
};
}
/*
* This server does not read from the wire, allowing its client to fill up
* their send buffer. Used to test scenarios with outstanding send
* operations.
*/
public static DummySecureWebSocketServer notReadingServer() {
return new DummySecureWebSocketServer() {
@Override
protected void read(WebSocketChannel ch) throws IOException {
try {
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
throw new IOException(e);
}
}
};
}
public static DummySecureWebSocketServer writingServer(int... data) {
byte[] copy = new byte[data.length];
for (int i = 0; i < data.length; i++) {
copy[i] = (byte) data[i];
}
return new DummySecureWebSocketServer() {
@Override
protected void read(WebSocketChannel ch) throws IOException {
try {
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
throw new IOException(e);
}
}
@Override
protected void write(WebSocketChannel ch) throws IOException {
int off = 0; int n = 1; // 1 byte at a time
while (off + n < copy.length + n) {
int len = Math.min(copy.length - off, n);
ByteBuffer bytes = ByteBuffer.wrap(copy, off, len);
off += len;
ch.write(bytes);
}
super.write(ch);
}
};
}
public static String stringWith2NBytes(int n) {
// -- Russian Alphabet (33 characters, 2 bytes per char) --
char[] abc = {
0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0401, 0x0416,
0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E,
0x041F, 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426,
0x0427, 0x0428, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E,
0x042F,
};
// repeat cyclically
StringBuilder sb = new StringBuilder(n);
for (int i = 0, j = 0; i < n; i++, j = (j + 1) % abc.length) {
sb.append(abc[j]);
}
String s = sb.toString();
assert s.length() == n && s.getBytes(StandardCharsets.UTF_8).length == 2 * n;
return s;
}
public static String malformedString() {
return new String(new char[]{0xDC00, 0xD800});
}
public static String incompleteString() {
return new String(new char[]{0xD800});
}
public static String stringWithNBytes(int n) {
char[] chars = new char[n];
Arrays.fill(chars, 'A');
return new String(chars);
}
}

View file

@ -0,0 +1,100 @@
/*
* Copyright (c) 2018, 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.
*/
/*
* @test
* @build DummyWebSocketServer
* @run junit/othervm
* -Djdk.internal.httpclient.websocket.debug=true
* SendTest
*/
import java.io.IOException;
import java.net.http.WebSocket;
import static java.net.http.HttpClient.Builder.NO_PROXY;
import static java.net.http.HttpClient.newBuilder;
import static java.net.http.WebSocket.NORMAL_CLOSURE;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class SendTest {
private static final Class<NullPointerException> NPE = NullPointerException.class;
@Test
public void sendMethodsThrowNPE() throws IOException {
try (var server = new DummyWebSocketServer()) {
server.open();
var webSocket = newBuilder().proxy(NO_PROXY).build().newWebSocketBuilder()
.buildAsync(server.getURI(), new WebSocket.Listener() { })
.join();
try {
assertThrows(NPE, () -> webSocket.sendText(null, false));
assertThrows(NPE, () -> webSocket.sendText(null, true));
assertThrows(NPE, () -> webSocket.sendBinary(null, false));
assertThrows(NPE, () -> webSocket.sendBinary(null, true));
assertThrows(NPE, () -> webSocket.sendPing(null));
assertThrows(NPE, () -> webSocket.sendPong(null));
assertThrows(NPE, () -> webSocket.sendClose(NORMAL_CLOSURE, null));
webSocket.abort();
assertThrows(NPE, () -> webSocket.sendText(null, false));
assertThrows(NPE, () -> webSocket.sendText(null, true));
assertThrows(NPE, () -> webSocket.sendBinary(null, false));
assertThrows(NPE, () -> webSocket.sendBinary(null, true));
assertThrows(NPE, () -> webSocket.sendPing(null));
assertThrows(NPE, () -> webSocket.sendPong(null));
assertThrows(NPE, () -> webSocket.sendClose(NORMAL_CLOSURE, null));
} finally {
webSocket.abort();
}
}
}
// TODO: request in onClose/onError
// TODO: throw exception in onClose/onError
// TODO: exception is thrown from request()
@Test
public void sendCloseCompleted() throws IOException {
try (var server = new DummyWebSocketServer()) {
server.open();
var webSocket = newBuilder().proxy(NO_PROXY).build().newWebSocketBuilder()
.buildAsync(server.getURI(), new WebSocket.Listener() { })
.join();
try {
webSocket.sendClose(NORMAL_CLOSURE, "").join();
assertTrue(webSocket.isOutputClosed());
assertEquals("", webSocket.getSubprotocol());
webSocket.request(1); // No exceptions must be thrown
} finally {
webSocket.abort();
}
}
}
}

View file

@ -0,0 +1,266 @@
/*
* Copyright (c) 2018, 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.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertFalse;
public class Support {
private Support() { }
public static void assertFails(Class<? extends Throwable> clazz,
CompletionStage<?> stage) {
Support.assertCompletesExceptionally(clazz, stage);
}
public static void assertNotDone(CompletableFuture<?> future) {
assertFalse(future.isDone());
}
public static void assertCompletesExceptionally(Class<? extends Throwable> clazz,
CompletionStage<?> stage) {
CompletableFuture<?> cf =
CompletableFuture.completedFuture(null).thenCompose(x -> stage);
assertThrows(clazz, () -> {
try {
cf.join();
} catch (CompletionException e) {
throw e.getCause();
}
});
}
public static void assertHangs(CompletionStage<?> stage) {
Support.assertDoesNotCompleteWithin(5, TimeUnit.SECONDS, stage);
}
public static void assertDoesNotCompleteWithin(long timeout,
TimeUnit unit,
CompletionStage<?> stage) {
CompletableFuture<?> cf =
CompletableFuture.completedFuture(null).thenCompose(x -> stage);
assertThrows(TimeoutException.class, () -> cf.get(timeout, unit));
}
public static ByteBuffer fullCopy(ByteBuffer src) {
ByteBuffer copy = ByteBuffer.allocate(src.capacity());
int p = src.position();
int l = src.limit();
src.clear();
copy.put(src).position(p).limit(l);
src.position(p).limit(l);
return copy;
}
public static DummyWebSocketServer serverWithCannedData(int... data) {
return serverWithCannedDataAndAuthentication(null, null, data);
}
public static DummyWebSocketServer serverWithCannedDataAndAuthentication(
String username,
String password,
int... data)
{
byte[] copy = new byte[data.length];
for (int i = 0; i < data.length; i++) {
copy[i] = (byte) data[i];
}
return serverWithCannedDataAndAuthentication(username, password, copy);
}
public static DummyWebSocketServer serverWithCannedData(byte... data) {
return serverWithCannedDataAndAuthentication(null, null, data);
}
public static DummyWebSocketServer serverWithCannedDataAndAuthentication(
String username,
String password,
byte... data)
{
byte[] copy = Arrays.copyOf(data, data.length);
return new DummyWebSocketServer(username, password) {
@Override
protected void write(SocketChannel ch) throws IOException {
int off = 0; int n = 1; // 1 byte at a time
while (off + n < copy.length + n) {
// try {
// TimeUnit.MICROSECONDS.sleep(500);
// } catch (InterruptedException e) {
// return;
// }
int len = Math.min(copy.length - off, n);
ByteBuffer bytes = ByteBuffer.wrap(copy, off, len);
off += len;
ch.write(bytes);
}
super.write(ch);
}
};
}
/*
* This server does not read from the wire, allowing its client to fill up
* their send buffer. Used to test scenarios with outstanding send
* operations.
*/
public static DummyWebSocketServer notReadingServer() {
return new DummyWebSocketServer() {
volatile Thread reader;
@Override
protected void read(SocketChannel ch) throws IOException {
reader = Thread.currentThread();
try {
System.out.println("Not reading server waiting");
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
throw new IOException(e);
}
}
@Override
protected void closeChannel(SocketChannel channel) {
try {
long read = drain(channel);
System.out.printf("Not reading server drained %s bytes%n", read);
} catch (IOException io) {
System.out.println("Not reading server failed to drain channel: " + io);
}
super.closeChannel(channel);
}
@Override
public void close() {
super.close();
Thread thread = reader;
if (thread != null && thread.isAlive() && thread != Thread.currentThread()) {
try {
thread.join();
System.out.println("Not reading server: closed");
} catch (InterruptedException x) {
System.out.println("Not reading server: close interrupted: " + x);
}
}
}
};
}
static long drain(SocketChannel channel) throws IOException {
System.out.println("Not reading server: draining socket");
var blocking = channel.isBlocking();
if (blocking) channel.configureBlocking(false);
long count = 0;
try {
ByteBuffer buffer = ByteBuffer.allocateDirect(8 * 1024);
int read;
while ((read = channel.read(buffer)) > 0) {
count += read;
buffer.clear();
}
return count;
} finally {
if (blocking != channel.isBlocking()) {
channel.configureBlocking(blocking);
}
}
}
public static DummyWebSocketServer writingServer(int... data) {
byte[] copy = new byte[data.length];
for (int i = 0; i < data.length; i++) {
copy[i] = (byte) data[i];
}
return new DummyWebSocketServer() {
@Override
protected void read(SocketChannel ch) throws IOException {
try {
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
throw new IOException(e);
}
}
@Override
protected void write(SocketChannel ch) throws IOException {
int off = 0; int n = 1; // 1 byte at a time
while (off + n < copy.length + n) {
// try {
// TimeUnit.MICROSECONDS.sleep(500);
// } catch (InterruptedException e) {
// return;
// }
int len = Math.min(copy.length - off, n);
ByteBuffer bytes = ByteBuffer.wrap(copy, off, len);
off += len;
ch.write(bytes);
}
super.write(ch);
}
};
}
public static String stringWith2NBytes(int n) {
// -- Russian Alphabet (33 characters, 2 bytes per char) --
char[] abc = {
0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0401, 0x0416,
0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E,
0x041F, 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426,
0x0427, 0x0428, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E,
0x042F,
};
// repeat cyclically
StringBuilder sb = new StringBuilder(n);
for (int i = 0, j = 0; i < n; i++, j = (j + 1) % abc.length) {
sb.append(abc[j]);
}
String s = sb.toString();
assert s.length() == n && s.getBytes(StandardCharsets.UTF_8).length == 2 * n;
return s;
}
public static String malformedString() {
return new String(new char[]{0xDC00, 0xD800});
}
public static String incompleteString() {
return new String(new char[]{0xD800});
}
public static String stringWithNBytes(int n) {
char[] chars = new char[n];
Arrays.fill(chars, 'A');
return new String(chars);
}
}

View file

@ -0,0 +1,5 @@
modules=java.net.http/jdk.internal.net.http.common \
java.net.http/jdk.internal.net.http.frame \
java.net.http/jdk.internal.net.http.hpack \
java.net.http/jdk.internal.net.http.websocket
maxOutputSize = 2500000

View file

@ -0,0 +1,211 @@
/*
* Copyright (c) 2017, 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.
*/
/*
* @test
* @bug 8240666
* @summary Basic test for WebSocketHandshakeException
* @library /test/lib /test/jdk/java/net/httpclient/lib
* @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.TestServerConfigurator
* @modules java.net.http/jdk.internal.net.http.common
* jdk.httpserver
* @run junit/othervm -Djdk.internal.httpclient.debug=true WSHandshakeExceptionTest
*/
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpsServer;
import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.http.HttpClient;
import java.net.http.WebSocket;
import java.net.http.WebSocketHandshakeException;
import jdk.httpclient.test.lib.common.TestServerConfigurator;
import jdk.test.lib.net.SimpleSSLContext;
import javax.net.ssl.SSLContext;
import java.net.InetSocketAddress;
import java.net.URI;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static java.net.http.HttpClient.Builder.NO_PROXY;
import static java.lang.System.out;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
public class WSHandshakeExceptionTest {
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
private static HttpServer httpTestServer; // HTTP/1.1 [ 2 servers ]
private static HttpsServer httpsTestServer; // HTTPS/1.1
private static String httpURI;
private static String httpsURI;
private static String httpNonUtf8URI;
private static String httpsNonUtf8URI;
private static HttpClient sharedClient;
static final int ITERATION_COUNT = 4;
// a shared executor helps reduce the amount of threads created by the test
static final ExecutorService executor = Executors.newCachedThreadPool();
public static Object[][] variants() {
return new Object[][]{
{ httpURI, false },
{ httpsURI, false },
{ httpURI, true },
{ httpsURI, true },
{ httpNonUtf8URI, true },
{ httpsNonUtf8URI, true },
{ httpNonUtf8URI, false },
{ httpsNonUtf8URI, false }
};
}
HttpClient newHttpClient() {
return HttpClient.newBuilder()
.proxy(NO_PROXY)
.executor(executor)
.sslContext(sslContext)
.build();
}
@ParameterizedTest
@MethodSource("variants")
public void test(String uri, boolean sameClient) {
HttpClient client = sharedClient;
boolean pause;
for (int i = 0; i < ITERATION_COUNT; i++) {
System.out.printf("iteration %s%n", i);
if (!sameClient || client == null) {
pause = client != null;
client = newHttpClient();
if (pause) gc(10); // give some time to gc
}
if (sharedClient == null) sharedClient = client;
try {
client.newWebSocketBuilder()
.buildAsync(URI.create(uri), new WebSocket.Listener() { })
.join();
fail("Expected to throw");
} catch (CompletionException ce) {
Throwable t = getCompletionCause(ce);
if (!(t instanceof WebSocketHandshakeException)) {
throw new AssertionError("Unexpected exception", t);
}
WebSocketHandshakeException wse = (WebSocketHandshakeException) t;
assertNotNull(wse.getResponse());
assertNotNull(wse.getResponse().body());
assertEquals(String.class, wse.getResponse().body().getClass());
String body = (String)wse.getResponse().body();
out.println("Status code is " + wse.getResponse().statusCode());
out.println("Response is " + body);
if(uri.contains("/nonutf8body")) {
// the invalid sequence 0xFF should have been replaced
// by the replacement character (U+FFFD)
assertTrue(body.equals("\ufffd"));
}
else {
// default HttpServer 404 body expected
assertTrue(body.contains("404"));
}
assertEquals(404, wse.getResponse().statusCode());
}
}
}
static void gc(long ms) {
System.gc();
try {
Thread.sleep(ms);
} catch (InterruptedException x) {
// OK
}
}
@BeforeAll
public static void setup() throws Exception {
// HTTP/1.1
InetSocketAddress sa = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
httpTestServer = HttpServer.create(sa, 0);
httpURI = "ws://localhost:" + httpTestServer.getAddress().getPort() + "/";
httpNonUtf8URI = "ws://localhost:" + httpTestServer.getAddress().getPort() + "/nonutf8body";
httpTestServer.createContext("/nonutf8body", new BodyHandler());
httpsTestServer = HttpsServer.create(sa, 0);
httpsTestServer.setHttpsConfigurator(new TestServerConfigurator(sa.getAddress(), sslContext));
httpsURI = "wss://localhost:" + httpsTestServer.getAddress().getPort() + "/";
httpsNonUtf8URI = "wss://localhost:" + httpsTestServer.getAddress().getPort() + "/nonutf8body";
httpsTestServer.createContext("/nonutf8body", new BodyHandler());
httpTestServer.start();
httpsTestServer.start();
}
@AfterAll
public static void teardown() {
sharedClient = null;
gc(100);
httpTestServer.stop(0);
httpsTestServer.stop(0);
executor.shutdownNow();
}
private static Throwable getCompletionCause(Throwable x) {
if (!(x instanceof CompletionException)
&& !(x instanceof ExecutionException)) return x;
final Throwable cause = x.getCause();
if (cause == null) {
throw new InternalError("Unexpected null cause", x);
}
return cause;
}
static class BodyHandler implements HttpHandler {
@Override
public void handle(HttpExchange e) throws IOException {
try(InputStream is = e.getRequestBody();
OutputStream os = e.getResponseBody()) {
byte[] invalidUtf8 = {(byte)0xFF}; //Invalid utf-8 byte
e.sendResponseHeaders(404, invalidUtf8.length);
os.write(invalidUtf8);
}
}
}
}

View file

@ -0,0 +1,236 @@
/*
* Copyright (c) 2016, 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.
*/
/*
* @test
* @bug 8159053
* @build DummyWebSocketServer
* Support
* @run junit/othervm WebSocketBuilderTest
*/
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.WebSocket;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CompletionStage;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static org.junit.jupiter.api.Assertions.assertThrows;
/*
* In some places in this test a new String is created out of a string literal.
* The idea is to make sure the code under test relies on something better than
* the reference equality ( == ) for string equality checks.
*/
public final class WebSocketBuilderTest {
private final static URI VALID_URI = URI.create("ws://websocket.example.com");
@Test
public void nullArguments() {
HttpClient c = HttpClient.newHttpClient();
assertThrows(NullPointerException.class,
() -> c.newWebSocketBuilder()
.buildAsync(null, listener()));
assertThrows(NullPointerException.class,
() -> c.newWebSocketBuilder()
.buildAsync(VALID_URI, null));
assertThrows(NullPointerException.class,
() -> c.newWebSocketBuilder()
.buildAsync(null, null));
assertThrows(NullPointerException.class,
() -> c.newWebSocketBuilder()
.header(null, "value"));
assertThrows(NullPointerException.class,
() -> c.newWebSocketBuilder()
.header("name", null));
assertThrows(NullPointerException.class,
() -> c.newWebSocketBuilder()
.header(null, null));
assertThrows(NullPointerException.class,
() -> c.newWebSocketBuilder()
.subprotocols(null));
assertThrows(NullPointerException.class,
() -> c.newWebSocketBuilder()
.subprotocols(null, "sub2.example.com"));
assertThrows(NullPointerException.class,
() -> c.newWebSocketBuilder()
.subprotocols("sub1.example.com", (String) null));
assertThrows(NullPointerException.class,
() -> c.newWebSocketBuilder()
.subprotocols("sub1.example.com", (String[]) null));
assertThrows(NullPointerException.class,
() -> c.newWebSocketBuilder()
.subprotocols("sub1.example.com", "sub2.example.com", null));
assertThrows(NullPointerException.class,
() -> c.newWebSocketBuilder()
.subprotocols("sub1.example.com", null, "sub3.example.com"));
assertThrows(NullPointerException.class,
() -> c.newWebSocketBuilder()
.connectTimeout(null));
}
@ParameterizedTest
@MethodSource("badURIs")
void illegalURI(URI uri) {
WebSocket.Builder b = HttpClient.newHttpClient().newWebSocketBuilder();
assertFails(IllegalArgumentException.class,
b.buildAsync(uri, listener()));
}
@Test
public void illegalHeaders() {
List<String> headers =
List.of("Sec-WebSocket-Accept",
"Sec-WebSocket-Extensions",
"Sec-WebSocket-Key",
"Sec-WebSocket-Protocol",
"Sec-WebSocket-Version")
.stream()
.flatMap(s -> Stream.of(s, new String(s))) // a string and a copy of it
.collect(Collectors.toList());
Function<String, CompletionStage<?>> f =
header -> HttpClient.newHttpClient()
.newWebSocketBuilder()
.header(header, "value")
.buildAsync(VALID_URI, listener());
headers.forEach(h -> assertFails(IllegalArgumentException.class, f.apply(h)));
}
// TODO: test for bad syntax headers
// TODO: test for overwrites (subprotocols) and additions (headers)
@ParameterizedTest
@MethodSource("badSubprotocols")
public void illegalSubprotocolsSyntax(String s) {
WebSocket.Builder b = HttpClient.newHttpClient()
.newWebSocketBuilder()
.subprotocols(s);
assertFails(IllegalArgumentException.class,
b.buildAsync(VALID_URI, listener()));
}
@ParameterizedTest
@MethodSource("duplicatingSubprotocols")
public void illegalSubprotocolsDuplicates(String mostPreferred,
String[] lesserPreferred) {
WebSocket.Builder b = HttpClient.newHttpClient()
.newWebSocketBuilder()
.subprotocols(mostPreferred, lesserPreferred);
assertFails(IllegalArgumentException.class,
b.buildAsync(VALID_URI, listener()));
}
@ParameterizedTest
@MethodSource("badConnectTimeouts")
public void illegalConnectTimeout(Duration d) {
WebSocket.Builder b = HttpClient.newHttpClient()
.newWebSocketBuilder()
.connectTimeout(d);
assertFails(IllegalArgumentException.class,
b.buildAsync(VALID_URI, listener()));
}
public static Object[][] badURIs() {
return new Object[][]{
{URI.create("http://example.com")},
{URI.create("ftp://example.com")},
{URI.create("wss://websocket.example.com/hello#fragment")},
{URI.create("ws://websocket.example.com/hello#fragment")},
};
}
public static Object[][] badConnectTimeouts() {
return new Object[][]{
{Duration.ofDays(0)},
{Duration.ofDays(-1)},
{Duration.ofHours(0)},
{Duration.ofHours(-1)},
{Duration.ofMinutes(0)},
{Duration.ofMinutes(-1)},
{Duration.ofSeconds(0)},
{Duration.ofSeconds(-1)},
{Duration.ofMillis(0)},
{Duration.ofMillis(-1)},
{Duration.ofNanos(0)},
{Duration.ofNanos(-1)},
{Duration.ZERO},
};
}
// https://tools.ietf.org/html/rfc7230#section-3.2.6
// https://tools.ietf.org/html/rfc20
public static Object[][] badSubprotocols() {
return new Object[][]{
{""},
{new String("")},
{"round-brackets("},
{"round-brackets)"},
{"comma,"},
{"slash/"},
{"colon:"},
{"semicolon;"},
{"angle-brackets<"},
{"angle-brackets>"},
{"equals="},
{"question-mark?"},
{"at@"},
{"brackets["},
{"backslash\\"},
{"brackets]"},
{"curly-brackets{"},
{"curly-brackets}"},
{"space "},
{"non-printable-character " + Character.toString((char) 31)},
{"non-printable-character " + Character.toString((char) 127)},
};
}
public static Object[][] duplicatingSubprotocols() {
return new Object[][]{
{"a.b.c", new String[]{"a.b.c"}},
{"a.b.c", new String[]{"x.y.z", "p.q.r", "x.y.z"}},
{"a.b.c", new String[]{new String("a.b.c")}},
};
}
private static WebSocket.Listener listener() {
return new WebSocket.Listener() { };
}
/* shortcut */
public static void assertFails(Class<? extends Throwable> clazz,
CompletionStage<?> stage) {
Support.assertCompletesExceptionally(clazz, stage);
}
}

View file

@ -0,0 +1,178 @@
/*
* 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 8351339
* @summary Verify the intact transmission of the binary payload regardless of its endianness
* @modules java.net.http/jdk.internal.net.http.websocket
* @library /test/lib
* @build DummyWebSocketServer
* jdk.test.lib.Asserts
* @run main WebSocketEndiannessTest
*/
import jdk.internal.net.http.websocket.Frame;
import java.net.http.HttpClient;
import java.net.http.WebSocket;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.List;
import java.util.function.Supplier;
import static java.net.http.HttpClient.Builder.NO_PROXY;
import static java.nio.ByteOrder.BIG_ENDIAN;
import static java.nio.ByteOrder.LITTLE_ENDIAN;
import static jdk.test.lib.Asserts.assertEquals;
import static jdk.test.lib.Asserts.assertEqualsByteArray;
import static jdk.test.lib.Asserts.assertTrue;
public class WebSocketEndiannessTest {
public static void main(String[] args) throws Exception {
assertEndiannessAgnosticTransfer();
assertSuccessfulMasking();
}
private static void assertEndiannessAgnosticTransfer() throws Exception {
Supplier<ByteBuffer> bufferSupplier = () -> ByteBuffer
.wrap(new byte[]{
// 4-byte words
0x00, 0x01, 0x02, 0x03,
0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b,
0x0c, 0x0d, 0x0e, 0x0f,
// 2-byte words
0x10, 0x11,
0x12, 0x13,
0x14, 0x15,
0x16, 0x17,
0x18, 0x19,
0x1a, 0x1b,
0x1c, 0x1d,
0x1e, 0x1f,
// negative ones
-1, -2, -3, 4,
-5, -6
});
assertEndiannessAgnosticTransfer(bufferSupplier.get().order(LITTLE_ENDIAN));
assertEndiannessAgnosticTransfer(bufferSupplier.get().order(BIG_ENDIAN));
}
private static void assertEndiannessAgnosticTransfer(ByteBuffer data) throws Exception {
List<DummyWebSocketServer.DecodedFrame> frames = sendDataAndReadFrames(data);
assertEquals(frames.size(), 1);
DummyWebSocketServer.DecodedFrame frame = frames.getFirst();
assertEquals(frame.opcode(), Frame.Opcode.BINARY);
assertTrue(frame.last());
assertEqualsByteArray(data.array(), frame.data().array());
}
private static List<DummyWebSocketServer.DecodedFrame> sendDataAndReadFrames(ByteBuffer data) throws Exception {
try (var server = new DummyWebSocketServer();
var client = HttpClient.newBuilder().proxy(NO_PROXY).build()) {
server.open();
WebSocket webSocket = client
.newWebSocketBuilder()
.buildAsync(server.getURI(), new WebSocket.Listener() {})
.join();
try {
webSocket.sendBinary(data, true).join();
} finally {
webSocket.abort();
}
return server.readFrames();
}
}
private static void assertSuccessfulMasking() {
assertSuccessfulMasking(LITTLE_ENDIAN, LITTLE_ENDIAN);
assertSuccessfulMasking(LITTLE_ENDIAN, BIG_ENDIAN);
assertSuccessfulMasking(BIG_ENDIAN, LITTLE_ENDIAN);
assertSuccessfulMasking(BIG_ENDIAN, BIG_ENDIAN);
}
private static void assertSuccessfulMasking(ByteOrder srcOrder, ByteOrder dstOrder) {
// Create the masker
Frame.Masker masker = new Frame.Masker()
// `0xB0` and `0xD0` is used instead of `0x0B` and `0x0D` to cover the negative `byte` range:
//
// (byte) 0x0A -> 10
// (byte) 0xB0 -> -80
// (byte) 0x0C -> 12
// (byte) 0xD0 -> -48
.setMask(0x0AB00CD0);
// Perform dummy masking to advance `Frame::offset` 1 byte, and effectively make it non-zero.
// A non-zero `Frame::offset` will trigger `Frame::initVectorMask` invocation.
masker.applyMask(ByteBuffer.wrap(new byte[1]), ByteBuffer.wrap(new byte[1]));
// Perform the actual masking
ByteBuffer src = ByteBuffer
.wrap(new byte[]{
// `initVectorMask` will mask 3 bytes to position the `offset` back to 0.
// It is 3 bytes, because of the 1 byte dummy advancement above.
-0x1, -0x2, 0x3,
// `applyVectorMask` will make a single 8-byte pass
0x1, 0x2, 0x3, 0x4, -0x5, 0x6, -0x7, -0x8,
// `applyPlainMask` will mask 3 bytes
0x1, -0x2, -0x3
// Note minus signs sprinkled above to cover the negative `byte` range in a certain structure:
// Some will get masked with a positive number, some with a negative.
// For instance, for `applyPlainMask`:
// - `0x1` will be masked with `0xA`
// - `-0x2` will be masked with `0xB0`
})
.order(srcOrder);
ByteBuffer dst = ByteBuffer.allocate(src.capacity()).order(dstOrder);
masker.applyMask(src, dst);
// Verify the masking
assertEqualsByteArray(
new byte[]{
// 3 bytes for `initVectorMask`.
// Remember 0xA is consumed by the initial dummy masking.
(byte) (-0x1 ^ 0xB0),
-0x2 ^ 0xC,
(byte) (0x3 ^ 0xD0),
// 8 bytes for `applyVectorMask`
0x1 ^ 0xA,
(byte) (0x2 ^ 0xB0),
0x3 ^ 0xC,
(byte) (0x4 ^ 0xD0),
-0x5 ^ 0xA,
(byte) (0x6 ^ 0xB0),
-0x7 ^ 0xC,
(byte) (-0x8 ^ 0xD0),
// 3 bytes for `applyPlainMask`
0x1 ^ 0xA,
(byte) (-0x2 ^ 0xB0),
-0x3 ^ 0xC
},
dst.array());
}
}

View file

@ -0,0 +1,242 @@
/*
* Copyright (c) 2018, 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.
*/
/*
* @test
* @bug 8159053
* @run junit/othervm
* -Djdk.internal.httpclient.websocket.debug=true
* -Djdk.internal.httpclient.debug=true
* -Djdk.httpclient.websocket.writeBufferSize=1024
* -Djdk.httpclient.websocket.intermediateBufferSize=2048 WebSocketExtendedTest
*/
import jdk.internal.net.http.websocket.Frame;
import java.io.IOException;
import java.net.http.WebSocket;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Random;
import static java.net.http.HttpClient.Builder.NO_PROXY;
import static java.net.http.HttpClient.newBuilder;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*
* This battery of tests exercises sending data (Text/Binary) messages with
* possible fragmentation.
*/
public class WebSocketExtendedTest {
// * run junit/othervm
// * -Djdk.httpclient.websocket.writeBufferSize=16
// * -Djdk.httpclient.sendBufferSize=32 WebSocketTextTest
private final static Random random;
static {
long seed = System.currentTimeMillis();
System.out.println("seed=" + seed);
random = new Random(seed);
}
// FIXME ensure subsequent (sendText/Binary, false) only CONTINUATIONs
@ParameterizedTest
@MethodSource("binary")
public void binary(ByteBuffer expected) throws IOException, InterruptedException {
try (DummyWebSocketServer server = new DummyWebSocketServer()) {
server.open();
WebSocket ws = newBuilder().proxy(NO_PROXY).build()
.newWebSocketBuilder()
.buildAsync(server.getURI(), new WebSocket.Listener() { })
.join();
ws.sendBinary(expected.duplicate(), true).join();
ws.abort();
List<DummyWebSocketServer.DecodedFrame> frames = server.readFrames();
assertEquals(1, frames.size());
DummyWebSocketServer.DecodedFrame f = frames.get(0);
assertTrue(f.last());
assertEquals(Frame.Opcode.BINARY, f.opcode());
assertEquals(expected, f.data());
}
}
@ParameterizedTest
@MethodSource("pingPongSizes")
public void ping(ByteBuffer expected) throws Exception {
try (DummyWebSocketServer server = new DummyWebSocketServer()) {
server.open();
WebSocket ws = newBuilder().proxy(NO_PROXY).build()
.newWebSocketBuilder()
.buildAsync(server.getURI(), new WebSocket.Listener() { })
.join();
ws.sendPing(expected.duplicate()).join();
ws.abort();
List<DummyWebSocketServer.DecodedFrame> frames = server.readFrames();
assertEquals(1, frames.size());
DummyWebSocketServer.DecodedFrame f = frames.get(0);
assertEquals(Frame.Opcode.PING, f.opcode());
ByteBuffer actual = ByteBuffer.allocate(expected.remaining());
actual.put(f.data());
actual.flip();
assertEquals(expected, actual);
}
}
@ParameterizedTest
@MethodSource("pingPongSizes")
public void pong(ByteBuffer expected) throws Exception {
try (DummyWebSocketServer server = new DummyWebSocketServer()) {
server.open();
WebSocket ws = newBuilder().proxy(NO_PROXY).build()
.newWebSocketBuilder()
.buildAsync(server.getURI(), new WebSocket.Listener() { })
.join();
ws.sendPong(expected.duplicate()).join();
ws.abort();
List<DummyWebSocketServer.DecodedFrame> frames = server.readFrames();
assertEquals(1, frames.size());
DummyWebSocketServer.DecodedFrame f = frames.get(0);
assertEquals(Frame.Opcode.PONG, f.opcode());
ByteBuffer actual = ByteBuffer.allocate(expected.remaining());
actual.put(f.data());
actual.flip();
assertEquals(expected, actual);
}
}
@ParameterizedTest
@MethodSource("closeArguments")
public void close(int statusCode, String reason) throws Exception {
try (DummyWebSocketServer server = new DummyWebSocketServer()) {
server.open();
WebSocket ws = newBuilder().proxy(NO_PROXY).build()
.newWebSocketBuilder()
.buildAsync(server.getURI(), new WebSocket.Listener() { })
.join();
ws.sendClose(statusCode, reason).join();
ws.abort();
List<DummyWebSocketServer.DecodedFrame> frames = server.readFrames();
assertEquals(1, frames.size());
DummyWebSocketServer.DecodedFrame f = frames.get(0);
assertEquals(Frame.Opcode.CLOSE, f.opcode());
ByteBuffer actual = ByteBuffer.allocate(Frame.MAX_CONTROL_FRAME_PAYLOAD_LENGTH);
actual.put(f.data());
actual.flip();
assertEquals(statusCode, actual.getChar());
assertEquals(reason, StandardCharsets.UTF_8.decode(actual).toString());
}
}
@ParameterizedTest
@MethodSource("texts")
public void text(String expected) throws Exception {
try (DummyWebSocketServer server = new DummyWebSocketServer()) {
server.open();
WebSocket ws = newBuilder().proxy(NO_PROXY).build()
.newWebSocketBuilder()
.buildAsync(server.getURI(), new WebSocket.Listener() { })
.join();
ws.sendText(expected, true).join();
ws.abort();
List<DummyWebSocketServer.DecodedFrame> frames = server.readFrames();
int maxBytes = (int) StandardCharsets.UTF_8.newEncoder().maxBytesPerChar() * expected.length();
ByteBuffer actual = ByteBuffer.allocate(maxBytes);
frames.stream().forEachOrdered(f -> actual.put(f.data()));
actual.flip();
assertEquals(expected, StandardCharsets.UTF_8.decode(actual).toString());
}
}
public static Object[][] pingPongSizes() {
return new Object[][]{
{bytes( 0)},
{bytes( 1)},
{bytes( 63)},
{bytes(125)},
};
}
public static Object[][] closeArguments() {
return new Object[][]{
{WebSocket.NORMAL_CLOSURE, utf8String( 0)},
{WebSocket.NORMAL_CLOSURE, utf8String( 1)},
// 123 / 3 = max reason bytes / max bytes per char
{WebSocket.NORMAL_CLOSURE, utf8String(41)},
};
}
private static String utf8String(int n) {
char[] abc = {
// -- English Alphabet (26 characters, 1 byte per char) --
0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047, 0x0048,
0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F, 0x0050,
0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057, 0x0058,
0x0059, 0x005A,
// -- Russian Alphabet (33 characters, 2 bytes per char) --
0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0401, 0x0416,
0x0417, 0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E,
0x041F, 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426,
0x0427, 0x0428, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E,
0x042F,
// -- Hiragana base characters (46 characters, 3 bytes per char) --
0x3042, 0x3044, 0x3046, 0x3048, 0x304A, 0x304B, 0x304D, 0x304F,
0x3051, 0x3053, 0x3055, 0x3057, 0x3059, 0x305B, 0x305D, 0x305F,
0x3061, 0x3064, 0x3066, 0x3068, 0x306A, 0x306B, 0x306C, 0x306D,
0x306E, 0x306F, 0x3072, 0x3075, 0x3078, 0x307B, 0x307E, 0x307F,
0x3080, 0x3081, 0x3082, 0x3084, 0x3086, 0x3088, 0x3089, 0x308A,
0x308B, 0x308C, 0x308D, 0x308F, 0x3092, 0x3093,
};
assert new String(abc).getBytes(StandardCharsets.UTF_8).length > abc.length;
StringBuilder str = new StringBuilder(n);
random.ints(0, abc.length).limit(n).forEach(i -> str.append(abc[i]));
return str.toString();
}
public static Object[][] texts() {
return new Object[][]{
{utf8String( 0)},
{utf8String(1024)},
};
}
public static Object[][] binary() {
return new Object[][]{
{bytes( 0)},
{bytes(1024)},
};
}
private static ByteBuffer bytes(int n) {
byte[] array = new byte[n];
random.nextBytes(array);
return ByteBuffer.wrap(array);
}
}

View file

@ -0,0 +1,409 @@
/*
* Copyright (c) 2019, 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.
*/
/*
* @test
* @bug 8217429 8236859
* @summary WebSocket proxy tunneling tests
* @library /test/lib
* @compile SecureSupport.java DummySecureWebSocketServer.java ../ProxyServer.java
* @build jdk.test.lib.net.SimpleSSLContext WebSocketProxyTest
* @run junit/othervm
* -Djdk.internal.httpclient.debug=true
* -Djdk.internal.httpclient.websocket.debug=true
* -Djdk.httpclient.HttpClient.log=errors,requests,headers
* -Djdk.http.auth.tunneling.disabledSchemes=
* WebSocketProxyTest
*/
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.Authenticator;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.ProxySelector;
import java.net.http.HttpResponse;
import java.net.http.WebSocket;
import java.net.http.WebSocketHandshakeException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.HexFormat;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import jdk.test.lib.net.SimpleSSLContext;
import javax.net.ssl.SSLContext;
import static java.net.http.HttpClient.newBuilder;
import static java.nio.charset.StandardCharsets.UTF_8;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
public class WebSocketProxyTest {
// Used to verify a proxy/websocket server requiring Authentication
private static final String USERNAME = "wally";
private static final String PASSWORD = "xyz987";
static {
SSLContext.setDefault(SimpleSSLContext.findSSLContext());
}
static class WSAuthenticator extends Authenticator {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(USERNAME, PASSWORD.toCharArray());
}
}
static final Function<int[],DummySecureWebSocketServer> SERVER_WITH_CANNED_DATA =
new Function<>() {
@Override public DummySecureWebSocketServer apply(int[] data) {
return SecureSupport.serverWithCannedData(data); }
@Override public String toString() { return "SERVER_WITH_CANNED_DATA"; }
};
static final Function<int[],DummySecureWebSocketServer> SSL_SERVER_WITH_CANNED_DATA =
new Function<>() {
@Override public DummySecureWebSocketServer apply(int[] data) {
return SecureSupport.serverWithCannedData(data).secure(); }
@Override public String toString() { return "SSL_SERVER_WITH_CANNED_DATA"; }
};
static final Function<int[],DummySecureWebSocketServer> AUTH_SERVER_WITH_CANNED_DATA =
new Function<>() {
@Override public DummySecureWebSocketServer apply(int[] data) {
return SecureSupport.serverWithCannedDataAndAuthentication(USERNAME, PASSWORD, data); }
@Override public String toString() { return "AUTH_SERVER_WITH_CANNED_DATA"; }
};
static final Function<int[],DummySecureWebSocketServer> AUTH_SSL_SVR_WITH_CANNED_DATA =
new Function<>() {
@Override public DummySecureWebSocketServer apply(int[] data) {
return SecureSupport.serverWithCannedDataAndAuthentication(USERNAME, PASSWORD, data).secure(); }
@Override public String toString() { return "AUTH_SSL_SVR_WITH_CANNED_DATA"; }
};
static final Supplier<ProxyServer> TUNNELING_PROXY_SERVER =
new Supplier<>() {
@Override public ProxyServer get() {
try { return new ProxyServer(0, true);}
catch(IOException e) { throw new UncheckedIOException(e); } }
@Override public String toString() { return "TUNNELING_PROXY_SERVER"; }
};
static final Supplier<ProxyServer> AUTH_TUNNELING_PROXY_SERVER =
new Supplier<>() {
@Override public ProxyServer get() {
try { return new ProxyServer(0, true, USERNAME, PASSWORD);}
catch(IOException e) { throw new UncheckedIOException(e); } }
@Override public String toString() { return "AUTH_TUNNELING_PROXY_SERVER"; }
};
public static Object[][] servers() {
return new Object[][] {
{ SERVER_WITH_CANNED_DATA, TUNNELING_PROXY_SERVER },
{ SERVER_WITH_CANNED_DATA, AUTH_TUNNELING_PROXY_SERVER },
{ SSL_SERVER_WITH_CANNED_DATA, TUNNELING_PROXY_SERVER },
{ SSL_SERVER_WITH_CANNED_DATA, AUTH_TUNNELING_PROXY_SERVER },
{ AUTH_SERVER_WITH_CANNED_DATA, TUNNELING_PROXY_SERVER },
{ AUTH_SSL_SVR_WITH_CANNED_DATA, TUNNELING_PROXY_SERVER },
{ AUTH_SERVER_WITH_CANNED_DATA, AUTH_TUNNELING_PROXY_SERVER },
{ AUTH_SSL_SVR_WITH_CANNED_DATA, AUTH_TUNNELING_PROXY_SERVER },
};
}
record bytes(byte[] bytes) {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o instanceof bytes other) {
return Arrays.equals(bytes(), other.bytes());
}
return false;
}
@Override
public int hashCode() { return Arrays.hashCode(bytes()); }
public String toString() {
return "0x" + HexFormat.of()
.withUpperCase()
.formatHex(bytes());
}
}
static List<bytes> ofBytes(List<byte[]> bytes) {
return bytes.stream().map(bytes::new).toList();
}
static String diagnose(List<byte[]> a, List<byte[]> b) {
var actual = ofBytes(a);
var expected = ofBytes(b);
var message = actual.equals(expected) ? "match" : "differ";
return "%s and %s %s".formatted(actual, expected, message);
}
@ParameterizedTest
@MethodSource("servers")
public void simpleAggregatingBinaryMessages
(Function<int[],DummySecureWebSocketServer> serverSupplier,
Supplier<ProxyServer> proxyServerSupplier)
throws IOException
{
List<byte[]> expected = List.of("hello", "chegar")
.stream()
.map(s -> s.getBytes(StandardCharsets.US_ASCII))
.collect(Collectors.toList());
int[] binary = new int[]{
0x82, 0x05, 0x68, 0x65, 0x6C, 0x6C, 0x6F, // hello
0x82, 0x06, 0x63, 0x68, 0x65, 0x67, 0x61, 0x72, // chegar
0x88, 0x00 // <CLOSE>
};
CompletableFuture<List<byte[]>> actual = new CompletableFuture<>();
try (var proxyServer = proxyServerSupplier.get();
var server = serverSupplier.apply(binary)) {
InetSocketAddress proxyAddress = new InetSocketAddress(
InetAddress.getLoopbackAddress(), proxyServer.getPort());
server.open();
System.out.println("Server: " + server.getURI());
System.out.println("Proxy: " + proxyAddress);
WebSocket.Listener listener = new WebSocket.Listener() {
List<byte[]> collectedBytes = new ArrayList<>();
ByteBuffer buffer = ByteBuffer.allocate(1024);
@Override
public CompletionStage<?> onBinary(WebSocket webSocket,
ByteBuffer message,
boolean last) {
System.out.printf("onBinary(%s, %s)%n", message, last);
webSocket.request(1);
append(message);
if (last) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
buffer.clear();
processWholeBinary(bytes);
}
return null;
}
private void append(ByteBuffer message) {
if (buffer.remaining() < message.remaining()) {
assert message.remaining() > 0;
int cap = (buffer.capacity() + message.remaining()) * 2;
ByteBuffer b = ByteBuffer.allocate(cap);
b.put(buffer.flip());
buffer = b;
}
buffer.put(message);
}
private void processWholeBinary(byte[] bytes) {
String stringBytes = new String(bytes, UTF_8);
System.out.println("processWholeBinary: " + stringBytes);
collectedBytes.add(bytes);
}
@Override
public CompletionStage<?> onClose(WebSocket webSocket,
int statusCode,
String reason) {
actual.complete(collectedBytes);
return null;
}
@Override
public void onError(WebSocket webSocket, Throwable error) {
actual.completeExceptionally(error);
}
};
var webSocket = newBuilder()
.proxy(ProxySelector.of(proxyAddress))
.authenticator(new WSAuthenticator())
.build().newWebSocketBuilder()
.buildAsync(server.getURI(), listener)
.join();
List<byte[]> a = actual.join();
assertEquals(ofBytes(expected), ofBytes(a), diagnose(a, expected));
}
}
// -- authentication specific tests
/*
* Ensures authentication succeeds when an Authenticator set on client builder.
*/
@Test
public void clientAuthenticate() throws IOException {
try (var proxyServer = AUTH_TUNNELING_PROXY_SERVER.get();
var server = new DummySecureWebSocketServer()){
server.open();
InetSocketAddress proxyAddress = new InetSocketAddress(
InetAddress.getLoopbackAddress(), proxyServer.getPort());
var webSocket = newBuilder()
.proxy(ProxySelector.of(proxyAddress))
.authenticator(new WSAuthenticator())
.build()
.newWebSocketBuilder()
.buildAsync(server.getURI(), new WebSocket.Listener() { })
.join();
}
}
/*
* Ensures authentication succeeds when an `Authorization` header is explicitly set.
*/
@Test
public void explicitAuthenticate() throws IOException {
try (var proxyServer = AUTH_TUNNELING_PROXY_SERVER.get();
var server = new DummySecureWebSocketServer()) {
server.open();
InetSocketAddress proxyAddress = new InetSocketAddress(
InetAddress.getLoopbackAddress(), proxyServer.getPort());
String hv = "Basic " + Base64.getEncoder().encodeToString(
(USERNAME + ":" + PASSWORD).getBytes(UTF_8));
var webSocket = newBuilder()
.proxy(ProxySelector.of(proxyAddress)).build()
.newWebSocketBuilder()
.header("Proxy-Authorization", hv)
.buildAsync(server.getURI(), new WebSocket.Listener() { })
.join();
}
}
/*
* Ensures authentication succeeds when an `Authorization` header is explicitly set.
*/
@Test
public void explicitAuthenticate2() throws IOException {
try (var proxyServer = AUTH_TUNNELING_PROXY_SERVER.get();
var server = new DummySecureWebSocketServer(USERNAME, PASSWORD).secure()) {
server.open();
InetSocketAddress proxyAddress = new InetSocketAddress(
InetAddress.getLoopbackAddress(), proxyServer.getPort());
String hv = "Basic " + Base64.getEncoder().encodeToString(
(USERNAME + ":" + PASSWORD).getBytes(UTF_8));
var webSocket = newBuilder()
.proxy(ProxySelector.of(proxyAddress)).build()
.newWebSocketBuilder()
.header("Proxy-Authorization", hv)
.header("Authorization", hv)
.buildAsync(server.getURI(), new WebSocket.Listener() { })
.join();
}
}
/*
* Ensures authentication does not succeed when no authenticator is present.
*/
@Test
public void failNoAuthenticator() throws IOException {
try (var proxyServer = AUTH_TUNNELING_PROXY_SERVER.get();
var server = new DummySecureWebSocketServer(USERNAME, PASSWORD)) {
server.open();
InetSocketAddress proxyAddress = new InetSocketAddress(
InetAddress.getLoopbackAddress(), proxyServer.getPort());
CompletableFuture<WebSocket> cf = newBuilder()
.proxy(ProxySelector.of(proxyAddress)).build()
.newWebSocketBuilder()
.buildAsync(server.getURI(), new WebSocket.Listener() { });
try {
var webSocket = cf.join();
fail("Expected exception not thrown");
} catch (CompletionException expected) {
WebSocketHandshakeException e = (WebSocketHandshakeException)expected.getCause();
HttpResponse<?> response = e.getResponse();
assertEquals(407, response.statusCode());
}
}
}
/*
* Ensures authentication does not succeed when the authenticator presents
* unauthorized credentials.
*/
@Test
public void failBadCredentials() throws IOException {
try (var proxyServer = AUTH_TUNNELING_PROXY_SERVER.get();
var server = new DummySecureWebSocketServer(USERNAME, PASSWORD)) {
server.open();
InetSocketAddress proxyAddress = new InetSocketAddress(
InetAddress.getLoopbackAddress(), proxyServer.getPort());
Authenticator authenticator = new Authenticator() {
@Override protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication("BAD"+USERNAME, "".toCharArray());
}
};
CompletableFuture<WebSocket> cf = newBuilder()
.proxy(ProxySelector.of(proxyAddress))
.authenticator(authenticator)
.build()
.newWebSocketBuilder()
.buildAsync(server.getURI(), new WebSocket.Listener() { });
try {
var webSocket = cf.join();
fail("Expected exception not thrown");
} catch (CompletionException expected) {
System.out.println("caught expected exception:" + expected);
}
}
}
@BeforeEach
public void breakBetweenTests() {
System.gc();
try {Thread.sleep(100); } catch (InterruptedException x) { /* OK */ }
System.out.println("\n-------\n");
}
}

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2016, 2018, 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 8268294
* @modules java.net.http/jdk.internal.net.http.websocket:open jdk.httpserver
* @run main/othervm
* --add-reads java.net.http=ALL-UNNAMED
* --add-reads java.net.http=jdk.httpserver
* java.net.http/jdk.internal.net.http.websocket.WebSocketAndHttpTest
*/
public final class WebSocketServerDriver { }

View file

@ -0,0 +1,901 @@
/*
* Copyright (c) 2018, 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.
*/
/*
* @test
* @bug 8217429 8208693
* @library ../access
* @build DummyWebSocketServer
* java.net.http/jdk.internal.net.http.HttpClientTimerAccess
* @run junit/othervm
* WebSocketTest
*/
import java.io.IOException;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import java.net.http.HttpResponse;
import java.net.http.WebSocket;
import java.net.http.WebSocketHandshakeException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.HexFormat;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static java.net.http.HttpClient.Builder.NO_PROXY;
import static java.net.http.HttpClient.newBuilder;
import static java.net.http.WebSocket.NORMAL_CLOSURE;
import static java.nio.charset.StandardCharsets.UTF_8;
import static jdk.internal.net.http.HttpClientTimerAccess.assertNoResponseTimerEventRegistrations;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
public class WebSocketTest {
private static final Class<IllegalArgumentException> IAE = IllegalArgumentException.class;
private static final Class<IllegalStateException> ISE = IllegalStateException.class;
private static final Class<IOException> IOE = IOException.class;
/* shortcut */
private static void assertFails(Class<? extends Throwable> clazz,
CompletionStage<?> stage) {
Support.assertCompletesExceptionally(clazz, stage);
}
@Test
public void illegalArgument() throws IOException {
try (var server = new DummyWebSocketServer()) {
server.open();
var webSocket = newBuilder().proxy(NO_PROXY).build()
.newWebSocketBuilder()
.buildAsync(server.getURI(), new WebSocket.Listener() { })
.join();
try {
assertFails(IAE, webSocket.sendPing(ByteBuffer.allocate(126)));
assertFails(IAE, webSocket.sendPing(ByteBuffer.allocate(127)));
assertFails(IAE, webSocket.sendPing(ByteBuffer.allocate(128)));
assertFails(IAE, webSocket.sendPing(ByteBuffer.allocate(129)));
assertFails(IAE, webSocket.sendPing(ByteBuffer.allocate(256)));
assertFails(IAE, webSocket.sendPong(ByteBuffer.allocate(126)));
assertFails(IAE, webSocket.sendPong(ByteBuffer.allocate(127)));
assertFails(IAE, webSocket.sendPong(ByteBuffer.allocate(128)));
assertFails(IAE, webSocket.sendPong(ByteBuffer.allocate(129)));
assertFails(IAE, webSocket.sendPong(ByteBuffer.allocate(256)));
assertFails(IOE, webSocket.sendText(Support.incompleteString(), true));
assertFails(IOE, webSocket.sendText(Support.incompleteString(), false));
assertFails(IOE, webSocket.sendText(Support.malformedString(), true));
assertFails(IOE, webSocket.sendText(Support.malformedString(), false));
assertFails(IAE, webSocket.sendClose(NORMAL_CLOSURE, Support.stringWithNBytes(124)));
assertFails(IAE, webSocket.sendClose(NORMAL_CLOSURE, Support.stringWithNBytes(125)));
assertFails(IAE, webSocket.sendClose(NORMAL_CLOSURE, Support.stringWithNBytes(128)));
assertFails(IAE, webSocket.sendClose(NORMAL_CLOSURE, Support.stringWithNBytes(256)));
assertFails(IAE, webSocket.sendClose(NORMAL_CLOSURE, Support.stringWithNBytes(257)));
assertFails(IAE, webSocket.sendClose(NORMAL_CLOSURE, Support.stringWith2NBytes((123 / 2) + 1)));
assertFails(IAE, webSocket.sendClose(NORMAL_CLOSURE, Support.malformedString()));
assertFails(IAE, webSocket.sendClose(NORMAL_CLOSURE, Support.incompleteString()));
assertFails(IAE, webSocket.sendClose(-2, "a reason"));
assertFails(IAE, webSocket.sendClose(-1, "a reason"));
assertFails(IAE, webSocket.sendClose(0, "a reason"));
assertFails(IAE, webSocket.sendClose(1, "a reason"));
assertFails(IAE, webSocket.sendClose(500, "a reason"));
assertFails(IAE, webSocket.sendClose(998, "a reason"));
assertFails(IAE, webSocket.sendClose(999, "a reason"));
assertFails(IAE, webSocket.sendClose(1002, "a reason"));
assertFails(IAE, webSocket.sendClose(1003, "a reason"));
assertFails(IAE, webSocket.sendClose(1006, "a reason"));
assertFails(IAE, webSocket.sendClose(1007, "a reason"));
assertFails(IAE, webSocket.sendClose(1009, "a reason"));
assertFails(IAE, webSocket.sendClose(1010, "a reason"));
assertFails(IAE, webSocket.sendClose(1012, "a reason"));
assertFails(IAE, webSocket.sendClose(1013, "a reason"));
assertFails(IAE, webSocket.sendClose(1015, "a reason"));
assertFails(IAE, webSocket.sendClose(5000, "a reason"));
assertFails(IAE, webSocket.sendClose(32768, "a reason"));
assertFails(IAE, webSocket.sendClose(65535, "a reason"));
assertFails(IAE, webSocket.sendClose(65536, "a reason"));
assertFails(IAE, webSocket.sendClose(Integer.MAX_VALUE, "a reason"));
assertFails(IAE, webSocket.sendClose(Integer.MIN_VALUE, "a reason"));
assertThrows(IAE, () -> webSocket.request(Integer.MIN_VALUE));
assertThrows(IAE, () -> webSocket.request(Long.MIN_VALUE));
assertThrows(IAE, () -> webSocket.request(-1));
assertThrows(IAE, () -> webSocket.request(0));
} finally {
webSocket.abort();
}
}
}
/**
* Verifies that the internally issued request to establish the WebSocket
* connection does not leave any response timers registered at the client
* after the WebSocket handshake.
*/
@Test
public void responseTimerCleanUp() throws Exception {
try (var server = new DummyWebSocketServer()) {
server.open();
try (var client = newBuilder().proxy(NO_PROXY).build()) {
var connectionEstablished = new CountDownLatch(1);
var webSocketListener = new WebSocket.Listener() {
@Override
public void onOpen(WebSocket webSocket) {
connectionEstablished.countDown();
}
};
var webSocket = client
.newWebSocketBuilder()
// Explicitly configure a timeout to get a response
// timer event get registered at the client. The query
// should succeed without timing out.
.connectTimeout(Duration.ofMinutes(2))
.buildAsync(server.getURI(), webSocketListener)
.join();
try {
connectionEstablished.await();
// We expect the response timer event to get evicted once
// the WebSocket handshake headers are received.
assertNoResponseTimerEventRegistrations(client);
} finally {
webSocket.abort();
}
}
}
}
@Test
public void partialBinaryThenText() throws IOException {
try (var server = new DummyWebSocketServer()) {
server.open();
var webSocket = newBuilder().proxy(NO_PROXY).build().newWebSocketBuilder()
.buildAsync(server.getURI(), new WebSocket.Listener() { })
.join();
try {
webSocket.sendBinary(ByteBuffer.allocate(16), false).join();
assertFails(ISE, webSocket.sendText("text", false));
assertFails(ISE, webSocket.sendText("text", true));
// Pings & Pongs are fine
webSocket.sendPing(ByteBuffer.allocate(125)).join();
webSocket.sendPong(ByteBuffer.allocate(125)).join();
} finally {
webSocket.abort();
}
}
}
@Test
public void partialTextThenBinary() throws IOException {
try (var server = new DummyWebSocketServer()) {
server.open();
var webSocket = newBuilder().proxy(NO_PROXY).build().newWebSocketBuilder()
.buildAsync(server.getURI(), new WebSocket.Listener() { })
.join();
try {
webSocket.sendText("text", false).join();
assertFails(ISE, webSocket.sendBinary(ByteBuffer.allocate(16), false));
assertFails(ISE, webSocket.sendBinary(ByteBuffer.allocate(16), true));
// Pings & Pongs are fine
webSocket.sendPing(ByteBuffer.allocate(125)).join();
webSocket.sendPong(ByteBuffer.allocate(125)).join();
} finally {
webSocket.abort();
}
}
}
@Test
public void sendMethodsThrowIOE1() throws IOException {
try (var server = new DummyWebSocketServer()) {
server.open();
var webSocket = newBuilder().proxy(NO_PROXY).build()
.newWebSocketBuilder()
.buildAsync(server.getURI(), new WebSocket.Listener() { })
.join();
try {
webSocket.sendClose(NORMAL_CLOSURE, "ok").join();
assertFails(IOE, webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "ok"));
assertFails(IOE, webSocket.sendText("", true));
assertFails(IOE, webSocket.sendText("", false));
assertFails(IOE, webSocket.sendText("abc", true));
assertFails(IOE, webSocket.sendText("abc", false));
assertFails(IOE, webSocket.sendBinary(ByteBuffer.allocate(0), true));
assertFails(IOE, webSocket.sendBinary(ByteBuffer.allocate(0), false));
assertFails(IOE, webSocket.sendBinary(ByteBuffer.allocate(1), true));
assertFails(IOE, webSocket.sendBinary(ByteBuffer.allocate(1), false));
assertFails(IOE, webSocket.sendPing(ByteBuffer.allocate(125)));
assertFails(IOE, webSocket.sendPing(ByteBuffer.allocate(124)));
assertFails(IOE, webSocket.sendPing(ByteBuffer.allocate(1)));
assertFails(IOE, webSocket.sendPing(ByteBuffer.allocate(0)));
assertFails(IOE, webSocket.sendPong(ByteBuffer.allocate(125)));
assertFails(IOE, webSocket.sendPong(ByteBuffer.allocate(124)));
assertFails(IOE, webSocket.sendPong(ByteBuffer.allocate(1)));
assertFails(IOE, webSocket.sendPong(ByteBuffer.allocate(0)));
} finally {
webSocket.abort();
}
}
}
public static Object[][] data1() {
int[] CLOSE = {
0x81, 0x00, // ""
0x82, 0x00, // []
0x89, 0x00, // <PING>
0x8a, 0x00, // <PONG>
0x88, 0x00, // <CLOSE>
};
int[] ERROR = {
0x81, 0x00, // ""
0x82, 0x00, // []
0x89, 0x00, // <PING>
0x8a, 0x00, // <PONG>
0x8b, 0x00, // 0xB control frame (causes an error)
};
return new Object[][]{
{CLOSE, 1},
{CLOSE, 3},
{CLOSE, 4},
{CLOSE, Long.MAX_VALUE},
{ERROR, 1},
{ERROR, 3},
{ERROR, 4},
{ERROR, Long.MAX_VALUE},
};
}
@ParameterizedTest
@MethodSource("data1")
public void listenerSequentialOrder(int[] binary, long requestSize)
throws IOException
{
try (var server = Support.serverWithCannedData(binary)) {
server.open();
CompletableFuture<Void> violation = new CompletableFuture<>();
MockListener listener = new MockListener(requestSize) {
final AtomicBoolean guard = new AtomicBoolean();
private <T> T checkRunExclusively(Supplier<T> action) {
if (guard.getAndSet(true)) {
violation.completeExceptionally(new RuntimeException());
}
try {
return action.get();
} finally {
if (!guard.getAndSet(false)) {
violation.completeExceptionally(new RuntimeException());
}
}
}
@Override
public void onOpen(WebSocket webSocket) {
checkRunExclusively(() -> {
super.onOpen(webSocket);
return null;
});
}
@Override
public CompletionStage<?> onText(WebSocket webSocket,
CharSequence data,
boolean last) {
return checkRunExclusively(
() -> super.onText(webSocket, data, last));
}
@Override
public CompletionStage<?> onBinary(WebSocket webSocket,
ByteBuffer data,
boolean last) {
return checkRunExclusively(
() -> super.onBinary(webSocket, data, last));
}
@Override
public CompletionStage<?> onPing(WebSocket webSocket,
ByteBuffer message) {
return checkRunExclusively(
() -> super.onPing(webSocket, message));
}
@Override
public CompletionStage<?> onPong(WebSocket webSocket,
ByteBuffer message) {
return checkRunExclusively(
() -> super.onPong(webSocket, message));
}
@Override
public CompletionStage<?> onClose(WebSocket webSocket,
int statusCode,
String reason) {
return checkRunExclusively(
() -> super.onClose(webSocket, statusCode, reason));
}
@Override
public void onError(WebSocket webSocket, Throwable error) {
checkRunExclusively(() -> {
super.onError(webSocket, error);
return null;
});
}
};
var webSocket = newBuilder().proxy(NO_PROXY).build().newWebSocketBuilder()
.buildAsync(server.getURI(), listener)
.join();
try {
listener.invocations();
violation.complete(null); // won't affect if completed exceptionally
violation.join();
} finally {
webSocket.abort();
}
}
}
@Test
public void sendMethodsThrowIOE2() throws Exception {
try (var server = Support.serverWithCannedData(0x88, 0x00)) {
server.open();
CompletableFuture<Void> onCloseCalled = new CompletableFuture<>();
CompletableFuture<Void> canClose = new CompletableFuture<>();
WebSocket.Listener listener = new WebSocket.Listener() {
@Override
public CompletionStage<?> onClose(WebSocket webSocket,
int statusCode,
String reason) {
System.out.printf("onClose(%s, '%s')%n", statusCode, reason);
onCloseCalled.complete(null);
return canClose;
}
@Override
public void onError(WebSocket webSocket, Throwable error) {
System.out.println("onError(" + error + ")");
onCloseCalled.completeExceptionally(error);
}
};
var webSocket = newBuilder().proxy(NO_PROXY).build().newWebSocketBuilder()
.buildAsync(server.getURI(), listener)
.join();
try {
onCloseCalled.join(); // Wait for onClose to be called
canClose.complete(null); // Signal to the WebSocket it can close the output
TimeUnit.SECONDS.sleep(5); // Give canClose some time to reach the WebSocket
assertFails(IOE, webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "ok"));
assertFails(IOE, webSocket.sendText("", true));
assertFails(IOE, webSocket.sendText("", false));
assertFails(IOE, webSocket.sendText("abc", true));
assertFails(IOE, webSocket.sendText("abc", false));
assertFails(IOE, webSocket.sendBinary(ByteBuffer.allocate(0), true));
assertFails(IOE, webSocket.sendBinary(ByteBuffer.allocate(0), false));
assertFails(IOE, webSocket.sendBinary(ByteBuffer.allocate(1), true));
assertFails(IOE, webSocket.sendBinary(ByteBuffer.allocate(1), false));
assertFails(IOE, webSocket.sendPing(ByteBuffer.allocate(125)));
assertFails(IOE, webSocket.sendPing(ByteBuffer.allocate(124)));
assertFails(IOE, webSocket.sendPing(ByteBuffer.allocate(1)));
assertFails(IOE, webSocket.sendPing(ByteBuffer.allocate(0)));
assertFails(IOE, webSocket.sendPong(ByteBuffer.allocate(125)));
assertFails(IOE, webSocket.sendPong(ByteBuffer.allocate(124)));
assertFails(IOE, webSocket.sendPong(ByteBuffer.allocate(1)));
assertFails(IOE, webSocket.sendPong(ByteBuffer.allocate(0)));
} finally {
webSocket.abort();
}
}
}
// Used to verify a server requiring Authentication
private static final String USERNAME = "chegar";
private static final String PASSWORD = "a1b2c3";
static class WSAuthenticator extends Authenticator {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(USERNAME, PASSWORD.toCharArray());
}
}
static final Function<int[],DummyWebSocketServer> SERVER_WITH_CANNED_DATA =
new Function<>() {
@Override public DummyWebSocketServer apply(int[] data) {
return Support.serverWithCannedData(data); }
@Override public String toString() { return "SERVER_WITH_CANNED_DATA"; }
};
static final Function<int[],DummyWebSocketServer> AUTH_SERVER_WITH_CANNED_DATA =
new Function<>() {
@Override public DummyWebSocketServer apply(int[] data) {
return Support.serverWithCannedDataAndAuthentication(USERNAME, PASSWORD, data); }
@Override public String toString() { return "AUTH_SERVER_WITH_CANNED_DATA"; }
};
public static Object[][] servers() {
return new Object[][] {
{ SERVER_WITH_CANNED_DATA },
{ AUTH_SERVER_WITH_CANNED_DATA },
};
}
record bytes(byte[] bytes) {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o instanceof bytes other) {
return Arrays.equals(bytes(), other.bytes());
}
return false;
}
@Override
public int hashCode() { return Arrays.hashCode(bytes()); }
public String toString() {
return "0x" + HexFormat.of()
.withUpperCase()
.formatHex(bytes());
}
}
static List<bytes> ofBytes(List<byte[]> bytes) {
return bytes.stream().map(bytes::new).toList();
}
static String diagnose(List<byte[]> a, List<byte[]> b) {
var actual = ofBytes(a);
var expected = ofBytes(b);
var message = actual.equals(expected) ? "match" : "differ";
return "%s and %s %s".formatted(actual, expected, message);
}
@ParameterizedTest
@MethodSource("servers")
public void simpleAggregatingBinaryMessages
(Function<int[],DummyWebSocketServer> serverSupplier)
throws IOException
{
List<byte[]> expected = List.of("alpha", "beta", "gamma", "delta")
.stream()
.map(s -> s.getBytes(StandardCharsets.US_ASCII))
.collect(Collectors.toList());
int[] binary = new int[]{
0x82, 0x05, 0x61, 0x6c, 0x70, 0x68, 0x61, // [alpha]
0x02, 0x02, 0x62, 0x65, // [be
0x80, 0x02, 0x74, 0x61, // ta]
0x02, 0x01, 0x67, // [g
0x00, 0x01, 0x61, // a
0x00, 0x00, //
0x00, 0x00, //
0x00, 0x01, 0x6d, // m
0x00, 0x01, 0x6d, // m
0x80, 0x01, 0x61, // a]
0x8a, 0x00, // <PONG>
0x02, 0x04, 0x64, 0x65, 0x6c, 0x74, // [delt
0x00, 0x01, 0x61, // a
0x80, 0x00, // ]
0x88, 0x00 // <CLOSE>
};
CompletableFuture<List<byte[]>> actual = new CompletableFuture<>();
try (var server = serverSupplier.apply(binary)) {
server.open();
WebSocket.Listener listener = new WebSocket.Listener() {
List<byte[]> collectedBytes = new ArrayList<>();
ByteBuffer buffer = ByteBuffer.allocate(1024);
@Override
public CompletionStage<?> onBinary(WebSocket webSocket,
ByteBuffer message,
boolean last) {
System.out.printf("onBinary(%s, %s)%n", message, last);
webSocket.request(1);
append(message);
if (last) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
buffer.clear();
processWholeBinary(bytes);
}
return null;
}
private void append(ByteBuffer message) {
if (buffer.remaining() < message.remaining()) {
assert message.remaining() > 0;
int cap = (buffer.capacity() + message.remaining()) * 2;
ByteBuffer b = ByteBuffer.allocate(cap);
b.put(buffer.flip());
buffer = b;
}
buffer.put(message);
}
private void processWholeBinary(byte[] bytes) {
String stringBytes = new String(bytes, UTF_8);
System.out.println("processWholeBinary: " + stringBytes);
collectedBytes.add(bytes);
}
@Override
public CompletionStage<?> onClose(WebSocket webSocket,
int statusCode,
String reason) {
actual.complete(collectedBytes);
return null;
}
@Override
public void onError(WebSocket webSocket, Throwable error) {
actual.completeExceptionally(error);
}
};
var webSocket = newBuilder()
.proxy(NO_PROXY)
.authenticator(new WSAuthenticator())
.build().newWebSocketBuilder()
.buildAsync(server.getURI(), listener)
.join();
try {
List<byte[]> a = actual.join();
assertEquals(ofBytes(expected), ofBytes(a), diagnose(a, expected));
} finally {
webSocket.abort();
}
}
}
@ParameterizedTest
@MethodSource("servers")
public void simpleAggregatingTextMessages
(Function<int[],DummyWebSocketServer> serverSupplier)
throws IOException
{
List<String> expected = List.of("alpha", "beta", "gamma", "delta");
int[] binary = new int[]{
0x81, 0x05, 0x61, 0x6c, 0x70, 0x68, 0x61, // "alpha"
0x01, 0x02, 0x62, 0x65, // "be
0x80, 0x02, 0x74, 0x61, // ta"
0x01, 0x01, 0x67, // "g
0x00, 0x01, 0x61, // a
0x00, 0x00, //
0x00, 0x00, //
0x00, 0x01, 0x6d, // m
0x00, 0x01, 0x6d, // m
0x80, 0x01, 0x61, // a"
0x8a, 0x00, // <PONG>
0x01, 0x04, 0x64, 0x65, 0x6c, 0x74, // "delt
0x00, 0x01, 0x61, // a
0x80, 0x00, // "
0x88, 0x00 // <CLOSE>
};
CompletableFuture<List<String>> actual = new CompletableFuture<>();
try (var server = serverSupplier.apply(binary)) {
server.open();
WebSocket.Listener listener = new WebSocket.Listener() {
List<String> collectedStrings = new ArrayList<>();
StringBuilder text = new StringBuilder();
@Override
public CompletionStage<?> onText(WebSocket webSocket,
CharSequence message,
boolean last) {
System.out.printf("onText(%s, %s)%n", message, last);
webSocket.request(1);
text.append(message);
if (last) {
String str = text.toString();
text.setLength(0);
processWholeText(str);
}
return null;
}
private void processWholeText(String string) {
System.out.println(string);
collectedStrings.add(string);
}
@Override
public CompletionStage<?> onClose(WebSocket webSocket,
int statusCode,
String reason) {
actual.complete(collectedStrings);
return null;
}
@Override
public void onError(WebSocket webSocket, Throwable error) {
actual.completeExceptionally(error);
}
};
var webSocket = newBuilder()
.proxy(NO_PROXY)
.authenticator(new WSAuthenticator())
.build().newWebSocketBuilder()
.buildAsync(server.getURI(), listener)
.join();
try {
List<String> a = actual.join();
assertEquals(expected, a);
} finally {
webSocket.abort();
}
}
}
/*
* Exercises the scenario where requests for more messages are made prior to
* completing the returned CompletionStage instances.
*/
@ParameterizedTest
@MethodSource("servers")
public void aggregatingTextMessages
(Function<int[],DummyWebSocketServer> serverSupplier)
throws IOException
{
List<String> expected = List.of("alpha", "beta", "gamma", "delta");
int[] binary = new int[]{
0x81, 0x05, 0x61, 0x6c, 0x70, 0x68, 0x61, // "alpha"
0x01, 0x02, 0x62, 0x65, // "be
0x80, 0x02, 0x74, 0x61, // ta"
0x01, 0x01, 0x67, // "g
0x00, 0x01, 0x61, // a
0x00, 0x00, //
0x00, 0x00, //
0x00, 0x01, 0x6d, // m
0x00, 0x01, 0x6d, // m
0x80, 0x01, 0x61, // a"
0x8a, 0x00, // <PONG>
0x01, 0x04, 0x64, 0x65, 0x6c, 0x74, // "delt
0x00, 0x01, 0x61, // a
0x80, 0x00, // "
0x88, 0x00 // <CLOSE>
};
CompletableFuture<List<String>> actual = new CompletableFuture<>();
try (var server = serverSupplier.apply(binary)) {
server.open();
WebSocket.Listener listener = new WebSocket.Listener() {
List<CharSequence> parts = new ArrayList<>();
/*
* A CompletableFuture which will complete once the current
* message has been fully assembled. Until then the listener
* returns this instance for every call.
*/
CompletableFuture<?> currentCf = new CompletableFuture<>();
List<String> collected = new ArrayList<>();
@Override
public CompletionStage<?> onText(WebSocket webSocket,
CharSequence message,
boolean last) {
parts.add(message);
if (!last) {
webSocket.request(1);
} else {
this.currentCf.thenRun(() -> webSocket.request(1));
CompletableFuture<?> refCf = this.currentCf;
processWholeMessage(new ArrayList<>(parts), refCf);
currentCf = new CompletableFuture<>();
parts.clear();
return refCf;
}
return currentCf;
}
@Override
public CompletionStage<?> onClose(WebSocket webSocket,
int statusCode,
String reason) {
actual.complete(collected);
return null;
}
@Override
public void onError(WebSocket webSocket, Throwable error) {
actual.completeExceptionally(error);
}
public void processWholeMessage(List<CharSequence> data,
CompletableFuture<?> cf) {
StringBuilder b = new StringBuilder();
data.forEach(b::append);
String s = b.toString();
System.out.println(s);
cf.complete(null);
collected.add(s);
}
};
var webSocket = newBuilder()
.proxy(NO_PROXY)
.authenticator(new WSAuthenticator())
.build().newWebSocketBuilder()
.buildAsync(server.getURI(), listener)
.join();
try {
List<String> a = actual.join();
assertEquals(expected, a);
} finally {
webSocket.abort();
}
}
}
// -- authentication specific tests
/*
* Ensures authentication succeeds when an Authenticator set on client builder.
*/
@Test
public void clientAuthenticate() throws IOException {
try (var server = new DummyWebSocketServer(USERNAME, PASSWORD)){
server.open();
var webSocket = newBuilder()
.proxy(NO_PROXY)
.authenticator(new WSAuthenticator())
.build()
.newWebSocketBuilder()
.buildAsync(server.getURI(), new WebSocket.Listener() { })
.join();
webSocket.abort();
}
}
/*
* Ensures authentication succeeds when an `Authorization` header is explicitly set.
*/
@Test
public void explicitAuthenticate() throws IOException {
try (var server = new DummyWebSocketServer(USERNAME, PASSWORD)) {
server.open();
String hv = "Basic " + Base64.getEncoder().encodeToString(
(USERNAME + ":" + PASSWORD).getBytes(UTF_8));
var webSocket = newBuilder()
.proxy(NO_PROXY).build()
.newWebSocketBuilder()
.header("Authorization", hv)
.buildAsync(server.getURI(), new WebSocket.Listener() { })
.join();
webSocket.abort();
}
}
/*
* Ensures authentication does not succeed when no authenticator is present.
*/
@Test
public void failNoAuthenticator() throws IOException {
try (var server = new DummyWebSocketServer(USERNAME, PASSWORD)) {
server.open();
CompletableFuture<WebSocket> cf = newBuilder()
.proxy(NO_PROXY).build()
.newWebSocketBuilder()
.buildAsync(server.getURI(), new WebSocket.Listener() { });
try {
var webSocket = cf.join();
silentAbort(webSocket);
fail("Expected exception not thrown");
} catch (CompletionException expected) {
WebSocketHandshakeException e = (WebSocketHandshakeException)expected.getCause();
HttpResponse<?> response = e.getResponse();
assertEquals(401, response.statusCode());
}
}
}
/*
* Ensures authentication does not succeed when the authenticator presents
* unauthorized credentials.
*/
@Test
public void failBadCredentials() throws IOException {
try (var server = new DummyWebSocketServer(USERNAME, PASSWORD)) {
server.open();
Authenticator authenticator = new Authenticator() {
@Override protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication("BAD" + USERNAME, "".toCharArray());
}
};
CompletableFuture<WebSocket> cf = newBuilder()
.proxy(NO_PROXY)
.authenticator(authenticator)
.build()
.newWebSocketBuilder()
.buildAsync(server.getURI(), new WebSocket.Listener() { });
try {
var webSocket = cf.join();
silentAbort(webSocket);
fail("Expected exception not thrown");
} catch (CompletionException expected) {
System.out.println("caught expected exception:" + expected);
}
}
}
private static void silentAbort(WebSocket ws) {
try {
ws.abort();
} catch (Throwable t) { }
}
}

View file

@ -0,0 +1,47 @@
/*
* Copyright (c) 2021, 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.
*/
package jdk.internal.net.http.websocket;
import java.nio.ByteBuffer;
/**
* No implementation provided for onInit() because that must always be
* implemented by user
*/
abstract class DefaultMessageStreamHandler implements MessageStreamHandler {
public void onText(CharSequence data, boolean last) {}
public void onBinary(ByteBuffer data, boolean last) {}
public void onPing(ByteBuffer data) {}
public void onPong(ByteBuffer data) {}
public void onClose(int statusCode, CharSequence reason) {}
public void onComplete() {}
public void onError(Throwable e) {}
}

View file

@ -0,0 +1,116 @@
/*
* Copyright (c) 2016, 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.
*/
package jdk.internal.net.http.websocket;
import jdk.internal.net.http.websocket.Frame.HeaderWriter;
import jdk.internal.net.http.websocket.Frame.Opcode;
import java.nio.ByteBuffer;
import java.util.OptionalInt;
import static java.util.OptionalInt.empty;
import static java.util.OptionalInt.of;
import static jdk.internal.net.http.websocket.TestSupport.assertThrows;
import static jdk.internal.net.http.websocket.TestSupport.forEachPermutation;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class HeaderWriterTest {
private long cases, frames;
@Test
public void negativePayload() {
System.out.println("testing negative payload");
HeaderWriter w = new HeaderWriter();
assertThrows(IllegalArgumentException.class,
".*(?i)negative.*",
() -> w.payloadLen(-1));
}
@Test
public void test() {
System.out.println("testing regular payloads");
final long[] payloads = {0, 126, 65536, Integer.MAX_VALUE + 1L};
final OptionalInt[] masks = {empty(), of(-1), of(0), of(0xCAFEBABE),
of(Integer.MAX_VALUE), of(Integer.MIN_VALUE)};
for (boolean fin : new boolean[]{true, false}) {
for (boolean rsv1 : new boolean[]{true, false}) {
for (boolean rsv2 : new boolean[]{true, false}) {
for (boolean rsv3 : new boolean[]{true, false}) {
for (Opcode opcode : Opcode.values()) {
for (long payloadLen : payloads) {
for (OptionalInt mask : masks) {
verify(fin, rsv1, rsv2, rsv3, opcode, payloadLen, mask);
}
}
}
}
}
}
}
System.out.println("Frames: " + frames + ", Total cases: " + cases);
}
private void verify(boolean fin,
boolean rsv1,
boolean rsv2,
boolean rsv3,
Opcode opcode,
long payloadLen,
OptionalInt mask) {
frames++;
HeaderWriter writer = new HeaderWriter();
ByteBuffer expected = ByteBuffer.allocate(Frame.MAX_HEADER_SIZE_BYTES);
writer.fin(fin).rsv1(rsv1).rsv2(rsv2).rsv3(rsv3).opcode(opcode).payloadLen(payloadLen);
mask.ifPresentOrElse(writer::mask, writer::noMask);
writer.write(expected);
expected.flip();
verifyPermutations(expected, writer,
() -> writer.fin(fin),
() -> writer.rsv1(rsv1),
() -> writer.rsv2(rsv2),
() -> writer.rsv3(rsv3),
() -> writer.opcode(opcode),
() -> writer.payloadLen(payloadLen),
() -> mask.ifPresentOrElse(writer::mask, writer::noMask));
}
private void verifyPermutations(ByteBuffer expected,
HeaderWriter writer,
Runnable... actions) {
forEachPermutation(actions.length,
order -> {
cases++;
for (int i : order) {
actions[i].run();
}
ByteBuffer actual = ByteBuffer.allocate(Frame.MAX_HEADER_SIZE_BYTES + 2);
writer.write(actual);
actual.flip();
assertEquals(expected, actual);
});
}
}

View file

@ -0,0 +1,158 @@
/*
* Copyright (c) 2016, 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.
*/
package jdk.internal.net.http.websocket;
import java.nio.ByteBuffer;
import java.security.SecureRandom;
import java.util.stream.IntStream;
import static jdk.internal.net.http.websocket.Frame.Masker.applyMask;
import static jdk.internal.net.http.websocket.TestSupport.forEachBufferPartition;
import static jdk.internal.net.http.websocket.TestSupport.fullCopy;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class MaskerTest {
private static final SecureRandom random = new SecureRandom();
@Test
public void stateless() {
IntStream.iterate(0, i -> i + 1).limit(125).boxed()
.forEach(r -> {
int m = random.nextInt();
ByteBuffer src = createSourceBuffer(r);
ByteBuffer dst = createDestinationBuffer(r);
verify(src, dst, maskArray(m), 0,
() -> applyMask(src, dst, m));
});
}
/*
* Stateful masker to make sure setting a mask resets the state as if a new
* Masker instance is created each time
*/
private final Frame.Masker masker = new Frame.Masker();
@Test
public void stateful0() {
// This size (17 = 8 + 8 + 1) should test all the stages
// (galloping/slow) of masking good enough
int N = 17;
ByteBuffer src = createSourceBuffer(N);
ByteBuffer dst = createDestinationBuffer(N);
int mask = random.nextInt();
forEachBufferPartition(src,
buffers -> {
int offset = 0;
masker.setMask(mask);
int[] maskBytes = maskArray(mask);
for (ByteBuffer s : buffers) {
offset = verify(s, dst, maskBytes, offset,
() -> masker.applyMask(s, dst));
}
});
}
@Test
public void stateful1() {
int m = random.nextInt();
masker.setMask(m);
ByteBuffer src = ByteBuffer.allocate(0);
ByteBuffer dst = ByteBuffer.allocate(16);
verify(src, dst, maskArray(m), 0,
() -> masker.applyMask(src, dst));
}
private static int verify(ByteBuffer src,
ByteBuffer dst,
int[] maskBytes,
int offset,
Runnable masking) {
ByteBuffer srcCopy = fullCopy(src);
ByteBuffer dstCopy = fullCopy(dst);
masking.run();
int srcRemaining = srcCopy.remaining();
int dstRemaining = dstCopy.remaining();
int masked = Math.min(srcRemaining, dstRemaining);
// 1. position check
assertEquals(srcCopy.position() + masked, src.position());
assertEquals(dstCopy.position() + masked, dst.position());
// 2. masking check
src.position(srcCopy.position());
dst.position(dstCopy.position());
for (; src.hasRemaining() && dst.hasRemaining();
offset = (offset + 1) & 3) {
assertEquals(src.get() ^ maskBytes[offset], dst.get());
}
// 3. corruption check
// 3.1 src contents haven't changed
int srcPosition = src.position();
int srcLimit = src.limit();
src.clear();
srcCopy.clear();
assertEquals(srcCopy, src);
src.limit(srcLimit).position(srcPosition); // restore src
// 3.2 dst leading and trailing regions' contents haven't changed
int dstPosition = dst.position();
int dstInitialPosition = dstCopy.position();
int dstLimit = dst.limit();
// leading
dst.position(0).limit(dstInitialPosition);
dstCopy.position(0).limit(dstInitialPosition);
assertEquals(dstCopy, dst);
// trailing
dst.limit(dst.capacity()).position(dstLimit);
dstCopy.limit(dst.capacity()).position(dstLimit);
assertEquals(dstCopy, dst);
// restore dst
dst.position(dstPosition).limit(dstLimit);
return offset;
}
private static ByteBuffer createSourceBuffer(int remaining) {
int leading = random.nextInt(4);
int trailing = random.nextInt(4);
byte[] bytes = new byte[leading + remaining + trailing];
random.nextBytes(bytes);
return ByteBuffer.wrap(bytes).position(leading).limit(leading + remaining);
}
private static ByteBuffer createDestinationBuffer(int remaining) {
int leading = random.nextInt(4);
int trailing = random.nextInt(4);
return ByteBuffer.allocate(leading + remaining + trailing)
.position(leading).limit(leading + remaining);
}
private static int[] maskArray(int mask) {
return new int[]{
(byte) (mask >>> 24),
(byte) (mask >>> 16),
(byte) (mask >>> 8),
(byte) (mask >>> 0)
};
}
}

View file

@ -0,0 +1,483 @@
/*
* Copyright (c) 2018, 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.
*/
package jdk.internal.net.http.websocket;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Queue;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
import static jdk.internal.net.http.websocket.MessageQueue.effectiveCapacityOf;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*
* A unit test for MessageQueue. The test is aware of the details of the queue
* implementation.
*/
public class MessageQueueTest {
private static final Random r = new SecureRandom();
public static Object[][] illegalCapacities() {
return new Object[][]{
new Object[]{Integer.MIN_VALUE},
new Object[]{-2},
new Object[]{-1},
new Object[]{ 0},
};
}
@ParameterizedTest
@MethodSource("illegalCapacities")
public void illegalCapacity(int n) {
assertThrows(IllegalArgumentException.class, () -> new MessageQueue(n));
}
@ParameterizedTest
@MethodSource("capacities")
public void emptiness(int n) {
assertTrue(new MessageQueue(n).isEmpty());
}
@ParameterizedTest
@MethodSource("capacities")
public void fullness(int n) throws IOException {
MessageQueue q = new MessageQueue(n);
int cap = effectiveCapacityOf(n);
Adder adder = new Adder();
Queue<Message> referenceQueue = new LinkedList<>();
for (int i = 0; i < cap; i++) {
Message m = createRandomMessage();
referenceQueue.add(m);
adder.add(q, m);
}
for (int i = 0; i < cap + 1; i++) {
Message m = createRandomMessage();
assertThrows(IOException.class, () -> adder.add(q, m));
}
for (int i = 0; i < cap; i++) {
Message expected = referenceQueue.remove();
Message actual = new Remover().removeFrom(q);
assertEquals(expected, actual);
}
}
private Message createRandomMessage() {
Message.Type[] values = Message.Type.values();
Message.Type type = values[r.nextInt(values.length)];
Supplier<? extends ByteBuffer> binarySupplier = null;
ByteBuffer binary = null;
CharBuffer text = null;
boolean isLast = false;
int statusCode = -1;
switch (type) {
case TEXT:
text = CharBuffer.allocate(r.nextInt(17));
isLast = r.nextBoolean();
break;
case BINARY:
binary = ByteBuffer.allocate(r.nextInt(19));
isLast = r.nextBoolean();
break;
case PING:
binary = ByteBuffer.allocate(r.nextInt(19));
break;
case PONG:
if (r.nextBoolean()) {
binary = ByteBuffer.allocate(r.nextInt(19));
} else {
binarySupplier = () -> ByteBuffer.allocate(r.nextInt(19));
}
break;
case CLOSE:
text = CharBuffer.allocate(r.nextInt(17));
statusCode = r.nextInt();
break;
default:
throw new AssertionError();
}
BiConsumer<Integer, Throwable> action = new BiConsumer<>() {
@Override
public void accept(Integer o, Throwable throwable) { }
};
CompletableFuture<Integer> future = new CompletableFuture<>();
return new Message(type, binarySupplier, binary, text, isLast, statusCode, r.nextInt(),
action, future);
}
@ParameterizedTest
@MethodSource("capacities")
public void caterpillarWalk(int n) throws IOException {
// System.out.println("n: " + n);
int cap = effectiveCapacityOf(n);
for (int p = 1; p <= cap; p++) { // pace
// System.out.println(" pace: " + p);
MessageQueue q = new MessageQueue(n);
Queue<Message> referenceQueue = new LinkedList<>();
Adder adder = new Adder();
for (int k = 0; k < (cap / p) + 1; k++) {
// System.out.println(" cycle: " + k);
for (int i = 0; i < p; i++) {
Message m = createRandomMessage();
referenceQueue.add(m);
adder.add(q, m);
}
Remover remover = new Remover();
for (int i = 0; i < p; i++) {
Message expected = referenceQueue.remove();
Message actual = remover.removeFrom(q);
assertEquals(expected, actual);
}
assertTrue(q.isEmpty());
}
}
}
/* Exercises only concurrent additions */
@Test
public void halfConcurrency() throws ExecutionException, InterruptedException {
int n = Runtime.getRuntime().availableProcessors() + 2;
ExecutorService executorService = Executors.newFixedThreadPool(n);
CyclicBarrier start = new CyclicBarrier(n);
Adder adder = new Adder();
List<Future<?>> futures = new ArrayList<>(n);
try {
for (int k = 0; k < 1024; k++) {
MessageQueue q = new MessageQueue(n);
for (int i = 0; i < n; i++) {
Message m = createRandomMessage();
Future<Void> f = executorService.submit(() -> {
start.await();
adder.add(q, m);
return null;
});
futures.add(f);
}
for (Future<?> f : futures) {
f.get(); // Just to check for exceptions
}
futures.clear();
}
} finally {
executorService.shutdownNow();
}
}
// TODO: same message; different messages; a mix thereof
@Test
public void concurrency() throws ExecutionException, InterruptedException {
int nProducers = Runtime.getRuntime().availableProcessors() + 2;
int nThreads = nProducers + 1;
ExecutorService executorService = Executors.newFixedThreadPool(nThreads);
CyclicBarrier start = new CyclicBarrier(nThreads);
MessageQueue q = new MessageQueue(nProducers);
Adder adder = new Adder();
Remover remover = new Remover();
List<Message> expectedList = new ArrayList<>(nProducers);
List<Message> actualList = new ArrayList<>(nProducers);
List<Future<?>> futures = new ArrayList<>(nProducers);
try {
for (int k = 0; k < 1024; k++) {
for (int i = 0; i < nProducers; i++) {
Message m = createRandomMessage();
expectedList.add(m);
Future<Void> f = executorService.submit(() -> {
start.await();
adder.add(q, m);
return null;
});
futures.add(f);
}
Future<Void> consumer = executorService.submit(() -> {
int i = 0;
start.await();
while (i < nProducers) {
Message m = remover.removeFrom(q);
if (m != null) {
actualList.add(m);
i++;
}
}
return null;
});
for (Future<?> f : futures) {
f.get(); // Just to check for exceptions
}
consumer.get(); // Waiting for consumer to collect all the messages
assertEquals(expectedList.size(), actualList.size());
for (Message m : expectedList) {
assertTrue(actualList.remove(m));
}
assertTrue(actualList.isEmpty());
assertTrue(q.isEmpty());
expectedList.clear();
futures.clear();
}
} finally {
executorService.shutdownNow();
}
}
@ParameterizedTest
@MethodSource("capacities")
public void testSingleThreaded(int n) throws IOException {
Queue<Message> referenceQueue = new LinkedList<>();
MessageQueue q = new MessageQueue(n);
int cap = effectiveCapacityOf(n);
Adder adder = new Adder();
for (int i = 0; i < cap; i++) {
Message m = createRandomMessage();
referenceQueue.add(m);
adder.add(q, m);
}
for (int i = 0; i < cap; i++) {
Message expected = referenceQueue.remove();
Message actual = new Remover().removeFrom(q);
assertEquals(expected, actual);
}
assertTrue(q.isEmpty());
}
public static Object[][] capacities() {
return new Object[][]{
new Object[]{ 1},
new Object[]{ 2},
new Object[]{ 3},
new Object[]{ 4},
new Object[]{ 5},
new Object[]{ 6},
new Object[]{ 7},
new Object[]{ 8},
new Object[]{ 9},
new Object[]{128},
new Object[]{256},
};
}
// -- auxiliary test infrastructure --
static class Adder {
@SuppressWarnings("unchecked")
void add(MessageQueue q, Message m) throws IOException {
switch (m.type) {
case TEXT:
q.addText(m.text, m.isLast, m.attachment, m.action, m.future);
break;
case BINARY:
q.addBinary(m.binary, m.isLast, m.attachment, m.action, m.future);
break;
case PING:
q.addPing(m.binary, m.attachment, m.action, m.future);
break;
case PONG:
if (m.binarySupplier != null) {
q.addPong(m.binarySupplier, m.attachment, m.action, m.future);
} else {
q.addPong(m.binary, m.attachment, m.action, m.future);
}
break;
case CLOSE:
q.addClose(m.statusCode, m.text, m.attachment, m.action, m.future);
break;
default:
throw new InternalError();
}
}
}
static class Remover {
Message removeFrom(MessageQueue q) {
Message m = q.peek(new MessageQueue.QueueCallback<>() {
boolean called;
@Override
public <T> Message onText(CharBuffer message,
boolean isLast,
T attachment,
BiConsumer<? super T, ? super Throwable> action,
CompletableFuture<? super T> future) {
assertFalse(called);
called = true;
return new Message(Message.Type.TEXT, null, null, message, isLast,
-1, attachment, action, future);
}
@Override
public <T> Message onBinary(ByteBuffer message,
boolean isLast,
T attachment,
BiConsumer<? super T, ? super Throwable> action,
CompletableFuture<? super T> future) {
assertFalse(called);
called = true;
return new Message(Message.Type.BINARY, null, message, null, isLast,
-1, attachment, action, future);
}
@Override
public <T> Message onPing(ByteBuffer message,
T attachment,
BiConsumer<? super T, ? super Throwable> action,
CompletableFuture<? super T> future) {
assertFalse(called);
called = true;
return new Message(Message.Type.PING, null, message, null, false,
-1, attachment, action, future);
}
@Override
public <T> Message onPong(ByteBuffer message,
T attachment,
BiConsumer<? super T, ? super Throwable> action,
CompletableFuture<? super T> future) {
assertFalse(called);
called = true;
return new Message(Message.Type.PONG, null, message, null, false,
-1, attachment, action, future);
}
@Override
public <T> Message onPong(Supplier<? extends ByteBuffer> message,
T attachment,
BiConsumer<? super T, ? super Throwable> action,
CompletableFuture<? super T> future) {
assertFalse(called);
called = true;
return new Message(Message.Type.PONG, message, null, null, false,
-1, attachment, action, future);
}
@Override
public <T> Message onClose(int statusCode,
CharBuffer reason,
T attachment,
BiConsumer<? super T, ? super Throwable> action,
CompletableFuture<? super T> future) {
assertFalse(called);
called = true;
return new Message(Message.Type.CLOSE, null, null, reason, false,
statusCode, attachment, action, future);
}
@Override
public Message onEmpty() throws RuntimeException {
return null;
}
});
if (m != null) {
q.remove();
}
return m;
}
}
static class Message {
private final Type type;
private final Supplier<? extends ByteBuffer> binarySupplier;
private final ByteBuffer binary;
private final CharBuffer text;
private final boolean isLast;
private final int statusCode;
private final Object attachment;
@SuppressWarnings("rawtypes")
private final BiConsumer action;
@SuppressWarnings("rawtypes")
private final CompletableFuture future;
<T> Message(Type type,
Supplier<? extends ByteBuffer> binarySupplier,
ByteBuffer binary,
CharBuffer text,
boolean isLast,
int statusCode,
T attachment,
BiConsumer<? super T, ? super Throwable> action,
CompletableFuture<? super T> future) {
this.type = type;
this.binarySupplier = binarySupplier;
this.binary = binary;
this.text = text;
this.isLast = isLast;
this.statusCode = statusCode;
this.attachment = attachment;
this.action = action;
this.future = future;
}
@Override
public int hashCode() {
return Objects.hash(type, binarySupplier, binary, text, isLast, statusCode, attachment, action, future);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Message message = (Message) o;
return isLast == message.isLast &&
statusCode == message.statusCode &&
type == message.type &&
Objects.equals(binarySupplier, message.binarySupplier) &&
Objects.equals(binary, message.binary) &&
Objects.equals(text, message.text) &&
Objects.equals(attachment, message.attachment) &&
Objects.equals(action, message.action) &&
Objects.equals(future, message.future);
}
enum Type {
TEXT,
BINARY,
PING,
PONG,
CLOSE
}
}
}

View file

@ -0,0 +1,57 @@
/*
* Copyright (c) 2021, 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.
*/
package jdk.internal.net.http.websocket;
/**
* WebSocket server listener interface, which is the same as the client API
* in java.net.http. See MessageStreamResponder for how listener methods
* can send response messages back to the client
*
* All MessageStreamConsumer methods must be implemented (plus the handler method
* declared here). DefaultMessageStreamHandler provides empty implementations of all
* that can be extended, except for onInit() which must always be implemented.
*
* void onText(CharSequence data, boolean last);
*
* void onBinary(ByteBuffer data, boolean last);
*
* void onPing(ByteBuffer data);
*
* void onPong(ByteBuffer data);
*
* void onClose(int statusCode, CharSequence reason);
*
* void onComplete();
*
* void onError(Throwable e);
*/
interface MessageStreamHandler extends MessageStreamConsumer {
/**
* called before any of the methods above to supply a
* MessageStreamResponder for any new connection, which can be used to send replies
* sendText(), sendBinary(), sendClose() etc
*/
void onInit(MessageStreamResponder responder);
}

View file

@ -0,0 +1,47 @@
/*
* Copyright (c) 2021, 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.
*/
package jdk.internal.net.http.websocket;
import java.io.*;
import java.nio.*;
import java.util.List;
/**
* One of these supplied for each incoming client connection for use
* by user written MessageStreamConsumer.
*/
interface MessageStreamResponder {
public void sendText(CharBuffer src, boolean last) throws IOException;
public void sendBinary(ByteBuffer src, boolean last) throws IOException;
public void sendPing(ByteBuffer src) throws IOException;
public void sendPong(ByteBuffer src) throws IOException;
public void sendClose(int statusCode, CharBuffer reason) throws IOException;
public void close();
}

View file

@ -0,0 +1,272 @@
/*
* Copyright (c) 2016, 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.
*/
package jdk.internal.net.http.websocket;
import jdk.internal.net.http.websocket.Frame.Opcode;
import java.nio.ByteBuffer;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.function.IntPredicate;
import java.util.function.IntUnaryOperator;
import static java.util.OptionalInt.empty;
import static java.util.OptionalInt.of;
import static jdk.internal.net.http.websocket.TestSupport.assertThrows;
import static jdk.internal.net.http.websocket.TestSupport.forEachBufferPartition;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class ReaderTest {
private long cases, frames;
@Test
void notMinimalEncoding01() {
ByteBuffer h = ByteBuffer.allocate(Frame.MAX_HEADER_SIZE_BYTES);
h.put((byte) 0b1000_0000).put((byte) 0b0111_1110).putChar((char) 125).flip();
assertThrows(FailWebSocketException.class,
".*(?i)minimally-encoded.*",
() -> new Frame.Reader().readFrame(h, new MockConsumer()));
}
@Test
void notMinimalEncoding02() {
ByteBuffer h = ByteBuffer.allocate(Frame.MAX_HEADER_SIZE_BYTES);
h.put((byte) 0b1000_0000).put((byte) 0b0111_1111).putLong(125).flip();
assertThrows(FailWebSocketException.class,
".*(?i)minimally-encoded.*",
() -> new Frame.Reader().readFrame(h, new MockConsumer()));
}
@Test
void notMinimalEncoding03() {
ByteBuffer h = ByteBuffer.allocate(Frame.MAX_HEADER_SIZE_BYTES);
h.put((byte) 0b1000_0000).put((byte) 0b0111_1111).putLong(65535).flip();
assertThrows(FailWebSocketException.class,
".*(?i)minimally-encoded.*",
() -> new Frame.Reader().readFrame(h, new MockConsumer()));
}
@Test
public void negativePayload() {
ByteBuffer h = ByteBuffer.allocate(Frame.MAX_HEADER_SIZE_BYTES);
h.put((byte) 0b1000_0000).put((byte) 0b0111_1111).putLong(-2L).flip();
assertThrows(FailWebSocketException.class,
".*(?i)negative.*",
() -> new Frame.Reader().readFrame(h, new MockConsumer()));
}
@Test
public void frameStart() {
final long[] payloads = {0, 126, 65536, Integer.MAX_VALUE + 1L};
final OptionalInt[] masks = {empty(), of(-1), of(0), of(0xCAFEBABE),
of(Integer.MAX_VALUE), of(Integer.MIN_VALUE)};
for (boolean fin : new boolean[]{true, false}) {
for (boolean rsv1 : new boolean[]{true, false}) {
for (boolean rsv2 : new boolean[]{true, false}) {
for (boolean rsv3 : new boolean[]{true, false}) {
for (Opcode opcode : Opcode.values()) {
for (long payloadLen : payloads) {
for (OptionalInt mask : masks) {
verifyFrameStart(fin, rsv1, rsv2, rsv3, opcode, payloadLen, mask);
}
}
}
}
}
}
}
System.out.println("Frames: " + frames + ", Total cases: " + cases);
}
/*
* Tests whether or not the frame starts properly.
* That is, a header and the first invocation of payloadData (if any).
*/
private void verifyFrameStart(boolean fin,
boolean rsv1,
boolean rsv2,
boolean rsv3,
Opcode opcode,
long payloadLen,
OptionalInt mask) {
frames++;
Frame.HeaderWriter w = new Frame.HeaderWriter();
ByteBuffer h = ByteBuffer.allocate(Frame.MAX_HEADER_SIZE_BYTES);
w.fin(fin).rsv1(rsv1).rsv2(rsv2).rsv3(rsv3).opcode(opcode).payloadLen(payloadLen);
mask.ifPresentOrElse(w::mask, w::noMask);
w.write(h);
h.flip();
forEachBufferPartition(h,
buffers -> {
cases++;
Frame.Reader r = new Frame.Reader();
MockConsumer c = new MockConsumer();
for (ByteBuffer b : buffers) {
r.readFrame(b, c);
}
assertEquals(c.fin(), fin);
assertEquals(c.rsv1(), rsv1);
assertEquals(c.rsv2(), rsv2);
assertEquals(c.rsv3(), rsv3);
assertEquals(c.opcode(), opcode);
assertEquals(c.mask(), mask.isPresent());
assertEquals(c.payloadLen(), payloadLen);
assertEquals(c.maskingKey(), mask);
assertEquals(c.isEndFrame(), payloadLen == 0);
});
}
/*
* Used to verify the order, the number of invocations as well as the
* arguments of each individual invocation to Frame.Consumer's methods.
*/
private static class MockConsumer implements Frame.Consumer {
private int invocationOrder;
private Optional<Boolean> fin = Optional.empty();
private Optional<Boolean> rsv1 = Optional.empty();
private Optional<Boolean> rsv2 = Optional.empty();
private Optional<Boolean> rsv3 = Optional.empty();
private Optional<Opcode> opcode = Optional.empty();
private Optional<Boolean> mask = Optional.empty();
private OptionalLong payloadLen = OptionalLong.empty();
private OptionalInt maskingKey = OptionalInt.empty();
@Override
public void fin(boolean value) {
checkAndSetOrder(0, 1);
fin = Optional.of(value);
}
@Override
public void rsv1(boolean value) {
checkAndSetOrder(1, 2);
rsv1 = Optional.of(value);
}
@Override
public void rsv2(boolean value) {
checkAndSetOrder(2, 3);
rsv2 = Optional.of(value);
}
@Override
public void rsv3(boolean value) {
checkAndSetOrder(3, 4);
rsv3 = Optional.of(value);
}
@Override
public void opcode(Opcode value) {
checkAndSetOrder(4, 5);
opcode = Optional.of(value);
}
@Override
public void mask(boolean value) {
checkAndSetOrder(5, 6);
mask = Optional.of(value);
}
@Override
public void payloadLen(long value) {
checkAndSetOrder(p -> p == 5 || p == 6, n -> 7);
payloadLen = OptionalLong.of(value);
}
@Override
public void maskingKey(int value) {
checkAndSetOrder(7, 8);
maskingKey = of(value);
}
@Override
public void payloadData(ByteBuffer data) {
checkAndSetOrder(p -> p == 7 || p == 8, n -> 9);
assert payloadLen.isPresent();
if (payloadLen.getAsLong() != 0 && !data.hasRemaining()) {
throw new TestSupport.AssertionFailedException("Artefact of reading");
}
}
@Override
public void endFrame() {
checkAndSetOrder(9, 10);
}
boolean isEndFrame() {
return invocationOrder == 10;
}
public boolean fin() {
return fin.get();
}
public boolean rsv1() {
return rsv1.get();
}
public boolean rsv2() {
return rsv2.get();
}
public boolean rsv3() {
return rsv3.get();
}
public Opcode opcode() {
return opcode.get();
}
public boolean mask() {
return mask.get();
}
public long payloadLen() {
return payloadLen.getAsLong();
}
public OptionalInt maskingKey() {
return maskingKey;
}
private void checkAndSetOrder(int expectedValue, int newValue) {
checkAndSetOrder(p -> p == expectedValue, n -> newValue);
}
private void checkAndSetOrder(IntPredicate expectedValue,
IntUnaryOperator newValue) {
if (!expectedValue.test(invocationOrder)) {
throw new TestSupport.AssertionFailedException(
expectedValue + " -> " + newValue);
}
invocationOrder = newValue.applyAsInt(invocationOrder);
}
}
}

View file

@ -0,0 +1,324 @@
/*
* Copyright (c) 2016, 2018, 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.
*/
package jdk.internal.net.http.websocket;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Stack;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import static java.util.List.of;
import static java.util.Objects.requireNonNull;
/*
* Auxiliary test infrastructure
*/
final class TestSupport {
private TestSupport() { }
static <A, B, R> Iterator<R> cartesianIterator(List<A> a,
List<B> b,
F2<A, B, R> f2) {
@SuppressWarnings("unchecked")
F<R> t = p -> f2.apply((A) p[0], (B) p[1]);
return cartesianIterator(of(a, b), t);
}
static <A, B, C, R> Iterator<R> cartesianIterator(List<A> a,
List<B> b,
List<C> c,
F3<A, B, C, R> f3) {
@SuppressWarnings("unchecked")
F<R> t = p -> f3.apply((A) p[0], (B) p[1], (C) p[2]);
return cartesianIterator(of(a, b, c), t);
}
static <A, B, C, D, R> Iterator<R> cartesianIterator(List<A> a,
List<B> b,
List<C> c,
List<D> d,
F4<A, B, C, D, R> f4) {
@SuppressWarnings("unchecked")
F<R> t = p -> f4.apply((A) p[0], (B) p[1], (C) p[2], (D) p[3]);
return cartesianIterator(of(a, b, c, d), t);
}
static <A, B, C, D, E, R> Iterator<R> cartesianIterator(List<A> a,
List<B> b,
List<C> c,
List<D> d,
List<E> e,
F5<A, B, C, D, E, R> f5) {
@SuppressWarnings("unchecked")
F<R> t = p -> f5.apply((A) p[0], (B) p[1], (C) p[2], (D) p[3], (E) p[4]);
return cartesianIterator(of(a, b, c, d, e), t);
}
static <R> Iterator<R> cartesianIterator(List<? extends List<?>> params,
F<R> function) {
if (params.isEmpty()) {
return Collections.emptyIterator();
}
for (List<?> l : params) {
if (l.isEmpty()) {
return Collections.emptyIterator();
}
}
// Assertion: if we are still here, there is at least a single element
// in the product
return new Iterator<>() {
private final int arity = params.size();
private final int[] coordinates = new int[arity];
private boolean hasNext = true;
@Override
public boolean hasNext() {
return hasNext;
}
@Override
public R next() {
if (!hasNext) {
throw new NoSuchElementException();
}
Object[] array = new Object[arity];
for (int i = 0; i < arity; i++) {
array[i] = params.get(i).get(coordinates[i]);
}
int p = arity - 1;
while (p >= 0 && coordinates[p] == params.get(p).size() - 1) {
p--;
}
if (p < 0) {
hasNext = false;
} else {
coordinates[p]++;
for (int i = p + 1; i < arity; i++) {
coordinates[i] = 0;
}
}
return function.apply(array);
}
};
}
@FunctionalInterface
public interface F1<A, R> {
R apply(A a);
}
@FunctionalInterface
public interface F2<A, B, R> {
R apply(A a, B b);
}
@FunctionalInterface
public interface F3<A, B, C, R> {
R apply(A a, B b, C c);
}
@FunctionalInterface
public interface F4<A, B, C, D, R> {
R apply(A a, B b, C c, D d);
}
@FunctionalInterface
public interface F5<A, B, C, D, E, R> {
R apply(A a, B b, C c, D d, E e);
}
@FunctionalInterface
public interface F<R> {
R apply(Object[] args);
}
static <T> Iterator<T> iteratorOf1(T element) {
return List.of(element).iterator();
}
@SafeVarargs
static <T> Iterator<T> iteratorOf(T... elements) {
return List.of(elements).iterator();
}
static <T> Iterator<T> limit(int maxElements, Iterator<? extends T> elements) {
return new Iterator<>() {
int count = maxElements;
@Override
public boolean hasNext() {
return count > 0 && elements.hasNext();
}
@Override
public T next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
count--;
return elements.next();
}
};
}
static ByteBuffer fullCopy(ByteBuffer src) {
ByteBuffer copy = ByteBuffer.allocate(src.capacity());
int p = src.position();
int l = src.limit();
src.clear();
copy.put(src).position(p).limit(l);
src.position(p).limit(l);
return copy;
}
static void forEachBufferPartition(ByteBuffer src,
Consumer<? super Iterable<? extends ByteBuffer>> action) {
forEachPartition(src.remaining(),
(lengths) -> {
int end = src.position();
List<ByteBuffer> buffers = new LinkedList<>();
for (int len : lengths) {
ByteBuffer d = src.duplicate();
d.position(end);
d.limit(end + len);
end += len;
buffers.add(d);
}
action.accept(buffers);
});
}
private static void forEachPartition(int n,
Consumer<? super Iterable<Integer>> action) {
forEachPartition(n, new Stack<>(), action);
}
private static void forEachPartition(int n,
Stack<Integer> path,
Consumer<? super Iterable<Integer>> action) {
if (n == 0) {
action.accept(path);
} else {
for (int i = 1; i <= n; i++) {
path.push(i);
forEachPartition(n - i, path, action);
path.pop();
}
}
}
static void forEachPermutation(int n, Consumer<? super int[]> c) {
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = i;
}
permutations(0, a, c);
}
private static void permutations(int i, int[] a, Consumer<? super int[]> c) {
if (i == a.length) {
c.accept(Arrays.copyOf(a, a.length));
return;
}
for (int j = i; j < a.length; j++) {
swap(a, i, j);
permutations(i + 1, a, c);
swap(a, i, j);
}
}
private static void swap(int[] a, int i, int j) {
int x = a[i];
a[i] = a[j];
a[j] = x;
}
public static <T extends Throwable> T assertThrows(Class<? extends T> clazz,
ThrowingProcedure code) {
@SuppressWarnings("unchecked")
T t = (T) assertThrows(clazz::isInstance, code);
return t;
}
/*
* The rationale behind asking for a regex is to not pollute variable names
* space in the scope of assertion: if it's something as simple as checking
* a message, we can do it inside
*/
@SuppressWarnings("unchecked")
static <T extends Throwable> T assertThrows(Class<? extends T> clazz,
String messageRegex,
ThrowingProcedure code) {
requireNonNull(messageRegex, "messagePattern");
Predicate<Throwable> p = e -> clazz.isInstance(e)
&& Pattern.matches(messageRegex, e.getMessage());
return (T) assertThrows(p, code);
}
static Throwable assertThrows(Predicate<? super Throwable> predicate,
ThrowingProcedure code) {
requireNonNull(predicate, "predicate");
requireNonNull(code, "code");
Throwable caught = null;
try {
code.run();
} catch (Throwable t) {
caught = t;
}
if (caught == null) {
throw new AssertionFailedException("No exception was thrown");
}
if (predicate.test(caught)) {
System.out.println("Got expected exception: " + caught);
return caught;
}
throw new AssertionFailedException("Caught exception didn't match the predicate", caught);
}
interface ThrowingProcedure {
void run() throws Throwable;
}
static final class AssertionFailedException extends RuntimeException {
private static final long serialVersionUID = 1L;
AssertionFailedException(String message) {
super(message);
}
AssertionFailedException(String message, Throwable cause) {
super(message, cause);
}
}
}

View file

@ -0,0 +1,113 @@
package jdk.internal.net.http.websocket;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.WebSocket;
import java.util.Optional;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
/**
* This is the client side of the test invoked from WebSocketAndHttpTest:
*
* The two args are the addresses of a (local) Websocket and Http server
*
* The test first sends a request to the WS server and in the listener
* which handles the response, it tries to send a request to the http
* server. This hangs if the listener was invoked from the selector
* manager thread. If invoked from a different thread then the http
* response is received and the response string is mapped to string
* "succeeded"
*/
public class WebSocketAndHttpClient {
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
HttpClient httpClient = HttpClient.newBuilder().executor(executorService).build();
WebSocketTest wsTest = new WebSocketTest(httpClient, args[0]);
HttpTest httpTest = new HttpTest(httpClient, args[1]);
final CompletableFuture<String> result = new CompletableFuture<>();
wsTest.listen(message -> {
try {
String r = httpTest.getData(message);
result.complete(r);
} catch (Exception e) {
result.completeExceptionally(e);
}
});
wsTest.sendData("TEST_DATA");
System.out.println("Wait for result");
try {
result.join();
System.out.println("Result: success");
} finally {
executorService.shutdownNow();
}
}
static class WebSocketTest {
final HttpClient httpClient;
final String server;
volatile WebSocket webSocket;
WebSocketTest(HttpClient httpClient, String server) {
this.httpClient = httpClient;
this.server = server;
}
public void listen(Consumer<String> consumer) {
URI uri = URI.create(server);
System.out.println("WS API client - Connecting to " + uri.toString());
CompletableFuture<WebSocket> cf = httpClient.newWebSocketBuilder()
.buildAsync(uri, new WebSocket.Listener() {
@Override
public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
System.out.println("WS API client - received data: " + data);
consumer.accept(data.toString());
return null;
}
public void onError(WebSocket webSocket, Throwable error) {
System.out.println("WS API client - error");
error.printStackTrace();
}
});
System.out.println("CF created");
webSocket = cf.join();
System.out.println("Websocket created");
}
void sendData(String data) {
System.out.println("WS API client - sending data via WebSocket: {}" + data);
webSocket.sendText(data, true).join();
}
}
static class HttpTest {
final HttpClient httpClient;
final String baseUrl;
HttpTest(HttpClient httpClient, String baseUrl) {
this.httpClient = httpClient;
this.baseUrl = baseUrl;
}
private String getData(String data) throws Exception {
URI uri = URI.create(baseUrl + "?param=" + data);
HttpRequest request = HttpRequest.newBuilder().GET().uri(uri).build();
System.out.println("Http API Client - send HTTP GET request with parameter {}" + data);
HttpResponse<String> send = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
return send.body();
}
}
}

View file

@ -0,0 +1,75 @@
/*
* Copyright (c) 2016, 2018, 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.
*/
package jdk.internal.net.http.websocket;
import java.net.*;
import java.nio.CharBuffer;
import java.io.*;
import com.sun.net.httpserver.*;
public class WebSocketAndHttpTest {
static class WHandler extends DefaultMessageStreamHandler {
volatile MessageStreamResponder responder;
public void onText(CharSequence data, boolean last) {
System.out.println("onText: " + data);
System.out.println("onText: " + Thread.currentThread());
try {
responder.sendText(CharBuffer.wrap(data), true);
System.out.println("onText: send ok");
} catch (IOException e) {
System.out.println("onText: " + e);
throw new UncheckedIOException(e);
}
}
public void onInit(MessageStreamResponder responder) {
System.out.println("onInit");
this.responder = responder;
}
}
static HttpHandler httpHandler = (ex) -> ex.sendResponseHeaders(200, -1);
public static void main(String[] args) throws Exception {
HttpServer hserver = null;
try {
WebSocketServer server = new WebSocketServer(new WHandler());
server.open();
URI uri = server.getURI();
hserver = HttpServer.create(new InetSocketAddress(0), 4);
hserver.createContext("/", httpHandler);
hserver.start();
int port = hserver.getAddress().getPort();
URI huri = new URI("http://127.0.0.1:" + port + "/foo");
WebSocketAndHttpClient.main(new String[]{uri.toString(), huri.toString()});
} finally {
hserver.stop(0);
}
}
}

View file

@ -0,0 +1,186 @@
/*
* Copyright (c) 2021, 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.
*/
package jdk.internal.net.http.websocket;
import java.util.LinkedList;
import java.util.List;
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
public class WebSocketResponder implements MessageStreamResponder {
final MessageStreamConsumer consumer;
final LinkedList<ByteBuffer> queue;
volatile boolean closed = false;
final MessageEncoder encoder;
final MessageDecoder decoder;
static final int BUF_SIZE = 1024;
public WebSocketResponder(MessageStreamConsumer consumer) {
this.consumer = consumer;
this.queue = new LinkedList<>();
this.decoder = new MessageDecoder(consumer, true);
this.encoder = new MessageEncoder(true);
}
// own thread
public void readLoop(SocketChannel chan) throws IOException {
chan.configureBlocking(true);
boolean eof = false;
ByteBuffer buf = ByteBuffer.allocate(8 * 1024);
Frame.Reader reader = new Frame.Reader();
try {
while (!eof) {
int count;
buf.clear();
eof = ((count=chan.read(buf)) == -1);
if (!eof) {
buf.flip();
reader.readFrame(buf, decoder);
}
}
} catch (IOException e) {
if (!closed)
throw e;
}
}
// own thread
public void writeLoop(SocketChannel chan) throws IOException {
// read queue and send data
while (true) {
ByteBuffer buf;
synchronized(queue) {
while (queue.isEmpty()) {
try {
queue.wait();
} catch (InterruptedException e) {
throw new IOException(e);
}
if (queue.isEmpty() && closed) {
chan.close();
return;
}
}
buf = queue.remove(0);
}
chan.write(buf);
}
}
/**
* Public methods below used y MessageStreamHandler to send replies
* to client.
*/
@Override
public void sendText(CharBuffer src, boolean last) throws IOException {
ByteBuffer buf = ByteBuffer.allocate(BUF_SIZE);
LinkedList<ByteBuffer> bufs = new LinkedList<>();
boolean done = false;
do {
buf.clear();
done = encoder.encodeText(src, last, buf);
buf.flip();
bufs.add(buf);
} while (!done);
sendMessage(bufs);
}
@Override
public void sendBinary(ByteBuffer src, boolean last) throws IOException {
ByteBuffer buf = ByteBuffer.allocate(BUF_SIZE);
LinkedList<ByteBuffer> bufs = new LinkedList<>();
boolean done = false;
do {
buf.clear();
done = encoder.encodeBinary(src, last, buf);
buf.flip();
bufs.add(buf);
} while (!done);
sendMessage(bufs);
}
@Override
public void sendPing(ByteBuffer src) throws IOException {
ByteBuffer buf = ByteBuffer.allocate(BUF_SIZE);
LinkedList<ByteBuffer> bufs = new LinkedList<>();
boolean done = false;
do {
buf.clear();
done = encoder.encodePing(src, buf);
buf.flip();
bufs.add(buf);
} while (!done);
sendMessage(bufs);
}
@Override
public void sendPong(ByteBuffer src) throws IOException {
ByteBuffer buf = ByteBuffer.allocate(BUF_SIZE);
LinkedList<ByteBuffer> bufs = new LinkedList<>();
boolean done = false;
do {
buf.clear();
done = encoder.encodePong(src, buf);
buf.flip();
bufs.add(buf);
} while (!done);
sendMessage(bufs);
}
@Override
public void sendClose(int statusCode, CharBuffer reason) throws IOException {
ByteBuffer buf = ByteBuffer.allocate(BUF_SIZE);
LinkedList<ByteBuffer> bufs = new LinkedList<>();
boolean done = false;
do {
buf.clear();
done = encoder.encodeClose(statusCode, reason, buf);
buf.flip();
bufs.add(buf);
} while (!done);
sendMessage(bufs);
close();
}
private void sendMessage(List<ByteBuffer> bufs) throws IOException {
if (closed)
throw new IOException("closed");
synchronized(queue) {
queue.addAll(bufs);
queue.notify();
}
}
@Override
public void close() {
synchronized(queue) {
closed = true;
queue.notify();
}
}
}

View file

@ -0,0 +1,435 @@
/*
* Copyright (c) 2016, 2019, 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.
*/
package jdk.internal.net.http.websocket;
import java.io.Closeable;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.CharacterCodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiFunction;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static java.lang.String.format;
import static java.lang.System.err;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Arrays.asList;
import static java.util.Objects.requireNonNull;
/**
* WebSocket Server. This is a copy of the DummyWebSocketServer test class
* but which also supports sending and receiving of websocket messages
* using a simple API once the connection has been established
*
* MessageStreamHandler is the "listener" API to be implemented for handling
* incoming messages. MessageStreamResponder is used by that handler to send
* responses back to the client.
*
* Performs simpler version of the WebSocket Opening Handshake over HTTP (i.e.
* no proxying, cookies, etc.) Supports sequential connections, one at a time,
* i.e. in order for a client to connect to the server the previous client must
* disconnect first.
*
* Expected client request:
*
* GET /chat HTTP/1.1
* Host: server.example.com
* Upgrade: websocket
* Connection: Upgrade
* Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
* Origin: http://example.com
* Sec-WebSocket-Protocol: chat, superchat
* Sec-WebSocket-Version: 13
*
* This server response:
*
* HTTP/1.1 101 Switching Protocols
* Upgrade: websocket
* Connection: Upgrade
* Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
* Sec-WebSocket-Protocol: chat
*/
public class WebSocketServer implements Closeable {
private final AtomicBoolean started = new AtomicBoolean();
private final Thread thread;
private volatile ServerSocketChannel ssc;
private volatile InetSocketAddress address;
private ByteBuffer read = ByteBuffer.allocate(16384);
private final CountDownLatch readReady = new CountDownLatch(1);
private final MessageStreamHandler handler;
private final WebSocketResponder responder;
private volatile int receiveBufferSize;
private static class Credentials {
private final String name;
private final String password;
private Credentials(String name, String password) {
this.name = name;
this.password = password;
}
public String name() { return name; }
public String password() { return password; }
}
public WebSocketServer(MessageStreamHandler handler) {
this(handler, defaultMapping(), null, null);
}
public WebSocketServer() {
this(null, defaultMapping(), null, null);
}
public WebSocketServer(String username, String password) {
this(null, defaultMapping(), username, password);
}
public WebSocketServer(MessageStreamHandler handler,
BiFunction<List<String>,Credentials,List<String>> mapping,
String username, String password) {
requireNonNull(mapping);
this.handler = handler;
if (handler == null) {
this.responder = null;
} else {
this.responder = new WebSocketResponder(handler);
handler.onInit(this.responder);
}
Credentials credentials = username != null ?
new Credentials(username, password) : null;
thread = new Thread(() -> {
try {
while (!Thread.currentThread().isInterrupted()) {
err.println("Accepting next connection at: " + ssc);
SocketChannel channel = ssc.accept();
err.println("Accepted: " + channel);
try {
channel.setOption(StandardSocketOptions.TCP_NODELAY, true);
channel.configureBlocking(true);
while (true) {
StringBuilder request = new StringBuilder();
if (!readRequest(channel, request)) {
throw new IOException("Bad request:[" + request + "]");
}
List<String> strings = asList(request.toString().split("\r\n"));
List<String> response = mapping.apply(strings, credentials);
writeResponse(channel, response);
if (response.get(0).startsWith("HTTP/1.1 401")) {
err.println("Sent 401 Authentication response " + channel);
continue;
} else {
serve(channel);
break;
}
}
} catch (IOException e) {
err.println("Error in connection: " + channel + ", " + e);
} finally {
err.println("Closed: " + channel);
close(channel);
readReady.countDown();
}
}
} catch (ClosedByInterruptException ignored) {
} catch (Exception e) {
e.printStackTrace(err);
} finally {
close(ssc);
err.println("Stopped at: " + getURI());
}
});
thread.setName("WebSocketServer");
thread.setDaemon(false);
}
// runs in own thread. Override to implement different behavior
protected void read(SocketChannel ch) throws IOException {
responder.readLoop(ch);
}
// runs in own thread. Override to implement different behavior
protected void write(SocketChannel ch) throws IOException {
responder.writeLoop(ch);
}
protected final void serve(SocketChannel channel)
throws InterruptedException
{
Thread reader = new Thread(() -> {
try {
read(channel);
} catch (IOException ignored) { }
});
Thread writer = new Thread(() -> {
try {
write(channel);
} catch (IOException ignored) { }
});
reader.start();
writer.start();
try {
reader.join();
} finally {
reader.interrupt();
try {
writer.join();
} finally {
writer.interrupt();
}
}
}
public ByteBuffer read() throws InterruptedException {
readReady.await();
return read.duplicate().asReadOnlyBuffer().flip();
}
public void setReceiveBufferSize(int bufsize) {
assert ssc == null : "Must configure before calling open()";
this.receiveBufferSize = bufsize;
}
public void open() throws IOException {
err.println("Starting");
if (!started.compareAndSet(false, true)) {
throw new IllegalStateException("Already started");
}
ssc = ServerSocketChannel.open();
try {
ssc.configureBlocking(true);
var bufsize = receiveBufferSize;
if (bufsize > 0) {
err.printf("Configuring receive buffer size to %d%n", bufsize);
try {
ssc.setOption(StandardSocketOptions.SO_RCVBUF, bufsize);
} catch (IOException x) {
err.printf("Failed to configure receive buffer size to %d%n", bufsize);
}
}
ssc.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0));
address = (InetSocketAddress) ssc.getLocalAddress();
thread.start();
} catch (IOException e) {
close(ssc);
throw e;
}
err.println("Started at: " + getURI());
}
@Override
public void close() {
err.println("Stopping: " + getURI());
thread.interrupt();
close(ssc);
}
URI getURI() {
if (!started.get()) {
throw new IllegalStateException("Not yet started");
}
return URI.create("ws://localhost:" + address.getPort());
}
private boolean readRequest(SocketChannel channel, StringBuilder request)
throws IOException
{
ByteBuffer buffer = ByteBuffer.allocate(512);
while (channel.read(buffer) != -1) {
// read the complete HTTP request headers, there should be no body
CharBuffer decoded;
buffer.flip();
try {
decoded = ISO_8859_1.newDecoder().decode(buffer);
} catch (CharacterCodingException e) {
throw new UncheckedIOException(e);
}
request.append(decoded);
if (Pattern.compile("\r\n\r\n").matcher(request).find())
return true;
buffer.clear();
}
return false;
}
private void writeResponse(SocketChannel channel, List<String> response)
throws IOException
{
String s = response.stream().collect(Collectors.joining("\r\n"))
+ "\r\n\r\n";
ByteBuffer encoded;
try {
encoded = ISO_8859_1.newEncoder().encode(CharBuffer.wrap(s));
} catch (CharacterCodingException e) {
throw new UncheckedIOException(e);
}
while (encoded.hasRemaining()) {
channel.write(encoded);
}
}
private static BiFunction<List<String>,Credentials,List<String>> defaultMapping() {
return (request, credentials) -> {
List<String> response = new LinkedList<>();
Iterator<String> iterator = request.iterator();
if (!iterator.hasNext()) {
throw new IllegalStateException("The request is empty");
}
String statusLine = iterator.next();
if (!(statusLine.startsWith("GET /") && statusLine.endsWith(" HTTP/1.1"))) {
throw new IllegalStateException
("Unexpected status line: " + request.get(0));
}
response.add("HTTP/1.1 101 Switching Protocols");
Map<String, List<String>> requestHeaders = new HashMap<>();
while (iterator.hasNext()) {
String header = iterator.next();
String[] split = header.split(": ");
if (split.length != 2) {
throw new IllegalStateException
("Unexpected header: " + header
+ ", split=" + Arrays.toString(split));
}
requestHeaders.computeIfAbsent(split[0], k -> new ArrayList<>()).add(split[1]);
}
if (requestHeaders.containsKey("Sec-WebSocket-Protocol")) {
throw new IllegalStateException("Subprotocols are not expected");
}
if (requestHeaders.containsKey("Sec-WebSocket-Extensions")) {
throw new IllegalStateException("Extensions are not expected");
}
expectHeader(requestHeaders, "Connection", "Upgrade");
response.add("Connection: Upgrade");
expectHeader(requestHeaders, "Upgrade", "websocket");
response.add("Upgrade: websocket");
expectHeader(requestHeaders, "Sec-WebSocket-Version", "13");
List<String> key = requestHeaders.get("Sec-WebSocket-Key");
if (key == null || key.isEmpty()) {
throw new IllegalStateException("Sec-WebSocket-Key is missing");
}
if (key.size() != 1) {
throw new IllegalStateException("Sec-WebSocket-Key has too many values : " + key);
}
MessageDigest sha1 = null;
try {
sha1 = MessageDigest.getInstance("SHA-1");
} catch (NoSuchAlgorithmException e) {
throw new InternalError(e);
}
String x = key.get(0) + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
sha1.update(x.getBytes(ISO_8859_1));
String v = Base64.getEncoder().encodeToString(sha1.digest());
response.add("Sec-WebSocket-Accept: " + v);
// check authorization credentials, if required by the server
if (credentials != null && !authorized(credentials, requestHeaders)) {
response.clear();
response.add("HTTP/1.1 401 Unauthorized");
response.add("Content-Length: 0");
response.add("WWW-Authenticate: Basic realm=\"dummy server realm\"");
}
return response;
};
}
// Checks credentials in the request against those allowable by the server.
private static boolean authorized(Credentials credentials,
Map<String,List<String>> requestHeaders) {
List<String> authorization = requestHeaders.get("Authorization");
if (authorization == null)
return false;
if (authorization.size() != 1) {
throw new IllegalStateException("Authorization unexpected count:" + authorization);
}
String header = authorization.get(0);
if (!header.startsWith("Basic "))
throw new IllegalStateException("Authorization not Basic: " + header);
header = header.substring("Basic ".length());
String values = new String(Base64.getDecoder().decode(header), UTF_8);
int sep = values.indexOf(':');
if (sep < 1) {
throw new IllegalStateException("Authorization not colon: " + values);
}
String name = values.substring(0, sep);
String password = values.substring(sep + 1);
if (name.equals(credentials.name()) && password.equals(credentials.password()))
return true;
return false;
}
protected static String expectHeader(Map<String, List<String>> headers,
String name,
String value) {
List<String> v = headers.get(name);
if (v == null) {
throw new IllegalStateException(
format("Expected '%s' header, not present in %s",
name, headers));
}
if (!v.contains(value)) {
throw new IllegalStateException(
format("Expected '%s: %s', actual: '%s: %s'",
name, value, name, v)
);
}
return value;
}
private static void close(AutoCloseable... acs) {
for (AutoCloseable ac : acs) {
try {
ac.close();
} catch (Exception ignored) { }
}
}
}

View file

@ -0,0 +1,5 @@
handlers=java.util.logging.ConsoleHandler
.level=ALL
java.util.logging.ConsoleHandler.level=ALL
java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n

View file

@ -0,0 +1,197 @@
/*
* Copyright (c) 2017, 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.
*/
/*
* @test
* @summary Basic sanity checks for WebSocket URI from the Builder
* @compile ../DummyWebSocketServer.java ../../ProxyServer.java
* @run junit/othervm WSSanityTest
*/
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.SocketAddress;
import java.net.URI;
import java.util.List;
import java.net.http.HttpClient;
import java.net.http.WebSocket;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static org.junit.jupiter.api.Assertions.*;
public class WSSanityTest {
private static URI wsURI;
private static DummyWebSocketServer webSocketServer;
private static InetSocketAddress proxyAddress;
@BeforeAll
public static void setup() throws Exception {
ProxyServer proxyServer = new ProxyServer(0, true);
proxyAddress = new InetSocketAddress(InetAddress.getLoopbackAddress(),
proxyServer.getPort());
webSocketServer = new DummyWebSocketServer();
webSocketServer.open();
wsURI = webSocketServer.getURI();
System.out.println("Proxy Server: " + proxyAddress);
System.out.println("DummyWebSocketServer: " + wsURI);
}
@AfterAll
public static void teardown() {
webSocketServer.close();
}
static class NoOpListener implements WebSocket.Listener {}
static final WebSocket.Listener noOpListener = new NoOpListener();
interface ExceptionAction<T> {
T run() throws Exception;
}
public static Object[][] passingScenarios() {
HttpClient noProxyClient = HttpClient.newHttpClient();
return new Object[][]{
{ (ExceptionAction<?>)() -> {
noProxyClient.newWebSocketBuilder()
.buildAsync(wsURI, noOpListener).get().abort();
return null; },
"0" /* for log file identification */ },
{ (ExceptionAction<?>)() -> {
URI uriWithPath = wsURI.resolve("/path/x");
noProxyClient.newWebSocketBuilder()
.buildAsync(uriWithPath, noOpListener).get().abort();
return null; },
"1" },
{ (ExceptionAction<?>)() -> {
noProxyClient.newWebSocketBuilder()
.header("A-Header", "A-Value") // header
.buildAsync(wsURI, noOpListener).get().abort();
return null; },
"2" },
{ (ExceptionAction<?>)() -> {
noProxyClient.newWebSocketBuilder()
.header("A-Header", "A-Value") // headers
.header("B-Header", "B-Value") // headers
.buildAsync(wsURI, noOpListener).get().abort();
return null; },
"3" },
// client with a DIRECT proxy
{ (ExceptionAction<?>)() -> {
ProxySelector ps = ProxySelector.of(null);
HttpClient client = HttpClient.newBuilder().proxy(ps).build();
client.newWebSocketBuilder()
.buildAsync(wsURI, noOpListener).get().abort();
return null; },
"4" },
// client with a SOCKS proxy! ( expect implementation to ignore SOCKS )
{ (ExceptionAction<?>)() -> {
ProxySelector ps = new ProxySelector() {
@Override public List<Proxy> select(URI uri) {
return List.of(new Proxy(Proxy.Type.SOCKS, proxyAddress)); }
@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) { }
};
HttpClient client = HttpClient.newBuilder().proxy(ps).build();
client.newWebSocketBuilder()
.buildAsync(wsURI, noOpListener).get().abort();
return null; },
"5" },
// client with an HTTP/HTTPS proxy
{ (ExceptionAction<?>)() -> {
assert proxyAddress != null;
ProxySelector ps = ProxySelector.of(proxyAddress);
HttpClient client = HttpClient.newBuilder().proxy(ps).build();
client.newWebSocketBuilder()
.buildAsync(wsURI, noOpListener).get().abort();
return null; },
"6" },
{ (ExceptionAction<?>)() -> {
assert proxyAddress != null;
CountingProxySelector ps = CountingProxySelector.of(proxyAddress);
HttpClient client = HttpClient.newBuilder().proxy(ps).build();
client.newWebSocketBuilder()
.buildAsync(wsURI, noOpListener).get().abort();
assertEquals(1, ps.count()); // ps.select only invoked once
return null; },
"7" },
};
}
@ParameterizedTest
@MethodSource("passingScenarios")
public void testScenarios(ExceptionAction<?> action, String dataProviderId)
throws Exception
{
action.run();
}
/**
* A Proxy Selector that wraps a ProxySelector.of(), and counts the number
* of times its select method has been invoked. This can be used to ensure
* that the Proxy Selector is invoked only once per WebSocket.Builder::buildAsync
* invocation.
*/
static class CountingProxySelector extends ProxySelector {
private final ProxySelector proxySelector;
private volatile int count; // 0
private CountingProxySelector(InetSocketAddress proxyAddress) {
proxySelector = ProxySelector.of(proxyAddress);
}
public static CountingProxySelector of(InetSocketAddress proxyAddress) {
return new CountingProxySelector(proxyAddress);
}
int count() { return count; }
@Override
public List<Proxy> select(URI uri) {
System.out.println("PS: uri");
Throwable t = new Throwable();
t.printStackTrace(System.out);
count++;
return proxySelector.select(uri);
}
@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
proxySelector.connectFailed(uri, sa, ioe);
}
}
}