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.JettyWebSocketAdapter;
48 import org.htmlunit.websocket.WebSocketAdapter;
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 webSocketImpl_ = new JettyWebSocketAdapter(webClient) {
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_.start();
209 containingPage_.addAutoCloseable(this);
210 url_ = new URI(url);
211
212 webSocketImpl_.connect(url_);
213 }
214 catch (final Exception e) {
215 if (LOG.isErrorEnabled()) {
216 LOG.error("WebSocket Error: 'url' parameter '" + url + "' is invalid.", e);
217 }
218 throw JavaScriptEngine.reportRuntimeError("WebSocket Error: 'url' parameter '" + url + "' is invalid.");
219 }
220 }
221
222
223
224
225
226
227
228
229
230
231
232 @JsxConstructor
233 public static Scriptable jsConstructor(final Context cx, final Scriptable scope, final Object[] args,
234 final Function ctorObj, final boolean inNewExpr) {
235 if (args.length < 1 || args.length > 2) {
236 throw JavaScriptEngine
237 .reportRuntimeError("WebSocket Error: constructor must have one or two String parameters.");
238 }
239
240 final Window win = getWindow(ctorObj);
241 String urlString = JavaScriptEngine.toString(args[0]);
242 try {
243 final Page page = win.getWebWindow().getEnclosedPage();
244 if (page instanceof HtmlPage) {
245 URL url = ((HtmlPage) page).getFullyQualifiedUrl(urlString);
246 url = UrlUtils.getUrlWithNewProtocol(url, "ws");
247 urlString = url.toExternalForm();
248 }
249 }
250 catch (final MalformedURLException e) {
251 throw JavaScriptEngine.reportRuntimeError(
252 "WebSocket Error: 'url' parameter '" + urlString + "' is not a valid url.");
253 }
254 return new WebSocket(urlString, win);
255 }
256
257
258
259
260
261
262 @JsxGetter
263 public Function getOnclose() {
264 return closeHandler_;
265 }
266
267
268
269
270
271
272 @JsxSetter
273 public void setOnclose(final Function closeHandler) {
274 closeHandler_ = closeHandler;
275 }
276
277
278
279
280
281
282 @JsxGetter
283 public Function getOnerror() {
284 return errorHandler_;
285 }
286
287
288
289
290
291
292 @JsxSetter
293 public void setOnerror(final Function errorHandler) {
294 errorHandler_ = errorHandler;
295 }
296
297
298
299
300
301
302 @JsxGetter
303 public Function getOnmessage() {
304 return messageHandler_;
305 }
306
307
308
309
310
311
312 @JsxSetter
313 public void setOnmessage(final Function messageHandler) {
314 messageHandler_ = messageHandler;
315 }
316
317
318
319
320
321
322 @JsxGetter
323 public Function getOnopen() {
324 return openHandler_;
325 }
326
327
328
329
330
331
332 @JsxSetter
333 public void setOnopen(final Function openHandler) {
334 openHandler_ = openHandler;
335 }
336
337
338
339
340
341
342
343 @JsxGetter
344 public int getReadyState() {
345 return readyState_;
346 }
347
348 void setReadyState(final int readyState) {
349 readyState_ = readyState;
350 }
351
352
353
354
355 @JsxGetter
356 public String getUrl() {
357 if (url_ == null) {
358 throw JavaScriptEngine.typeError("invalid call");
359 }
360 return url_.toString();
361 }
362
363
364
365
366 @JsxGetter
367 public String getProtocol() {
368 return "";
369 }
370
371
372
373
374 @JsxGetter
375 public long getBufferedAmount() {
376 return 0L;
377 }
378
379
380
381
382 @JsxGetter
383 public String getBinaryType() {
384 return binaryType_;
385 }
386
387
388
389
390
391
392 @JsxSetter
393 public void setBinaryType(final String type) {
394 if ("arraybuffer".equals(type) || "blob".equals(type)) {
395 binaryType_ = type;
396 }
397 }
398
399
400
401
402 @Override
403 public void close() throws IOException {
404 close(null, null);
405 }
406
407
408
409
410
411
412
413
414
415
416 @JsxFunction
417 public void close(final Object code, final Object reason) {
418 if (readyState_ != CLOSED) {
419 try {
420 webSocketImpl_.closeIncommingSession();
421 }
422 catch (final Throwable e) {
423 LOG.error("WS close error - incomingSession_.close() failed", e);
424 }
425
426 try {
427 webSocketImpl_.closeOutgoingSession();
428 }
429 catch (final Throwable e) {
430 LOG.error("WS close error - outgoingSession_.close() failed", e);
431 }
432 }
433
434 try {
435 webSocketImpl_.closeClient();
436 }
437 catch (final Exception e) {
438 throw new RuntimeException(e);
439 }
440 }
441
442
443
444
445
446
447 @JsxFunction
448 public void send(final Object content) {
449 try {
450 if (content instanceof NativeArrayBuffer) {
451 final byte[] bytes = ((NativeArrayBuffer) content).getBuffer();
452 final ByteBuffer buffer = ByteBuffer.wrap(bytes);
453 webSocketImpl_.send(buffer);
454 return;
455 }
456 webSocketImpl_.send(content);
457 }
458 catch (final IOException e) {
459 LOG.error("WS send error", e);
460 }
461 }
462
463 void fire(final Event evt) {
464 evt.setTarget(this);
465 evt.setParentScope(getParentScope());
466 evt.setPrototype(getPrototype(evt.getClass()));
467
468 final AbstractJavaScriptEngine<?> engine = containingPage_.getWebClient().getJavaScriptEngine();
469 engine.getContextFactory().call(cx -> {
470 executeEventLocally(evt);
471 return null;
472 });
473 }
474
475 void callFunction(final Function function, final Object[] args) {
476 if (function == null) {
477 return;
478 }
479 final Scriptable scope = function.getParentScope();
480 final JavaScriptEngine engine = (JavaScriptEngine) containingPage_.getWebClient().getJavaScriptEngine();
481 engine.callFunction(containingPage_, function, scope, this, args);
482 }
483 }