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.typedarrays.NativeArrayBuffer;
33 import org.htmlunit.html.HtmlPage;
34 import org.htmlunit.javascript.AbstractJavaScriptEngine;
35 import org.htmlunit.javascript.JavaScriptEngine;
36 import org.htmlunit.javascript.configuration.JsxClass;
37 import org.htmlunit.javascript.configuration.JsxConstant;
38 import org.htmlunit.javascript.configuration.JsxConstructor;
39 import org.htmlunit.javascript.configuration.JsxFunction;
40 import org.htmlunit.javascript.configuration.JsxGetter;
41 import org.htmlunit.javascript.configuration.JsxSetter;
42 import org.htmlunit.javascript.host.event.CloseEvent;
43 import org.htmlunit.javascript.host.event.Event;
44 import org.htmlunit.javascript.host.event.EventTarget;
45 import org.htmlunit.javascript.host.event.MessageEvent;
46 import org.htmlunit.util.UrlUtils;
47 import org.htmlunit.websocket.WebSocketAdapter;
48 import org.htmlunit.websocket.WebSocketListener;
49
50
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 private WebSocket(final String url, final Window window) {
105 super();
106 try {
107 final WebWindow webWindow = window.getWebWindow();
108 containingPage_ = (HtmlPage) webWindow.getEnclosedPage();
109 setParentScope(window);
110 setDomNode(containingPage_.getDocumentElement(), false);
111
112 final WebClient webClient = webWindow.getWebClient();
113 originSet_ = true;
114
115 final WebSocketListener webSocketListener = new WebSocketListener() {
116
117 @Override
118 public void onWebSocketConnecting() {
119 setReadyState(CONNECTING);
120 }
121
122 @Override
123 public void onWebSocketConnect() {
124 setReadyState(OPEN);
125
126 final Event openEvent = new Event(Event.TYPE_OPEN);
127 openEvent.setParentScope(window);
128 openEvent.setPrototype(getPrototype(openEvent.getClass()));
129 openEvent.setSrcElement(WebSocket.this);
130 fire(openEvent);
131 callFunction(openHandler_, new Object[] {openEvent});
132 }
133
134 @Override
135 public void onWebSocketClose(final int statusCode, final String reason) {
136 setReadyState(CLOSED);
137
138 final CloseEvent closeEvent = new CloseEvent();
139 closeEvent.setParentScope(window);
140 closeEvent.setPrototype(getPrototype(closeEvent.getClass()));
141 closeEvent.setCode(statusCode);
142 closeEvent.setReason(reason);
143 closeEvent.setWasClean(true);
144 fire(closeEvent);
145 callFunction(closeHandler_, new Object[] {closeEvent});
146 }
147
148 @Override
149 public void onWebSocketText(final String message) {
150 final MessageEvent msgEvent = new MessageEvent(message);
151 msgEvent.setParentScope(window);
152 msgEvent.setPrototype(getPrototype(msgEvent.getClass()));
153 if (originSet_) {
154 msgEvent.setOrigin(getUrl());
155 }
156 msgEvent.setSrcElement(WebSocket.this);
157 fire(msgEvent);
158 callFunction(messageHandler_, new Object[] {msgEvent});
159 }
160
161 @Override
162 public void onWebSocketBinary(final byte[] data, final int offset, final int length) {
163 final NativeArrayBuffer buffer = new NativeArrayBuffer(length);
164 System.arraycopy(data, offset, buffer.getBuffer(), 0, length);
165 buffer.setParentScope(getParentScope());
166 buffer.setPrototype(ScriptableObject.getClassPrototype(getWindow(), buffer.getClassName()));
167
168 final MessageEvent msgEvent = new MessageEvent(buffer);
169 msgEvent.setParentScope(window);
170 msgEvent.setPrototype(getPrototype(msgEvent.getClass()));
171 if (originSet_) {
172 msgEvent.setOrigin(getUrl());
173 }
174 msgEvent.setSrcElement(WebSocket.this);
175 fire(msgEvent);
176 callFunction(messageHandler_, new Object[] {msgEvent});
177 }
178
179 @Override
180 public void onWebSocketConnectError(final Throwable cause) {
181 if (LOG.isErrorEnabled()) {
182 LOG.error("WS connect error for url '" + url + "':", cause);
183 }
184 }
185
186 @Override
187 public void onWebSocketError(final Throwable cause) {
188 setReadyState(CLOSED);
189
190 final Event errorEvent = new Event(Event.TYPE_ERROR);
191 errorEvent.setParentScope(window);
192 errorEvent.setPrototype(getPrototype(errorEvent.getClass()));
193 errorEvent.setSrcElement(WebSocket.this);
194 fire(errorEvent);
195 callFunction(errorHandler_, new Object[] {errorEvent});
196
197 final CloseEvent closeEvent = new CloseEvent();
198 closeEvent.setParentScope(window);
199 closeEvent.setPrototype(getPrototype(closeEvent.getClass()));
200 closeEvent.setCode(1006);
201 closeEvent.setReason(cause.getMessage());
202 closeEvent.setWasClean(false);
203 fire(closeEvent);
204 callFunction(closeHandler_, new Object[] {closeEvent});
205 }
206 };
207
208 webSocketImpl_ = webClient.buildWebSocketAdapter(webSocketListener);
209
210 webSocketImpl_.start();
211 containingPage_.addAutoCloseable(this);
212 url_ = new URI(url);
213
214 webSocketImpl_.connect(url_);
215 }
216 catch (final Exception e) {
217 if (LOG.isErrorEnabled()) {
218 LOG.error("WebSocket Error: 'url' parameter '" + url + "' is invalid.", e);
219 }
220 throw JavaScriptEngine.reportRuntimeError("WebSocket Error: 'url' parameter '" + url + "' is invalid.");
221 }
222 }
223
224
225
226
227
228
229
230
231
232
233
234 @JsxConstructor
235 public static Scriptable jsConstructor(final Context cx, final Scriptable scope, final Object[] args,
236 final Function ctorObj, final boolean inNewExpr) {
237 if (args.length < 1 || args.length > 2) {
238 throw JavaScriptEngine
239 .reportRuntimeError("WebSocket Error: constructor must have one or two String parameters.");
240 }
241
242 final Window win = getWindow(ctorObj);
243 String urlString = JavaScriptEngine.toString(args[0]);
244 try {
245 final Page page = win.getWebWindow().getEnclosedPage();
246 if (page instanceof HtmlPage) {
247 URL url = ((HtmlPage) page).getFullyQualifiedUrl(urlString);
248 url = UrlUtils.getUrlWithNewProtocol(url, "ws");
249 urlString = url.toExternalForm();
250 }
251 }
252 catch (final MalformedURLException e) {
253 throw JavaScriptEngine.reportRuntimeError(
254 "WebSocket Error: 'url' parameter '" + urlString + "' is not a valid url.");
255 }
256 return new WebSocket(urlString, win);
257 }
258
259
260
261
262
263
264 @JsxGetter
265 public Function getOnclose() {
266 return closeHandler_;
267 }
268
269
270
271
272
273
274 @JsxSetter
275 public void setOnclose(final Function closeHandler) {
276 closeHandler_ = closeHandler;
277 }
278
279
280
281
282
283
284 @JsxGetter
285 public Function getOnerror() {
286 return errorHandler_;
287 }
288
289
290
291
292
293
294 @JsxSetter
295 public void setOnerror(final Function errorHandler) {
296 errorHandler_ = errorHandler;
297 }
298
299
300
301
302
303
304 @JsxGetter
305 public Function getOnmessage() {
306 return messageHandler_;
307 }
308
309
310
311
312
313
314 @JsxSetter
315 public void setOnmessage(final Function messageHandler) {
316 messageHandler_ = messageHandler;
317 }
318
319
320
321
322
323
324 @JsxGetter
325 public Function getOnopen() {
326 return openHandler_;
327 }
328
329
330
331
332
333
334 @JsxSetter
335 public void setOnopen(final Function openHandler) {
336 openHandler_ = openHandler;
337 }
338
339
340
341
342
343
344
345 @JsxGetter
346 public int getReadyState() {
347 return readyState_;
348 }
349
350 void setReadyState(final int readyState) {
351 readyState_ = readyState;
352 }
353
354
355
356
357 @JsxGetter
358 public String getUrl() {
359 if (url_ == null) {
360 throw JavaScriptEngine.typeError("invalid call");
361 }
362 return url_.toString();
363 }
364
365
366
367
368 @JsxGetter
369 public String getProtocol() {
370 return "";
371 }
372
373
374
375
376 @JsxGetter
377 public long getBufferedAmount() {
378 return 0L;
379 }
380
381
382
383
384 @JsxGetter
385 public String getBinaryType() {
386 return binaryType_;
387 }
388
389
390
391
392
393
394 @JsxSetter
395 public void setBinaryType(final String type) {
396 if ("arraybuffer".equals(type) || "blob".equals(type)) {
397 binaryType_ = type;
398 }
399 }
400
401
402
403
404 @Override
405 public void close() throws IOException {
406 close(null, null);
407 }
408
409
410
411
412
413
414
415
416
417
418 @JsxFunction
419 public void close(final Object code, final Object reason) {
420 if (readyState_ != CLOSED) {
421 try {
422 webSocketImpl_.closeIncommingSession();
423 }
424 catch (final Throwable e) {
425 LOG.error("WS close error - incomingSession_.close() failed", e);
426 }
427
428 try {
429 webSocketImpl_.closeOutgoingSession();
430 }
431 catch (final Throwable e) {
432 LOG.error("WS close error - outgoingSession_.close() failed", e);
433 }
434 }
435
436 try {
437 webSocketImpl_.closeClient();
438 }
439 catch (final Exception e) {
440 throw new RuntimeException(e);
441 }
442 }
443
444
445
446
447
448
449 @JsxFunction
450 public void send(final Object content) {
451 try {
452 if (content instanceof NativeArrayBuffer) {
453 final byte[] bytes = ((NativeArrayBuffer) content).getBuffer();
454 final ByteBuffer buffer = ByteBuffer.wrap(bytes);
455 webSocketImpl_.send(buffer);
456 return;
457 }
458 webSocketImpl_.send(content);
459 }
460 catch (final IOException e) {
461 LOG.error("WS send error", e);
462 }
463 }
464
465 void fire(final Event evt) {
466 evt.setTarget(this);
467 evt.setParentScope(getParentScope());
468 evt.setPrototype(getPrototype(evt.getClass()));
469
470 final AbstractJavaScriptEngine<?> engine = containingPage_.getWebClient().getJavaScriptEngine();
471 engine.getContextFactory().call(cx -> {
472 executeEventLocally(evt);
473 return null;
474 });
475 }
476
477 void callFunction(final Function function, final Object[] args) {
478 if (function == null) {
479 return;
480 }
481 final Scriptable scope = function.getParentScope();
482 final JavaScriptEngine engine = (JavaScriptEngine) containingPage_.getWebClient().getJavaScriptEngine();
483 engine.callFunction(containingPage_, function, scope, this, args);
484 }
485 }