// 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