1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package org.htmlunit.javascript.host;
16
17 import java.io.IOException;
18 import java.net.MalformedURLException;
19 import java.net.URI;
20 import java.net.URL;
21 import java.nio.ByteBuffer;
22
23 import org.apache.commons.logging.Log;
24 import org.apache.commons.logging.LogFactory;
25 import org.htmlunit.Page;
26 import org.htmlunit.WebClient;
27 import org.htmlunit.WebWindow;
28 import org.htmlunit.corejs.javascript.Context;
29 import org.htmlunit.corejs.javascript.Function;
30 import org.htmlunit.corejs.javascript.Scriptable;
31 import org.htmlunit.corejs.javascript.ScriptableObject;
32 import org.htmlunit.corejs.javascript.VarScope;
33 import org.htmlunit.corejs.javascript.typedarrays.NativeArrayBuffer;
34 import org.htmlunit.html.HtmlPage;
35 import org.htmlunit.javascript.AbstractJavaScriptEngine;
36 import org.htmlunit.javascript.JavaScriptEngine;
37 import org.htmlunit.javascript.configuration.JsxClass;
38 import org.htmlunit.javascript.configuration.JsxConstant;
39 import org.htmlunit.javascript.configuration.JsxConstructor;
40 import org.htmlunit.javascript.configuration.JsxFunction;
41 import org.htmlunit.javascript.configuration.JsxGetter;
42 import org.htmlunit.javascript.configuration.JsxSetter;
43 import org.htmlunit.javascript.host.dom.DOMException;
44 import org.htmlunit.javascript.host.event.CloseEvent;
45 import org.htmlunit.javascript.host.event.Event;
46 import org.htmlunit.javascript.host.event.EventTarget;
47 import org.htmlunit.javascript.host.event.MessageEvent;
48 import org.htmlunit.util.UrlUtils;
49 import org.htmlunit.websocket.WebSocketAdapter;
50 import org.htmlunit.websocket.WebSocketListener;
51
52
53
54
55
56
57
58
59
60
61
62
63 @JsxClass
64 public class WebSocket extends EventTarget implements AutoCloseable {
65
66 private static final Log LOG = LogFactory.getLog(WebSocket.class);
67
68
69 @JsxConstant
70 public static final int CONNECTING = 0;
71
72 @JsxConstant
73 public static final int OPEN = 1;
74
75 @JsxConstant
76 public static final int CLOSING = 2;
77
78 @JsxConstant
79 public static final int CLOSED = 3;
80
81 private Function closeHandler_;
82 private Function errorHandler_;
83 private Function messageHandler_;
84 private Function openHandler_;
85 private URI url_;
86 private int readyState_ = CONNECTING;
87 private String binaryType_ = "blob";
88
89 private HtmlPage containingPage_;
90 private WebSocketAdapter webSocketImpl_;
91 private boolean originSet_;
92
93
94
95
96 public WebSocket() {
97 super();
98 }
99
100
101
102
103
104
105
106
107 private WebSocket(final String url, final VarScope scope, final Window window) {
108 super();
109 try {
110 final WebWindow webWindow = window.getWebWindow();
111 containingPage_ = (HtmlPage) webWindow.getEnclosedPage();
112
113 setParentScope(scope);
114 setDomNode(containingPage_.getDocumentElement(), false);
115
116 final WebClient webClient = webWindow.getWebClient();
117 originSet_ = true;
118
119 final WebSocketListener webSocketListener = new WebSocketListener() {
120
121 @Override
122 public void onWebSocketConnecting() {
123 setReadyState(CONNECTING);
124 }
125
126 @Override
127 public void onWebSocketOpen() {
128 setReadyState(OPEN);
129
130 final Event openEvent = new Event(Event.TYPE_OPEN);
131 openEvent.setParentScope(scope);
132 openEvent.setPrototype(getPrototype(openEvent.getClass()));
133 openEvent.setSrcElement(WebSocket.this);
134 fire(openEvent);
135 callFunction(openHandler_, new Object[] {openEvent});
136 }
137
138 @Override
139 public void onWebSocketClose(final int statusCode, final String reason) {
140 setReadyState(CLOSED);
141
142 final CloseEvent closeEvent = new CloseEvent();
143 closeEvent.setParentScope(scope);
144 closeEvent.setPrototype(getPrototype(closeEvent.getClass()));
145 closeEvent.setCode(statusCode);
146 closeEvent.setReason(reason);
147 closeEvent.setWasClean(statusCode == 1000);
148 fire(closeEvent);
149 callFunction(closeHandler_, new Object[] {closeEvent});
150 }
151
152 @Override
153 public void onWebSocketText(final String message) {
154 final MessageEvent msgEvent = new MessageEvent(message);
155 msgEvent.setParentScope(scope);
156 msgEvent.setPrototype(getPrototype(msgEvent.getClass()));
157 if (originSet_) {
158 try {
159 URL originUrl = UrlUtils.toUrlUnsafe(getUrl());
160 originUrl = UrlUtils.getUrlWithoutPathRefQuery(originUrl);
161 msgEvent.setOrigin(originUrl.toExternalForm());
162 }
163 catch (final MalformedURLException e) {
164
165 }
166 }
167 msgEvent.setSrcElement(WebSocket.this);
168 fire(msgEvent);
169 callFunction(messageHandler_, new Object[] {msgEvent});
170 }
171
172 @Override
173 public void onWebSocketBinary(final ByteBuffer payload) {
174 final NativeArrayBuffer buffer = new NativeArrayBuffer(payload.remaining());
175 payload.get(buffer.getBuffer());
176
177 buffer.setParentScope(getParentScope());
178 buffer.setPrototype(ScriptableObject.getClassPrototype(getParentScope(), buffer.getClassName()));
179
180 final MessageEvent msgEvent = new MessageEvent(buffer);
181 msgEvent.setParentScope(scope);
182 msgEvent.setPrototype(getPrototype(msgEvent.getClass()));
183 if (originSet_) {
184 try {
185 URL originUrl = UrlUtils.toUrlUnsafe(getUrl());
186 originUrl = UrlUtils.getUrlWithoutPathRefQuery(originUrl);
187 msgEvent.setOrigin(originUrl.toExternalForm());
188 }
189 catch (final MalformedURLException e) {
190
191 }
192 }
193 msgEvent.setSrcElement(WebSocket.this);
194 fire(msgEvent);
195 callFunction(messageHandler_, new Object[] {msgEvent});
196 }
197
198 @Override
199 public void onWebSocketConnectError(final Throwable cause) {
200 if (LOG.isErrorEnabled()) {
201 LOG.error("WS connect error for url '" + url + "':", cause);
202 }
203 onWebSocketError(cause);
204 }
205
206 @Override
207 public void onWebSocketError(final Throwable cause) {
208 if (CLOSED == getReadyState()) {
209 return;
210 }
211
212 setReadyState(CLOSED);
213
214 final Event errorEvent = new Event(Event.TYPE_ERROR);
215 errorEvent.setParentScope(scope);
216 errorEvent.setPrototype(getPrototype(errorEvent.getClass()));
217 errorEvent.setSrcElement(WebSocket.this);
218 fire(errorEvent);
219 callFunction(errorHandler_, new Object[] {errorEvent});
220
221 final CloseEvent closeEvent = new CloseEvent();
222 closeEvent.setParentScope(scope);
223 closeEvent.setPrototype(getPrototype(closeEvent.getClass()));
224 closeEvent.setCode(1006);
225 closeEvent.setReason(cause.getMessage());
226 closeEvent.setWasClean(false);
227 fire(closeEvent);
228 callFunction(closeHandler_, new Object[] {closeEvent});
229 }
230 };
231
232 webSocketImpl_ = webClient.buildWebSocketAdapter(webSocketListener);
233
234 webSocketImpl_.start();
235 containingPage_.addAutoCloseable(this);
236 url_ = new URI(url);
237
238 webSocketImpl_.connect(url_);
239 }
240 catch (final Exception e) {
241 if (LOG.isErrorEnabled()) {
242 LOG.error("WebSocket Error: 'url' parameter '" + url + "' is invalid.", e);
243 }
244 throw JavaScriptEngine.reportRuntimeError("WebSocket Error: 'url' parameter '" + url + "' is invalid.");
245 }
246 }
247
248
249
250
251
252
253
254
255
256
257
258 @JsxConstructor
259 public static Scriptable jsConstructor(final Context cx, final VarScope scope, final Object[] args,
260 final Function ctorObj, final boolean inNewExpr) {
261 if (args.length < 1 || args.length > 2) {
262 throw JavaScriptEngine
263 .typeError("WebSocket Error: constructor must have one or two String parameters.");
264 }
265
266 final Window win = getWindow(ctorObj);
267 String urlString = JavaScriptEngine.toString(args[0]);
268 try {
269 final Page page = win.getWebWindow().getEnclosedPage();
270 if (page instanceof HtmlPage htmlPage) {
271 URL url = htmlPage.getFullyQualifiedUrl(urlString);
272
273 if (url.getRef() != null) {
274 throw JavaScriptEngine.asJavaScriptException(
275 win,
276 "WebSocket Error: 'url' parameter '" + urlString + "' contains a fragment identifier.",
277 DOMException.SYNTAX_ERR);
278 }
279
280
281 final String scheme = url.getProtocol();
282 if ("http".equals(scheme)) {
283 url = UrlUtils.getUrlWithNewProtocol(url, "ws");
284 }
285 else if ("https".equals(scheme)) {
286 url = UrlUtils.getUrlWithNewProtocol(url, "wss");
287 }
288 else if (!"ws".equals(scheme) && !"wss".equals(scheme)) {
289 throw JavaScriptEngine.asJavaScriptException(
290 win,
291 "WebSocket Error: 'url' parameter '" + urlString + "' is not a valid url.",
292 DOMException.SYNTAX_ERR);
293 }
294
295 urlString = url.toExternalForm();
296 }
297 }
298 catch (final MalformedURLException e) {
299 throw JavaScriptEngine.asJavaScriptException(
300 win,
301 "WebSocket Error: 'url' parameter '" + urlString + "' is not a valid url.",
302 DOMException.SYNTAX_ERR);
303 }
304 return new WebSocket(urlString, getTopLevelScope(scope), win);
305 }
306
307
308
309
310
311
312 @JsxGetter
313 public Function getOnclose() {
314 return closeHandler_;
315 }
316
317
318
319
320
321
322 @JsxSetter
323 public void setOnclose(final Function closeHandler) {
324 closeHandler_ = closeHandler;
325 }
326
327
328
329
330
331
332 @JsxGetter
333 public Function getOnerror() {
334 return errorHandler_;
335 }
336
337
338
339
340
341
342 @JsxSetter
343 public void setOnerror(final Function errorHandler) {
344 errorHandler_ = errorHandler;
345 }
346
347
348
349
350
351
352 @JsxGetter
353 public Function getOnmessage() {
354 return messageHandler_;
355 }
356
357
358
359
360
361
362 @JsxSetter
363 public void setOnmessage(final Function messageHandler) {
364 messageHandler_ = messageHandler;
365 }
366
367
368
369
370
371
372 @JsxGetter
373 public Function getOnopen() {
374 return openHandler_;
375 }
376
377
378
379
380
381
382 @JsxSetter
383 public void setOnopen(final Function openHandler) {
384 openHandler_ = openHandler;
385 }
386
387
388
389
390
391
392
393 @JsxGetter
394 public int getReadyState() {
395 return readyState_;
396 }
397
398 void setReadyState(final int readyState) {
399 readyState_ = readyState;
400 }
401
402
403
404
405 @JsxGetter
406 public String getUrl() {
407 if (url_ == null) {
408 throw JavaScriptEngine.typeError("invalid call");
409 }
410 return url_.toString();
411 }
412
413
414
415
416 @JsxGetter
417 public String getProtocol() {
418 return "";
419 }
420
421
422
423
424 @JsxGetter
425 public long getBufferedAmount() {
426 return 0L;
427 }
428
429
430
431
432 @JsxGetter
433 public String getBinaryType() {
434 return binaryType_;
435 }
436
437
438
439
440
441
442 @JsxSetter
443 public void setBinaryType(final String type) {
444 if ("arraybuffer".equals(type) || "blob".equals(type)) {
445 binaryType_ = type;
446 }
447 }
448
449
450
451
452 @Override
453 public void close() throws IOException {
454 close(null, null);
455 }
456
457
458
459
460
461
462
463
464
465
466 @JsxFunction
467 public void close(final Object code, final Object reason) {
468 if (webSocketImpl_ == null) {
469 return;
470 }
471
472 if (readyState_ != CLOSED) {
473 try {
474 webSocketImpl_.closeIncomingSession();
475 }
476 catch (final Throwable e) {
477 LOG.error("WS close error - incomingSession_.close() failed", e);
478 }
479
480 try {
481 webSocketImpl_.closeOutgoingSession();
482 }
483 catch (final Throwable e) {
484 LOG.error("WS close error - outgoingSession_.close() failed", e);
485 }
486 }
487
488 try {
489 webSocketImpl_.closeClient();
490 }
491 catch (final Exception e) {
492 throw new RuntimeException(e);
493 }
494 }
495
496
497
498
499
500
501 @JsxFunction
502 public void send(final Object content) {
503 try {
504 if (content instanceof NativeArrayBuffer buffer1) {
505 final byte[] bytes = buffer1.getBuffer();
506 final ByteBuffer buffer = ByteBuffer.wrap(bytes);
507 webSocketImpl_.send(buffer);
508 return;
509 }
510 webSocketImpl_.send(content);
511 }
512 catch (final IOException e) {
513 LOG.error("WS send error", e);
514 }
515 }
516
517 void fire(final Event evt) {
518 evt.setTarget(this);
519 evt.setParentScope(getParentScope());
520 evt.setPrototype(getPrototype(evt.getClass()));
521
522 final AbstractJavaScriptEngine<?> engine = containingPage_.getWebClient().getJavaScriptEngine();
523 if (engine != null) {
524 engine.getContextFactory().call(cx -> {
525 executeEventLocally(evt);
526 return null;
527 });
528 }
529 }
530
531 void callFunction(final Function function, final Object[] args) {
532 if (function == null) {
533 return;
534 }
535 final VarScope scope = function.getParentScope();
536 final JavaScriptEngine engine = (JavaScriptEngine) containingPage_.getWebClient().getJavaScriptEngine();
537 if (engine != null) {
538 engine.callFunction(containingPage_, function, scope, this, args);
539 }
540 }
541 }