Source: org/terraswarm/accessor/accessors/web/hosts/node/node_modules/@accessors-modules/web-socket-server/web-socket-server.js

// Copyright (c) 2015-2017 The Regents of the University of California.
// All rights reserved.

// Permission is hereby granted, without written agreement and without
// license or royalty fees, to use, copy, modify, and distribute this
// software and its documentation for any purpose, provided that the above
// copyright notice and the following two paragraphs appear in all copies
// of this software.

// IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
// ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
// THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.

// THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
// PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
// CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
// ENHANCEMENTS, OR MODIFICATIONS.

/**
 * Module supporting web socket servers. Web sockets differ from HTTP
 * interactions by including a notion of a bidirectional connection
 * called a "socket". It differs from a TCP socket in that the connection
 * carries not just a byte stream, but a sequence of "messages," where
 * each message can have an arbitrary number of bytes. It also differs
 * from a TCP socket in that the connection is established through HTTP
 * and is supported by most web browsers.
 * 
 * This module defines two classes, Server, and Socket.
 * To make a connection, create an instance of Server, set up event listeners,
 * and start the server. On another machine (or the same machine), create
 * an instance of Client (defined in the webSocketClient module)
 * and set up listeners and/or invoke the send() function
 * of the client to send a message. When a client connects to the Server,
 * the Server will create an instance of the Socket object. This object
 * can be used to send and receive messages to and from the client.
 *
 * This module also provides two utility functions that return arrays
 * of MIME types supported for sending or receiving messages.
 * Specifying a message type facilitates conversion between the byte
 * streams transported over the socket and JavaScript objects that
 * are passed to send() or emitted as a 'message' event.
 *
 * @module @accessors-modules/web-socket-server
 * @author Christopher Brooks, Edward A. Lee, based on Cape Code webSocketServer.js by Hokeun Kim and Edward A. Lee
 * @version $$Id$$
 */

// Stop extra messages from jslint.  Note that there should be no
// space between the / and the * and global.
/*globals console, exports, require */
/*jshint globalstrict: true */
"use strict";

var EventEmitter = require('events').EventEmitter;
var util = require('util');
var ws = require('ws');
var debug = false;

///////////////////////////////////////////////////////////////////////////////
//// supportedReceiveTypes

/** Return an array of the types supported by the current host for
 *  receiveType arguments.
 */
exports.supportedReceiveTypes = function () {
    // These values are based on what Cape Code returns in
    // ptolemy.actor.lib.jjs.modules.webSocket.WebSocketHelper.supportedReceiveTypes().
    // Not sure about the validity of 'JPG' and subsequent args.
    return ['application/json', 'text/plain',
            'JPG', 'jpg', 'bmp', 'BMP', 'gif', 'GIF', 'WBMP', 'png', 'PNG', 'jpeg', 'wbmp', 'JPEG'];
};

///////////////////////////////////////////////////////////////////////////////
//// supportedSendTypes

/** Return an array of the types supported by the current host for
 *  sendType arguments.
 */
exports.supportedSendTypes = function () {
    // These values are based on what Cape Code returns in
    // ptolemy.actor.lib.jjs.modules.webSocket.WebSocketHelper.supportedSendTypes().
    // FIXME: Not sure about the validity of 'JPG' and subsequent args.
    return ['application/json', 'text/plain',
            'JPG', 'jpg', 'bmp', 'BMP', 'gif', 'GIF', 'WBMP', 'png', 'PNG', 'jpeg', 'wbmp', 'JPEG'];
};

///////////////////////////////////////////////////////////////////////////////
//// Server

/** Construct an instance of WebSocket Server.
 *  After invoking this constructor (using new), the user script should set up listeners
 *  and then invoke the start() function on this Server.
 *  This will create an HTTP server on the local host.
 *  The options argument is a JSON object containing the following optional fields:
 *  * hostInterface: The IP address or name of the local interface for the server
 *    to listen on.  This defaults to "localhost", but if the host machine has more
 *    than one network interface, e.g. an Ethernet and WiFi interface, then you may
 *    need to specifically specify the IP address of that interface here.
 *  * port: The port on which to listen for connections (the default is 80,
 *    which is the default HTTP port).
 *  * receiveType: The MIME type for incoming messages, which defaults to 'application/json'.
 *    See the Client documentation for supported types.
 *  * sendType: The MIME type for outgoing messages, which defaults to 'application/json'.
 *    See the Client documentation for supported types.
 * 
 *  This subclasses EventEmitter, emitting events 'listening' and 'connection'.
 *  A typical usage pattern looks like this:
 * 
 *  <pre>
 *     var webSocket = require('webSocketServer');
 *     var server = new webSocket.Server({'port':8082});
 *     server.on('listening', onListening);
 *     server.on('connection', onConnection);
 *     server.start();
 *  </pre>
 * 
 *  where onListening is a handler for an event that this Server emits
 *  when it is listening for connections, and onConnection is a handler
 *  for an event that this Server emits when a client requests a websocket
 *  connection and the socket has been successfully established.
 *  When the 'connection' event is emitted, it will be passed a Socket object,
 *  and the onConnection handler can register a listener for 'message' events
 *  on that Socket object, as follows:
 * 
 *  <pre>
 *    server.on('connection', function(socket) {
 *        socket.on('message', function(message) {
 *            console.log(message);
 *            socket.send('Reply message');
 *        });
 *     });
 *  </pre>
 * 
 *  The Socket object also has a close() function that allows the server to close
 *  the connection.
 * 
 *  FIXME: Should provide a mechanism to validate the "Origin" header during the
 *    connection establishment process on the serverside (against the expected origins)
 *    to avoid Cross-Site WebSocket Hijacking attacks.
 *
 *  @param options The options.
 */
exports.Server = function (options) {
    // console.log("webSocketServer.Server()");
    if (typeof options.port === 'undefined' || options.port === null) {
        this.port = 80;
    } else {
        this.port = options.port;
    }
    this.hostInterface = options.hostInterface || 'localhost';
    this.sslTls = options.sslTls || false;
    this.pfxKeyCertPassword = options.pfxKeyCertPassword || '';
    this.pfxKeyCertPath = options.pfxKeyCertPath || '';
    this.receiveType = options.receiveType || 'application/json';
    this.sendType = options.sendType || 'application/json';
};

util.inherits(exports.Server, EventEmitter);

/** Start the server. */
exports.Server.prototype.start = function () {
    // console.log("webSocketServer.start(): " + this.hostInterface + " " + this.port);
    
    // The ws module starts the server when you create an instance of ws.Server,
    // so this needs to be done here rather than in the constructor or we will
    // start the server prematurely.
    this.helper = new ws.Server({ 'host': this.hostInterface,
                                  'port': this.port
                                });
    // Register handlers. Note that in Node, this start function will be
    // completed before any of these handlers is invoked, so there is no race
    // condition caused by the fact that we already issued the command above
    // to start the server.
    this.helper.on('listening', function() {
        if (debug) {
            console.log('webSocketServer.start(): listening event');
        }
    });

    var self = this;

    // "When a new WebSocket connection is established. socket is an
    // object of type ws.WebSocket."
    this.helper.on('connection', function(socket) {
        // console.log('webSocketServer.start(): connection event');
        socket.on( 'message', function ( message ) {
            if (typeof message === 'string' && message.length > 70) {
                if (debug) {
                    console.log(self.accessorName + ': webSocketServer.js: message: ' + message.substring(0, 70) + '...');
                }
            } else {
                if (debug) {
                    console.log(self.accessorName + ': webSocketServer.js: message: ' + message);
                }
            }
        });

        // The node ws WebSocket definition in
        // node_modules/ws/lib/WebSocket.js does not have an isOpen()
        // function, so we define one here.
        socket.isOpen = function() {
            // console.log('webSocketServer.js: socket isOpen() xxxxxxxxxxx');
            return socket.readyState === ws.OPEN;
        };

        self.emit('connection', socket);
    });
    
    // "If the underlying server emits an error, it will be forwarded
    // here."
    this.helper.on('error', function(message) {
        // console.log(self.accessorName + ': webSocketServer.start(): error event: ' + message);
        self.emit('error', message);
    });

    // "Emitted with the object of HTTP headers that are going to be
    // written to the Stream as part of the handshake."
    this.helper.on('headers', function(headers) {
        // console.log(self.accessorName + ': webSocketServer.start(): headers event: ' + headers);
        self.emit('headers', headers);
    });
};

/** Stop the server. Note that this closing happens
 *  asynchronously. The server may not be closed when this returns.
 */
exports.Server.prototype.stop = function () {
    // console.log('webSocketServer.stop()');

    // "Close the server and terminate all clients, calls callback when done with an error if one occured."
    this.helper.close(/* callback */);
};

// Most hosts would define a Socket object here, but we are fortunate
// in that the WebSocket functions nicely match the functions required
// by the WebSocketServer Socket class as defined in
// https://www.icyphy.org/accessors/wiki/VersionCurrent/WebSocketServer#SocketClass