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