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.client.WebSocketClient;
39
40
41
42
43
44
45
46
47 public final class JettyWebSocketAdapter implements WebSocketAdapter {
48
49
50
51
52 public static final class JettyWebSocketAdapterFactory implements WebSocketAdapterFactory {
53
54
55
56 @Override
57 public WebSocketAdapter buildWebSocketAdapter(final WebClient webClient,
58 final WebSocketListener webSocketListener) {
59 return new JettyWebSocketAdapter(webClient, webSocketListener);
60 }
61 }
62
63 private static class WebSocketCookieStore implements HttpCookieStore {
64
65 private final WebClient webClient_;
66
67 WebSocketCookieStore(final WebClient webClient) {
68 webClient_ = webClient;
69 }
70
71 @Override
72 public boolean add(final URI uri, final HttpCookie cookie) {
73 return false;
74 }
75
76 @Override
77 public List<HttpCookie> all() {
78 throw new UnsupportedOperationException();
79 }
80
81 @Override
82 public List<HttpCookie> match(final URI uri) {
83 final List<HttpCookie> cookies = new ArrayList<>();
84 try {
85 final String urlString = uri.toString()
86 .replace("ws://", "http://")
87 .replace("wss://", "https://");
88 final URL url = new URL(urlString);
89 for (final Cookie cookie : webClient_.getCookies(url)) {
90 final HttpCookie.Builder builder = HttpCookie.build(cookie.getName(), cookie.getValue());
91 if (cookie.getDomain() != null) {
92 builder.domain(cookie.getDomain());
93 }
94 if (cookie.getPath() != null) {
95 builder.path(cookie.getPath());
96 }
97 if (cookie.getExpires() != null) {
98 builder.expires(cookie.getExpires().toInstant());
99 }
100 builder.secure(cookie.isSecure());
101 builder.httpOnly(cookie.isHttpOnly());
102 if (cookie.getSameSite() != null) {
103 final HttpCookie.SameSite sameSite = HttpCookie.SameSite.from(cookie.getSameSite());
104 if (sameSite != null) {
105 builder.sameSite(sameSite);
106 }
107 }
108 cookies.add(builder.build());
109 }
110 }
111 catch (final Exception e) {
112 throw new RuntimeException(e);
113 }
114 return cookies;
115 }
116
117 @Override
118 public boolean remove(final URI uri, final HttpCookie cookie) {
119 throw new UnsupportedOperationException();
120 }
121
122 @Override
123 public boolean clear() {
124 return false;
125 }
126 }
127
128 private final Object clientLock_ = new Object();
129 private WebSocketClient client_;
130 private final WebSocketListener listener_;
131
132 private volatile Session incomingSession_;
133 private Session outgoingSession_;
134
135
136
137
138
139
140 public JettyWebSocketAdapter(final WebClient webClient, final WebSocketListener listener) {
141 super();
142 final WebClientOptions options = webClient.getOptions();
143
144 if (webClient.getOptions().isUseInsecureSSL()) {
145 final HttpClient httpClient = new HttpClient();
146 httpClient.setSslContextFactory(new SslContextFactory.Client(true));
147 client_ = new WebSocketClient(httpClient);
148 }
149 else {
150 client_ = new WebSocketClient();
151 }
152
153 listener_ = listener;
154
155
156 client_.getHttpClient().setExecutor(webClient.getExecutor());
157
158 client_.getHttpClient().setHttpCookieStore(new WebSocketCookieStore(webClient));
159
160 int size = options.getWebSocketMaxBinaryMessageSize();
161 if (size > 0) {
162 client_.setMaxBinaryMessageSize(size);
163 }
164 size = options.getWebSocketMaxTextMessageSize();
165 if (size > 0) {
166 client_.setMaxTextMessageSize(size);
167 }
168 }
169
170
171
172
173 @Override
174 public void start() throws Exception {
175 synchronized (clientLock_) {
176 client_.start();
177 }
178 }
179
180
181
182
183 @Override
184 public void connect(final URI url) throws Exception {
185 synchronized (clientLock_) {
186 final CompletableFuture<Session> connectFuture = client_.connect(new JettyWebSocketAdapterImpl(), url);
187 client_.getExecutor().execute(() -> {
188 try {
189 listener_.onWebSocketConnecting();
190 incomingSession_ = connectFuture.get();
191 }
192 catch (final Exception e) {
193 if (!(e instanceof ExecutionException)
194 || !(e.getCause() instanceof AsynchronousCloseException)) {
195 listener_.onWebSocketConnectError(e);
196 }
197 }
198 });
199 }
200 }
201
202
203
204
205 @Override
206 public void send(final Object content) throws IOException {
207 if (content instanceof String string) {
208 outgoingSession_.sendText(string, Callback.NOOP);
209 }
210 else if (content instanceof ByteBuffer buffer) {
211 outgoingSession_.sendBinary(buffer, Callback.NOOP);
212 }
213 else {
214 throw new IllegalStateException("Not Yet Implemented: WebSocket.send() was used to send non-string value");
215 }
216 }
217
218
219
220
221 @Override
222 public void closeIncomingSession() {
223 if (incomingSession_ != null) {
224 incomingSession_.close();
225 }
226 }
227
228
229
230
231 @Override
232 public void closeOutgoingSession() {
233 if (outgoingSession_ != null) {
234 outgoingSession_.close();
235 }
236 }
237
238
239
240
241 @Override
242 public void closeClient() throws Exception {
243 synchronized (clientLock_) {
244 if (client_ != null) {
245 client_.stop();
246 client_.destroy();
247
248
249 client_ = null;
250 }
251 }
252 }
253
254
255
256
257
258
259 public class JettyWebSocketAdapterImpl implements AutoDemanding {
260
261
262 JettyWebSocketAdapterImpl() {
263 super();
264 }
265
266 @Override
267 public void onWebSocketOpen(final Session session) {
268 outgoingSession_ = session;
269 listener_.onWebSocketOpen();
270 }
271
272 @Override
273 public void onWebSocketClose(final int statusCode, final String reason, final Callback callback) {
274 outgoingSession_ = null;
275 listener_.onWebSocketClose(statusCode, reason);
276 callback.succeed();
277 }
278
279 @Override
280 public void onWebSocketText(final String message) {
281 listener_.onWebSocketText(message);
282 }
283
284 @Override
285 public void onWebSocketBinary(final ByteBuffer payload, final Callback callback) {
286 listener_.onWebSocketBinary(payload);
287 callback.succeed();
288 }
289
290 @Override
291 public void onWebSocketError(final Throwable cause) {
292 outgoingSession_ = null;
293
294
295 if (cause instanceof ClosedChannelException) {
296
297 return;
298 }
299
300 listener_.onWebSocketError(cause);
301 }
302 }
303 }