1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package org.htmlunit.websocket;
16
17 import java.io.IOException;
18 import java.net.URI;
19 import java.net.URL;
20 import java.nio.ByteBuffer;
21 import java.nio.channels.AsynchronousCloseException;
22 import java.nio.channels.ClosedChannelException;
23 import java.util.ArrayList;
24 import java.util.List;
25 import java.util.concurrent.CompletableFuture;
26 import java.util.concurrent.ExecutionException;
27
28 import org.htmlunit.WebClient;
29 import org.htmlunit.WebClientOptions;
30 import org.htmlunit.http.Cookie;
31 import org.htmlunit.jetty.client.HttpClient;
32 import org.htmlunit.jetty.http.HttpCookie;
33 import org.htmlunit.jetty.http.HttpCookieStore;
34 import org.htmlunit.jetty.util.ssl.SslContextFactory;
35 import org.htmlunit.jetty.websocket.api.Callback;
36 import org.htmlunit.jetty.websocket.api.Session;
37 import org.htmlunit.jetty.websocket.api.Session.Listener.AutoDemanding;
38 import org.htmlunit.jetty.websocket.api.UpgradeResponse;
39 import org.htmlunit.jetty.websocket.client.ClientUpgradeRequest;
40 import org.htmlunit.jetty.websocket.client.WebSocketClient;
41
42
43
44
45
46
47
48
49 public final class JettyWebSocketAdapter implements WebSocketAdapter {
50
51
52
53
54 public static final class JettyWebSocketAdapterFactory implements WebSocketAdapterFactory {
55
56
57
58 @Override
59 public WebSocketAdapter buildWebSocketAdapter(final WebClient webClient,
60 final WebSocketListener webSocketListener) {
61 return new JettyWebSocketAdapter(webClient, webSocketListener);
62 }
63 }
64
65 private static class WebSocketCookieStore implements HttpCookieStore {
66
67 private final WebClient webClient_;
68
69 WebSocketCookieStore(final WebClient webClient) {
70 webClient_ = webClient;
71 }
72
73 @Override
74 public boolean add(final URI uri, final HttpCookie cookie) {
75 return false;
76 }
77
78 @Override
79 public List<HttpCookie> all() {
80 throw new UnsupportedOperationException();
81 }
82
83 @Override
84 public List<HttpCookie> match(final URI uri) {
85 final List<HttpCookie> cookies = new ArrayList<>();
86 try {
87 final String urlString = uri.toString()
88 .replace("ws://", "http://")
89 .replace("wss://", "https://");
90 final URL url = new URL(urlString);
91 for (final Cookie cookie : webClient_.getCookies(url)) {
92 final HttpCookie.Builder builder = HttpCookie.build(cookie.getName(), cookie.getValue());
93 if (cookie.getDomain() != null) {
94 builder.domain(cookie.getDomain());
95 }
96 if (cookie.getPath() != null) {
97 builder.path(cookie.getPath());
98 }
99 if (cookie.getExpires() != null) {
100 builder.expires(cookie.getExpires().toInstant());
101 }
102 builder.secure(cookie.isSecure());
103 builder.httpOnly(cookie.isHttpOnly());
104 if (cookie.getSameSite() != null) {
105 final HttpCookie.SameSite sameSite = HttpCookie.SameSite.from(cookie.getSameSite());
106 if (sameSite != null) {
107 builder.sameSite(sameSite);
108 }
109 }
110 cookies.add(builder.build());
111 }
112 }
113 catch (final Exception e) {
114 throw new RuntimeException(e);
115 }
116 return cookies;
117 }
118
119 @Override
120 public boolean remove(final URI uri, final HttpCookie cookie) {
121 throw new UnsupportedOperationException();
122 }
123
124 @Override
125 public boolean clear() {
126 return false;
127 }
128 }
129
130 private final Object clientLock_ = new Object();
131 private WebSocketClient client_;
132 private final WebSocketListener listener_;
133
134 private volatile Session incomingSession_;
135 private volatile Session outgoingSession_;
136
137
138
139
140
141
142 public JettyWebSocketAdapter(final WebClient webClient, final WebSocketListener listener) {
143 super();
144 final WebClientOptions options = webClient.getOptions();
145
146 if (webClient.getOptions().isUseInsecureSSL()) {
147 final HttpClient httpClient = new HttpClient();
148 httpClient.setSslContextFactory(new SslContextFactory.Client(true));
149 client_ = new WebSocketClient(httpClient);
150 }
151 else {
152 client_ = new WebSocketClient();
153 }
154
155 listener_ = listener;
156
157
158 client_.getHttpClient().setExecutor(webClient.getExecutor());
159
160 client_.getHttpClient().setHttpCookieStore(new WebSocketCookieStore(webClient));
161
162 int size = options.getWebSocketMaxBinaryMessageSize();
163 if (size > 0) {
164 client_.setMaxBinaryMessageSize(size);
165 }
166 size = options.getWebSocketMaxTextMessageSize();
167 if (size > 0) {
168 client_.setMaxTextMessageSize(size);
169 }
170 }
171
172
173
174
175 @Override
176 public void start() throws Exception {
177 synchronized (clientLock_) {
178 client_.start();
179 }
180 }
181
182
183
184
185 @Override
186 public void connect(final URI url) throws Exception {
187 synchronized (clientLock_) {
188 listenForConnect(client_.connect(new JettyWebSocketAdapterImpl(), url));
189 }
190 }
191
192
193
194
195 @Override
196 public void connect(final URI url, final List<String> protocols) throws Exception {
197 if (protocols.isEmpty()) {
198 connect(url);
199 return;
200 }
201
202 synchronized (clientLock_) {
203 final ClientUpgradeRequest request = new ClientUpgradeRequest(url);
204 request.setSubProtocols(protocols);
205 listenForConnect(client_.connect(new JettyWebSocketAdapterImpl(), request));
206 }
207 }
208
209 private void listenForConnect(final CompletableFuture<Session> connectFuture) {
210 client_.getExecutor().execute(() -> {
211 try {
212 listener_.onWebSocketConnecting();
213 incomingSession_ = connectFuture.get();
214 }
215 catch (final Exception e) {
216 if (!(e instanceof ExecutionException)
217 || !(e.getCause() instanceof AsynchronousCloseException)) {
218 listener_.onWebSocketConnectError(e);
219 }
220 }
221 });
222 }
223
224
225
226
227 @Override
228 public String getSelectedProtocol() {
229 final Session session = outgoingSession_;
230 if (session == null) {
231 return "";
232 }
233
234 final UpgradeResponse response = session.getUpgradeResponse();
235 final String protocol = response == null ? null : response.getAcceptedSubProtocol();
236 return protocol == null ? "" : protocol;
237 }
238
239
240
241
242 @Override
243 public void send(final Object content) throws IOException {
244 final Session session = outgoingSession_;
245 if (session == null) {
246 throw new IOException("WebSocket is not connected");
247 }
248
249 if (content instanceof String string) {
250 session.sendText(string, Callback.NOOP);
251 }
252 else if (content instanceof ByteBuffer buffer) {
253 session.sendBinary(buffer, Callback.NOOP);
254 }
255 else {
256 throw new IllegalStateException("Not Yet Implemented: WebSocket.send() was used to send non-string value");
257 }
258 }
259
260
261
262
263 @Override
264 public void closeIncomingSession() {
265 final Session session = incomingSession_;
266 if (session != null) {
267 session.close();
268 }
269 }
270
271
272
273
274 @Override
275 public void closeOutgoingSession() {
276 final Session session = outgoingSession_;
277 if (session != null) {
278 session.close();
279 }
280 }
281
282
283
284
285 @Override
286 public void closeOutgoingSession(final int statusCode, final String reason) {
287 final Session session = outgoingSession_;
288 if (session != null) {
289 session.close(statusCode, reason, Callback.NOOP);
290 }
291 }
292
293
294
295
296 @Override
297 public void closeClient() throws Exception {
298 final WebSocketClient client;
299 synchronized (clientLock_) {
300 client = client_;
301 client_ = null;
302 }
303
304 if (client != null) {
305 client.stop();
306 client.destroy();
307 }
308 }
309
310
311
312
313
314
315 public class JettyWebSocketAdapterImpl implements AutoDemanding {
316
317
318 JettyWebSocketAdapterImpl() {
319 super();
320 }
321
322 @Override
323 public void onWebSocketOpen(final Session session) {
324 outgoingSession_ = session;
325 listener_.onWebSocketOpen();
326 }
327
328 @Override
329 public void onWebSocketClose(final int statusCode, final String reason, final Callback callback) {
330 outgoingSession_ = null;
331
332
333 callback.succeed();
334 listener_.onWebSocketClose(statusCode, reason);
335 }
336
337 @Override
338 public void onWebSocketText(final String message) {
339 listener_.onWebSocketText(message);
340 }
341
342 @Override
343 public void onWebSocketBinary(final ByteBuffer payload, final Callback callback) {
344 try {
345 listener_.onWebSocketBinary(payload);
346 callback.succeed();
347 }
348 catch (final Throwable ex) {
349 callback.fail(ex);
350 }
351 }
352
353 @Override
354 public void onWebSocketError(final Throwable cause) {
355 outgoingSession_ = null;
356
357
358 if (cause instanceof ClosedChannelException) {
359
360 return;
361 }
362
363 listener_.onWebSocketError(cause);
364 }
365 }
366 }