All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.gargoylesoftware.htmlunit.javascript.host.WebSocket Maven / Gradle / Ivy

There is a newer version: 2.70.0
Show newest version
/*
 * Copyright (c) 2002-2018 Gargoyle Software Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gargoylesoftware.htmlunit.javascript.host;

import java.io.IOException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.concurrent.Future;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.UpgradeException;
import org.eclipse.jetty.websocket.api.WebSocketAdapter;
import org.eclipse.jetty.websocket.api.WebSocketListener;
import org.eclipse.jetty.websocket.api.WebSocketPolicy;
import org.eclipse.jetty.websocket.client.WebSocketClient;

import com.gargoylesoftware.htmlunit.ScriptResult;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.javascript.JavaScriptEngine;
import com.gargoylesoftware.htmlunit.javascript.configuration.JsxClass;
import com.gargoylesoftware.htmlunit.javascript.configuration.JsxConstant;
import com.gargoylesoftware.htmlunit.javascript.configuration.JsxConstructor;
import com.gargoylesoftware.htmlunit.javascript.configuration.JsxFunction;
import com.gargoylesoftware.htmlunit.javascript.configuration.JsxGetter;
import com.gargoylesoftware.htmlunit.javascript.configuration.JsxSetter;
import com.gargoylesoftware.htmlunit.javascript.host.arrays.ArrayBuffer;
import com.gargoylesoftware.htmlunit.javascript.host.event.CloseEvent;
import com.gargoylesoftware.htmlunit.javascript.host.event.Event;
import com.gargoylesoftware.htmlunit.javascript.host.event.EventTarget;
import com.gargoylesoftware.htmlunit.javascript.host.event.MessageEvent;

import net.sourceforge.htmlunit.corejs.javascript.Context;
import net.sourceforge.htmlunit.corejs.javascript.ContextAction;
import net.sourceforge.htmlunit.corejs.javascript.Function;
import net.sourceforge.htmlunit.corejs.javascript.ScriptRuntime;
import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
import net.sourceforge.htmlunit.corejs.javascript.Undefined;

/**
 * A JavaScript object for {@code WebSocket}.
 *
 * @author Ahmed Ashour
 * @author Ronald Brill
 * @author Madis Pärn
 *
 * @see Mozilla documentation
 */
@JsxClass
public class WebSocket extends EventTarget implements AutoCloseable {

    private static final Log LOG = LogFactory.getLog(WebSocket.class);

    /** The connection has not yet been established. */
    @JsxConstant
    public static final int CONNECTING = 0;
    /** The WebSocket connection is established and communication is possible. */
    @JsxConstant
    public static final int OPEN = 1;
    /** The connection is going through the closing handshake. */
    @JsxConstant
    public static final int CLOSING = 2;
    /** The connection has been closed or could not be opened. */
    @JsxConstant
    public static final int CLOSED = 3;

    private Function closeHandler_;
    private Function errorHandler_;
    private Function messageHandler_;
    private Function openHandler_;
    private URI url_;
    private int readyState_ = CONNECTING;
    private String binaryType_ = "blob";

    private HtmlPage containingPage_;
    private WebSocketClient client_;
    private volatile Session incomingSession_;
    private Session outgoingSession_;
    private WebSocketListener listener_;

    /**
     * Creates a new instance.
     */
    public WebSocket() {
    }

    /**
     * Sets the {@link WebSocketListener}.
     *
     * @param listener the {@link WebSocketListener}
     */
    public void setWebSocketListener(final WebSocketListener listener) {
        listener_ = listener;
    }

    /**
     * Returns the the {@link WebSocketListener}.
     *
     * @return the the {@link WebSocketListener}
     */
    public WebSocketListener getWebSocketListener() {
        return listener_;
    }

    /**
     * Creates a new instance.
     * @param url the URL to which to connect
     * @param window the top level window
     */
    private WebSocket(final String url, final Window window) {
        try {
            containingPage_ = (HtmlPage) window.getWebWindow().getEnclosedPage();
            setParentScope(window);
            setDomNode(containingPage_.getDocumentElement(), false);

            final WebClient webClient = window.getWebWindow().getWebClient();
            if (webClient.getOptions().isUseInsecureSSL()) {
                client_ = new WebSocketClient(new SslContextFactory(true));
            }
            else {
                client_ = new WebSocketClient();
            }
            client_.setCookieStore(new WebSocketCookieStore(webClient));

            final WebSocketPolicy policy = client_.getPolicy();
            int size = webClient.getOptions().getWebSocketMaxBinaryMessageSize();
            if (size > 0) {
                policy.setMaxBinaryMessageSize(size);
            }
            size = webClient.getOptions().getWebSocketMaxBinaryMessageBufferSize();
            if (size > 0) {
                policy.setMaxBinaryMessageBufferSize(size);
            }
            size = webClient.getOptions().getWebSocketMaxTextMessageSize();
            if (size > 0) {
                policy.setMaxTextMessageSize(size);
            }
            size = webClient.getOptions().getWebSocketMaxTextMessageBufferSize();
            if (size > 0) {
                policy.setMaxTextMessageBufferSize(size);
            }

            client_.start();
            containingPage_.addAutoCloseable(this);
            url_ = new URI(url);

            webClient.getInternals().created(this);

            final Future connectFuture = client_.connect(new WebSocketImpl(), url_);
            client_.getExecutor().execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        readyState_ = CONNECTING;
                        incomingSession_ = connectFuture.get();
                    }
                    catch (final Exception e) {
                        LOG.error("WS connect error", e);
                    }
                }
            });
        }
        catch (final Exception e) {
            LOG.error("WebSocket Error: 'url' parameter '" + url + "' is invalid.", e);
            throw Context.reportRuntimeError("WebSocket Error: 'url' parameter '" + url + "' is invalid.");
        }
    }

    /**
     * JavaScript constructor.
     * @param cx the current context
     * @param args the arguments to the WebSocket constructor
     * @param ctorObj the function object
     * @param inNewExpr Is new or not
     * @return the java object to allow JavaScript to access
     */
    @JsxConstructor
    public static Scriptable jsConstructor(
            final Context cx, final Object[] args, final Function ctorObj,
            final boolean inNewExpr) {
        if (args.length < 1 || args.length > 2) {
            throw Context.reportRuntimeError(
                    "WebSocket Error: constructor must have one or two String parameters.");
        }
        if (args[0] == Undefined.instance) {
            throw Context.reportRuntimeError("WebSocket Error: 'url' parameter is undefined.");
        }
        if (!(args[0] instanceof String)) {
            throw Context.reportRuntimeError("WebSocket Error: 'url' parameter must be a String.");
        }
        final String url = (String) args[0];
        if (StringUtils.isBlank(url)) {
            throw Context.reportRuntimeError("WebSocket Error: 'url' parameter must be not empty.");
        }
        return new WebSocket(url, getWindow(ctorObj));
    }

    /**
     * Returns the event handler that fires on close.
     * @return the event handler that fires on close
     */
    @JsxGetter
    public Function getOnclose() {
        return closeHandler_;
    }

    /**
     * Sets the event handler that fires on close.
     * @param closeHandler the event handler that fires on close
     */
    @JsxSetter
    public void setOnclose(final Function closeHandler) {
        closeHandler_ = closeHandler;
    }

    /**
     * Returns the event handler that fires on error.
     * @return the event handler that fires on error
     */
    @JsxGetter
    public Function getOnerror() {
        return errorHandler_;
    }

    /**
     * Sets the event handler that fires on error.
     * @param errorHandler the event handler that fires on error
     */
    @JsxSetter
    public void setOnerror(final Function errorHandler) {
        errorHandler_ = errorHandler;
    }

    /**
     * Returns the event handler that fires on message.
     * @return the event handler that fires on message
     */
    @JsxGetter
    public Function getOnmessage() {
        return messageHandler_;
    }

    /**
     * Sets the event handler that fires on message.
     * @param messageHandler the event handler that fires on message
     */
    @JsxSetter
    public void setOnmessage(final Function messageHandler) {
        messageHandler_ = messageHandler;
    }

    /**
     * Returns the event handler that fires on open.
     * @return the event handler that fires on open
     */
    @JsxGetter
    public Function getOnopen() {
        return openHandler_;
    }

    /**
     * Sets the event handler that fires on open.
     * @param openHandler the event handler that fires on open
     */
    @JsxSetter
    public void setOnopen(final Function openHandler) {
        openHandler_ = openHandler;
    }

    /**
     * Returns The current state of the connection. The possible values are: {@link #CONNECTING}, {@link #OPEN},
     * {@link #CLOSING} or {@link #CLOSED}.
     * @return the current state of the connection
     */
    @JsxGetter
    public int getReadyState() {
        return readyState_;
    }

    /**
     * @return the url
     */
    @JsxGetter
    public String getUrl() {
        if (url_ == null) {
            throw ScriptRuntime.typeError("invalid call");
        }
        return url_.toString();
    }

    /**
     * @return the sub protocol used
     */
    @JsxGetter
    public String getProtocol() {
        return "";
    }

    /**
     * @return the sub protocol used
     */
    @JsxGetter
    public long getBufferedAmount() {
        return 0L;
    }

    /**
     * @return the used binary type
     */
    @JsxGetter
    public String getBinaryType() {
        return binaryType_;
    }

    /**
     * Sets the used binary type.
     * @param type the type
     */
    @JsxSetter
    public void setBinaryType(final String type) {
        if ("arraybuffer".equals(type)
            || "blob".equals(type)) {
            binaryType_ = type;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void close() throws Exception {
        close(null, null);
    }

    /**
     * Closes the WebSocket connection or connection attempt, if any.
     * If the connection is already {@link #CLOSED}, this method does nothing.
     * @param code A numeric value indicating the status code explaining why the connection is being closed
     * @param reason A human-readable string explaining why the connection is closing
     */
    @JsxFunction
    public void close(final Object code, final Object reason) {
        if (readyState_ != CLOSED) {
            if (incomingSession_ != null) {
                incomingSession_.close();
            }
            if (outgoingSession_ != null) {
                outgoingSession_.close();
            }
        }

        try {
            if (client_ != null) {
                try {
                    client_.stop();
                }
                catch (final UpgradeException e) {
                    LOG.error("WS stop error (connection was not established so far)", e);
                }
                client_ = null;
            }
        }
        catch (final Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Transmits data to the server over the WebSocket connection.
     * @param content the body of the message being sent with the request
     */
    @JsxFunction
    public void send(final Object content) {
        try {
            if (content instanceof String) {
                outgoingSession_.getRemote().sendString((String) content);
            }
            else if (content instanceof ArrayBuffer) {
                final byte[] bytes = ((ArrayBuffer) content).getBytes();
                final ByteBuffer buffer = ByteBuffer.wrap(bytes);
                outgoingSession_.getRemote().sendBytes(buffer);
            }
            else {
                throw new IllegalStateException(
                        "Not Yet Implemented: WebSocket.send() was used to send non-string value");
            }
        }
        catch (final IOException e) {
            LOG.error("WS send error", e);
        }
    }

    private void fire(final Event evt) {
        evt.setTarget(this);
        evt.setParentScope(getParentScope());
        evt.setPrototype(getPrototype(evt.getClass()));

        final JavaScriptEngine engine
            = (JavaScriptEngine) containingPage_.getWebClient().getJavaScriptEngine();
        engine.getContextFactory().call(new ContextAction() {
            @Override
            public ScriptResult run(final Context cx) {
                return executeEventLocally(evt);
            }
        });
    }

    private void callFunction(final Function function, final Object[] args) {
        if (function == null) {
            return;
        }
        final Scriptable scope = function.getParentScope();
        final JavaScriptEngine engine
            = (JavaScriptEngine) containingPage_.getWebClient().getJavaScriptEngine();
        engine.callFunction(containingPage_, function, scope, this, args);
    }

    private class WebSocketImpl extends WebSocketAdapter {

        @Override
        public void onWebSocketConnect(final Session session) {
            if (listener_ != null) {
                listener_.onWebSocketConnect(session);
            }
            super.onWebSocketConnect(session);
            readyState_ = OPEN;
            outgoingSession_ = session;

            final Event openEvent = new Event();
            openEvent.setType(Event.TYPE_OPEN);
            fire(openEvent);
            callFunction(openHandler_, ArrayUtils.EMPTY_OBJECT_ARRAY);
        }

        @Override
        public void onWebSocketClose(final int statusCode, final String reason) {
            if (listener_ != null) {
                listener_.onWebSocketClose(statusCode, reason);
            }
            super.onWebSocketClose(statusCode, reason);
            readyState_ = CLOSED;
            outgoingSession_ = null;

            final CloseEvent closeEvent = new CloseEvent();
            closeEvent.setCode(statusCode);
            closeEvent.setReason(reason);
            closeEvent.setWasClean(true);
            fire(closeEvent);
            callFunction(closeHandler_, new Object[] {closeEvent});
        }

        @Override
        public void onWebSocketText(final String message) {
            if (listener_ != null) {
                listener_.onWebSocketText(message);
            }
            super.onWebSocketText(message);

            final MessageEvent msgEvent = new MessageEvent(message);
            msgEvent.setOrigin(getUrl());
            fire(msgEvent);
            callFunction(messageHandler_, new Object[] {msgEvent});
        }

        @Override
        public void onWebSocketBinary(final byte[] data, final int offset, final int length) {
            if (listener_ != null) {
                listener_.onWebSocketBinary(data, offset, length);
            }
            super.onWebSocketBinary(data, offset, length);

            final ArrayBuffer buffer = new ArrayBuffer(Arrays.copyOfRange(data, offset, length));
            buffer.setParentScope(getParentScope());
            buffer.setPrototype(getPrototype(buffer.getClass()));

            final MessageEvent msgEvent = new MessageEvent(buffer);
            msgEvent.setOrigin(getUrl());
            fire(msgEvent);
            callFunction(messageHandler_, new Object[] {msgEvent});
        }

        @Override
        public void onWebSocketError(final Throwable cause) {
            if (listener_ != null) {
                listener_.onWebSocketError(cause);
            }
            super.onWebSocketError(cause);
            readyState_ = CLOSED;
            outgoingSession_ = null;

            final Event errorEvent = new Event();
            errorEvent.setType(Event.TYPE_ERROR);
            fire(errorEvent);
            callFunction(errorHandler_, new Object[] {errorEvent});

            final CloseEvent closeEvent = new CloseEvent();
            closeEvent.setCode(1006);
            closeEvent.setReason(cause.getMessage());
            closeEvent.setWasClean(false);
            fire(closeEvent);
            callFunction(closeHandler_, new Object[] {closeEvent});
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy