Skip to content

Commit 9228f3e

Browse files
Copilotrbri
andauthored
Replace Jetty WebSocket adapter with JDK HttpClient-based implementation
- Add JdkWebSocketAdapter using java.net.http.HttpClient and java.net.http.WebSocket - Remove JettyWebSocketAdapter and htmlunit-websocket-client dependency - Update WebClient to default to JdkWebSocketAdapterFactory - Update module-info.java to require java.net.http instead of htmlunit.websocket.client - Update ArchitectureTest to remove Jetty-specific exclusions Agent-Logs-Url: https://github.com/HtmlUnit/htmlunit/sessions/b3db8ccb-ee0a-436b-8c39-85fd25db4747 Co-authored-by: rbri <2544132+rbri@users.noreply.github.com>
1 parent b929959 commit 9228f3e

7 files changed

Lines changed: 328 additions & 261 deletions

File tree

javac.20260412_170050.args

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-d
2+
/tmp/test-compile
3+
-sourcepath
4+
src/main/java
5+
--module-path
6+
7+
-cp
8+
9+
src/main/java/org/htmlunit/websocket/JdkWebSocketAdapter.java

pom.xml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
<htmlunit-cssparser.version>5.0.0-SNAPSHOT</htmlunit-cssparser.version>
4545
<htmlunit-corejs.version>5.0.0-SNAPSHOT</htmlunit-corejs.version>
4646
<htmlunit-neko.version>5.0.0-SNAPSHOT</htmlunit-neko.version>
47-
<htmlunit-websocketclient.version>4.21.0</htmlunit-websocketclient.version>
4847
<htmlunit-xpath.version>5.0.0-SNAPSHOT</htmlunit-xpath.version>
4948

5049
<httpcomponents.version>4.5.14</httpcomponents.version>
@@ -1407,11 +1406,6 @@
14071406
<artifactId>htmlunit-csp</artifactId>
14081407
<version>${htmlunit-csp.version}</version>
14091408
</dependency>
1410-
<dependency>
1411-
<groupId>org.htmlunit</groupId>
1412-
<artifactId>htmlunit-websocket-client</artifactId>
1413-
<version>${htmlunit-websocketclient.version}</version>
1414-
</dependency>
14151409

14161410
<dependency>
14171411
<groupId>org.apache.commons</groupId>

src/main/java/org/htmlunit/WebClient.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@
100100
import org.htmlunit.util.NameValuePair;
101101
import org.htmlunit.util.StringUtils;
102102
import org.htmlunit.util.UrlUtils;
103-
import org.htmlunit.websocket.JettyWebSocketAdapter.JettyWebSocketAdapterFactory;
103+
import org.htmlunit.websocket.JdkWebSocketAdapter.JdkWebSocketAdapterFactory;
104104
import org.htmlunit.websocket.WebSocketAdapter;
105105
import org.htmlunit.websocket.WebSocketAdapterFactory;
106106
import org.htmlunit.websocket.WebSocketListener;
@@ -341,7 +341,7 @@ public WebClient(final BrowserVersion browserVersion, final boolean javaScriptEn
341341
}
342342
loadQueue_ = new ArrayList<>();
343343

344-
webSocketAdapterFactory_ = new JettyWebSocketAdapterFactory();
344+
webSocketAdapterFactory_ = new JdkWebSocketAdapterFactory();
345345

346346
// The window must be constructed AFTER the script engine.
347347
currentWindowTracker_ = new CurrentWindowTracker(this, true);
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
/*
2+
* Copyright (c) 2002-2026 Gargoyle Software Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* https://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
package org.htmlunit.websocket;
16+
17+
import java.io.IOException;
18+
import java.net.CookieHandler;
19+
import java.net.URI;
20+
import java.net.http.HttpClient;
21+
import java.net.http.HttpClient.Builder;
22+
import java.nio.ByteBuffer;
23+
import java.security.KeyManagementException;
24+
import java.security.NoSuchAlgorithmException;
25+
import java.security.SecureRandom;
26+
import java.security.cert.X509Certificate;
27+
import java.util.concurrent.CompletableFuture;
28+
import java.util.concurrent.CompletionStage;
29+
import java.util.concurrent.Executor;
30+
31+
import javax.net.ssl.SSLContext;
32+
import javax.net.ssl.TrustManager;
33+
import javax.net.ssl.X509TrustManager;
34+
35+
import org.htmlunit.WebClient;
36+
import org.htmlunit.WebClientOptions;
37+
38+
/**
39+
* JDK based implementation of the {@link WebSocketAdapter}.
40+
* Uses the {@link java.net.http.HttpClient} and {@link java.net.http.WebSocket}
41+
* APIs available since JDK 11.
42+
*
43+
* @author Ronald Brill
44+
*/
45+
public final class JdkWebSocketAdapter implements WebSocketAdapter {
46+
47+
/**
48+
* Our {@link WebSocketAdapterFactory}.
49+
*/
50+
public static final class JdkWebSocketAdapterFactory implements WebSocketAdapterFactory {
51+
/**
52+
* {@inheritDoc}
53+
*/
54+
@Override
55+
public WebSocketAdapter buildWebSocketAdapter(final WebClient webClient,
56+
final WebSocketListener webSocketListener) {
57+
return new JdkWebSocketAdapter(webClient, webSocketListener);
58+
}
59+
}
60+
61+
private final Object clientLock_ = new Object();
62+
private HttpClient httpClient_;
63+
private final WebClient webClient_;
64+
private final WebSocketListener listener_;
65+
66+
private volatile java.net.http.WebSocket incomingSession_;
67+
private java.net.http.WebSocket outgoingSession_;
68+
69+
/**
70+
* Ctor.
71+
* @param webClient the {@link WebClient}
72+
* @param listener the {@link WebSocketListener}
73+
*/
74+
public JdkWebSocketAdapter(final WebClient webClient, final WebSocketListener listener) {
75+
super();
76+
webClient_ = webClient;
77+
listener_ = listener;
78+
}
79+
80+
/**
81+
* {@inheritDoc}
82+
*/
83+
@Override
84+
public void start() throws Exception {
85+
synchronized (clientLock_) {
86+
final WebClientOptions options = webClient_.getOptions();
87+
final Executor executor = webClient_.getExecutor();
88+
89+
final Builder builder = HttpClient.newBuilder()
90+
.executor(executor)
91+
.cookieHandler(new WebSocketCookieHandler(webClient_));
92+
93+
if (options.isUseInsecureSSL()) {
94+
builder.sslContext(createInsecureSslContext());
95+
}
96+
97+
httpClient_ = builder.build();
98+
}
99+
}
100+
101+
/**
102+
* {@inheritDoc}
103+
*/
104+
@Override
105+
public void connect(final URI url) throws Exception {
106+
synchronized (clientLock_) {
107+
final Executor executor = webClient_.getExecutor();
108+
final CompletableFuture<java.net.http.WebSocket> connectFuture =
109+
httpClient_.newWebSocketBuilder()
110+
.buildAsync(url, new JdkWebSocketListenerImpl());
111+
112+
executor.execute(() -> {
113+
try {
114+
listener_.onWebSocketConnecting();
115+
incomingSession_ = connectFuture.join();
116+
}
117+
catch (final Exception e) {
118+
listener_.onWebSocketConnectError(e);
119+
}
120+
});
121+
}
122+
}
123+
124+
/**
125+
* {@inheritDoc}
126+
*/
127+
@Override
128+
public void send(final Object content) throws IOException {
129+
try {
130+
if (content instanceof String string) {
131+
outgoingSession_.sendText(string, true).join();
132+
}
133+
else if (content instanceof ByteBuffer buffer) {
134+
outgoingSession_.sendBinary(buffer, true).join();
135+
}
136+
else {
137+
throw new IllegalStateException(
138+
"Not Yet Implemented: WebSocket.send() was used to send non-string value");
139+
}
140+
}
141+
catch (final IllegalStateException e) {
142+
throw e;
143+
}
144+
catch (final Exception e) {
145+
throw new IOException(e);
146+
}
147+
}
148+
149+
/**
150+
* {@inheritDoc}
151+
*/
152+
@Override
153+
public void closeIncommingSession() {
154+
if (incomingSession_ != null) {
155+
incomingSession_.sendClose(java.net.http.WebSocket.NORMAL_CLOSURE, "").join();
156+
}
157+
}
158+
159+
/**
160+
* {@inheritDoc}
161+
*/
162+
@Override
163+
public void closeOutgoingSession() {
164+
if (outgoingSession_ != null) {
165+
outgoingSession_.sendClose(java.net.http.WebSocket.NORMAL_CLOSURE, "").join();
166+
}
167+
}
168+
169+
/**
170+
* {@inheritDoc}
171+
*/
172+
@Override
173+
public void closeClient() throws Exception {
174+
synchronized (clientLock_) {
175+
// HttpClient does not have a close() in Java 17;
176+
// simply drop the reference so it can be garbage-collected
177+
httpClient_ = null;
178+
}
179+
}
180+
181+
private static SSLContext createInsecureSslContext()
182+
throws NoSuchAlgorithmException, KeyManagementException {
183+
final TrustManager[] trustAllCerts = {
184+
new X509TrustManager() {
185+
@Override
186+
public X509Certificate[] getAcceptedIssuers() {
187+
return new X509Certificate[0];
188+
}
189+
190+
@Override
191+
public void checkClientTrusted(final X509Certificate[] certs, final String authType) {
192+
// trust all
193+
}
194+
195+
@Override
196+
public void checkServerTrusted(final X509Certificate[] certs, final String authType) {
197+
// trust all
198+
}
199+
}
200+
};
201+
final SSLContext sslContext = SSLContext.getInstance("TLS");
202+
sslContext.init(null, trustAllCerts, new SecureRandom());
203+
return sslContext;
204+
}
205+
206+
/**
207+
* A {@link CookieHandler} that bridges to the {@link WebClient} cookie store.
208+
*/
209+
private static class WebSocketCookieHandler extends CookieHandler {
210+
private final WebSocketCookieStore cookieStore_;
211+
212+
WebSocketCookieHandler(final WebClient webClient) {
213+
cookieStore_ = new WebSocketCookieStore(webClient);
214+
}
215+
216+
@Override
217+
public java.util.Map<String, java.util.List<String>> get(
218+
final URI uri,
219+
final java.util.Map<String, java.util.List<String>> requestHeaders) {
220+
final java.util.List<java.net.HttpCookie> cookies = cookieStore_.get(uri);
221+
final java.util.Map<String, java.util.List<String>> result = new java.util.HashMap<>();
222+
if (!cookies.isEmpty()) {
223+
final java.util.List<String> cookieValues = new java.util.ArrayList<>();
224+
for (final java.net.HttpCookie cookie : cookies) {
225+
cookieValues.add(cookie.toString());
226+
}
227+
result.put("Cookie", cookieValues);
228+
}
229+
return result;
230+
}
231+
232+
@Override
233+
public void put(final URI uri,
234+
final java.util.Map<String, java.util.List<String>> responseHeaders) {
235+
// not needed for WebSocket connections
236+
}
237+
}
238+
239+
private class JdkWebSocketListenerImpl implements java.net.http.WebSocket.Listener {
240+
241+
private StringBuilder textAccumulator_;
242+
private ByteBuffer binaryAccumulator_;
243+
244+
JdkWebSocketListenerImpl() {
245+
super();
246+
}
247+
248+
@Override
249+
public void onOpen(final java.net.http.WebSocket webSocket) {
250+
outgoingSession_ = webSocket;
251+
listener_.onWebSocketConnect();
252+
webSocket.request(1);
253+
}
254+
255+
@Override
256+
public CompletionStage<?> onText(final java.net.http.WebSocket webSocket,
257+
final CharSequence data, final boolean last) {
258+
if (textAccumulator_ == null) {
259+
textAccumulator_ = new StringBuilder();
260+
}
261+
textAccumulator_.append(data);
262+
263+
if (last) {
264+
final String message = textAccumulator_.toString();
265+
textAccumulator_ = null;
266+
listener_.onWebSocketText(message);
267+
}
268+
269+
webSocket.request(1);
270+
return null;
271+
}
272+
273+
@Override
274+
public CompletionStage<?> onBinary(final java.net.http.WebSocket webSocket,
275+
final ByteBuffer data, final boolean last) {
276+
if (binaryAccumulator_ == null) {
277+
binaryAccumulator_ = ByteBuffer.allocate(data.remaining());
278+
binaryAccumulator_.put(data);
279+
}
280+
else {
281+
final ByteBuffer newBuffer = ByteBuffer.allocate(
282+
binaryAccumulator_.position() + data.remaining());
283+
binaryAccumulator_.flip();
284+
newBuffer.put(binaryAccumulator_);
285+
newBuffer.put(data);
286+
binaryAccumulator_ = newBuffer;
287+
}
288+
289+
if (last) {
290+
binaryAccumulator_.flip();
291+
final byte[] bytes = new byte[binaryAccumulator_.remaining()];
292+
binaryAccumulator_.get(bytes);
293+
binaryAccumulator_ = null;
294+
listener_.onWebSocketBinary(bytes, 0, bytes.length);
295+
}
296+
297+
webSocket.request(1);
298+
return null;
299+
}
300+
301+
@Override
302+
public CompletionStage<?> onClose(final java.net.http.WebSocket webSocket,
303+
final int statusCode, final String reason) {
304+
outgoingSession_ = null;
305+
listener_.onWebSocketClose(statusCode, reason);
306+
return null;
307+
}
308+
309+
@Override
310+
public void onError(final java.net.http.WebSocket webSocket, final Throwable error) {
311+
outgoingSession_ = null;
312+
listener_.onWebSocketError(error);
313+
}
314+
}
315+
}

0 commit comments

Comments
 (0)