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:
commit
0a580b313d
70422 changed files with 17213626 additions and 0 deletions
448
test/jdk/java/net/httpclient/websocket/Abort.java
Normal file
448
test/jdk/java/net/httpclient/websocket/Abort.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
231
test/jdk/java/net/httpclient/websocket/AutomaticPong.java
Normal file
231
test/jdk/java/net/httpclient/websocket/AutomaticPong.java
Normal 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}};
|
||||
}
|
||||
}
|
||||
136
test/jdk/java/net/httpclient/websocket/BlowupOutputQueue.java
Normal file
136
test/jdk/java/net/httpclient/websocket/BlowupOutputQueue.java
Normal 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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
521
test/jdk/java/net/httpclient/websocket/DummyWebSocketServer.java
Normal file
521
test/jdk/java/net/httpclient/websocket/DummyWebSocketServer.java
Normal 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) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 { }
|
||||
32
test/jdk/java/net/httpclient/websocket/MaskerDriver.java
Normal file
32
test/jdk/java/net/httpclient/websocket/MaskerDriver.java
Normal 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 { }
|
||||
|
|
@ -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 { }
|
||||
490
test/jdk/java/net/httpclient/websocket/MockListener.java
Normal file
490
test/jdk/java/net/httpclient/websocket/MockListener.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
142
test/jdk/java/net/httpclient/websocket/PendingOperations.java
Normal file
142
test/jdk/java/net/httpclient/websocket/PendingOperations.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
109
test/jdk/java/net/httpclient/websocket/PendingPingTextClose.java
Normal file
109
test/jdk/java/net/httpclient/websocket/PendingPingTextClose.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
32
test/jdk/java/net/httpclient/websocket/ReaderDriver.java
Normal file
32
test/jdk/java/net/httpclient/websocket/ReaderDriver.java
Normal 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 { }
|
||||
163
test/jdk/java/net/httpclient/websocket/SecureSupport.java
Normal file
163
test/jdk/java/net/httpclient/websocket/SecureSupport.java
Normal 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);
|
||||
}
|
||||
}
|
||||
100
test/jdk/java/net/httpclient/websocket/SendTest.java
Normal file
100
test/jdk/java/net/httpclient/websocket/SendTest.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
266
test/jdk/java/net/httpclient/websocket/Support.java
Normal file
266
test/jdk/java/net/httpclient/websocket/Support.java
Normal 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);
|
||||
}
|
||||
}
|
||||
5
test/jdk/java/net/httpclient/websocket/TEST.properties
Normal file
5
test/jdk/java/net/httpclient/websocket/TEST.properties
Normal 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
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
236
test/jdk/java/net/httpclient/websocket/WebSocketBuilderTest.java
Normal file
236
test/jdk/java/net/httpclient/websocket/WebSocketBuilderTest.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
409
test/jdk/java/net/httpclient/websocket/WebSocketProxyTest.java
Normal file
409
test/jdk/java/net/httpclient/websocket/WebSocketProxyTest.java
Normal 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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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 { }
|
||||
901
test/jdk/java/net/httpclient/websocket/WebSocketTest.java
Normal file
901
test/jdk/java/net/httpclient/websocket/WebSocketTest.java
Normal 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) { }
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue