View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
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   * A JavaScript object for {@code WebSocket}.
52   *
53   * @author Ahmed Ashour
54   * @author Ronald Brill
55   * @author Madis Pärn
56   *
57   * @see <a href=
58   *      "https://developer.mozilla.org/en/WebSockets/WebSockets_reference/WebSocket">Mozilla
59   *      documentation</a>
60   */
61  @JsxClass
62  public class WebSocket extends EventTarget implements AutoCloseable {
63  
64      private static final Log LOG = LogFactory.getLog(WebSocket.class);
65  
66      /** The connection has not yet been established. */
67      @JsxConstant
68      public static final int CONNECTING = 0;
69      /** The WebSocket connection is established and communication is possible. */
70      @JsxConstant
71      public static final int OPEN = 1;
72      /** The connection is going through the closing handshake. */
73      @JsxConstant
74      public static final int CLOSING = 2;
75      /** The connection has been closed or could not be opened. */
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       * Creates a new instance.
93       */
94      public WebSocket() {
95          super();
96      }
97  
98      /**
99       * Creates a new instance.
100      *
101      * @param url    the URL to which to connect
102      * @param window the top level window
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      * JavaScript constructor.
224      *
225      * @param cx        the current context
226      * @param scope     the scope
227      * @param args      the arguments to the WebSocket constructor
228      * @param ctorObj   the function object
229      * @param inNewExpr Is new or not
230      * @return the java object to allow JavaScript to access
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      * Returns the event handler that fires on close.
259      *
260      * @return the event handler that fires on close
261      */
262     @JsxGetter
263     public Function getOnclose() {
264         return closeHandler_;
265     }
266 
267     /**
268      * Sets the event handler that fires on close.
269      *
270      * @param closeHandler the event handler that fires on close
271      */
272     @JsxSetter
273     public void setOnclose(final Function closeHandler) {
274         closeHandler_ = closeHandler;
275     }
276 
277     /**
278      * Returns the event handler that fires on error.
279      *
280      * @return the event handler that fires on error
281      */
282     @JsxGetter
283     public Function getOnerror() {
284         return errorHandler_;
285     }
286 
287     /**
288      * Sets the event handler that fires on error.
289      *
290      * @param errorHandler the event handler that fires on error
291      */
292     @JsxSetter
293     public void setOnerror(final Function errorHandler) {
294         errorHandler_ = errorHandler;
295     }
296 
297     /**
298      * Returns the event handler that fires on message.
299      *
300      * @return the event handler that fires on message
301      */
302     @JsxGetter
303     public Function getOnmessage() {
304         return messageHandler_;
305     }
306 
307     /**
308      * Sets the event handler that fires on message.
309      *
310      * @param messageHandler the event handler that fires on message
311      */
312     @JsxSetter
313     public void setOnmessage(final Function messageHandler) {
314         messageHandler_ = messageHandler;
315     }
316 
317     /**
318      * Returns the event handler that fires on open.
319      *
320      * @return the event handler that fires on open
321      */
322     @JsxGetter
323     public Function getOnopen() {
324         return openHandler_;
325     }
326 
327     /**
328      * Sets the event handler that fires on open.
329      *
330      * @param openHandler the event handler that fires on open
331      */
332     @JsxSetter
333     public void setOnopen(final Function openHandler) {
334         openHandler_ = openHandler;
335     }
336 
337     /**
338      * Returns The current state of the connection. The possible values are:
339      * {@link #CONNECTING}, {@link #OPEN}, {@link #CLOSING} or {@link #CLOSED}.
340      *
341      * @return the current state of the connection
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      * @return the url
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      * @return the sub protocol used
365      */
366     @JsxGetter
367     public String getProtocol() {
368         return "";
369     }
370 
371     /**
372      * @return the sub protocol used
373      */
374     @JsxGetter
375     public long getBufferedAmount() {
376         return 0L;
377     }
378 
379     /**
380      * @return the used binary type
381      */
382     @JsxGetter
383     public String getBinaryType() {
384         return binaryType_;
385     }
386 
387     /**
388      * Sets the used binary type.
389      *
390      * @param type the type
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      * {@inheritDoc}
401      */
402     @Override
403     public void close() throws IOException {
404         close(null, null);
405     }
406 
407     /**
408      * Closes the WebSocket connection or connection attempt, if any. If the
409      * connection is already {@link #CLOSED}, this method does nothing.
410      *
411      * @param code   A numeric value indicating the status code explaining why the
412      *               connection is being closed
413      * @param reason A human-readable string explaining why the connection is
414      *               closing
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      * Transmits data to the server over the WebSocket connection.
444      *
445      * @param content the body of the message being sent with the request
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 }