// Copyright (c) 2016-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.
//
/** This accessor starts a server that listens for web socket
* connection requests on the specified hostInterface and port. The
* hostInterface is needed only if the host machine has more than one
* network interface (e.g. Ethernet and WiFi) and 'localhost' does
* not resolve to the desired interface.
*
* The output `connection` reports when a
* connection is opened or closed.
* The output is an object with two fields, a 'socketID',
* which is a unique ID for this client connection, and a 'status' field,
* which is the string 'open' or 'closed'.
*
* When a message arrives on a connection, a `received`
* output is produced with that message. Note that the message may arrive in
* multiple frames, but it will be produced on this port as a single message.
* The output is an object with two fields,
* a 'socketID', which is a unique ID for this client connection, and a 'message' field,
* which is the message received from the client.
*
* When an input arrives on `toSend`, then a message is
* sent to one or all of the open socket connections.
* If this is an object with 'socketID' field and a 'message' field,
* then send the value of the message field to the socket identified
* by the socketID field. If the input has any other form, then the
* message is broadcast to all open socket connections.
*
* When `wrapup()` is invoked, this accessor closes the
* server and all connections.
*
* The default type for both sending and receiving
* is 'application/json', which allows sending and receiving anything that has
* a string representation in JSON. The types supported by this implementation
* include at least:
* * __application/json__: The this.send() function uses JSON.stringify() and sends the
* result with a UTF-8 encoding. An incoming byte stream will be parsed as JSON,
* and if the parsing fails, will be provided as a string interpretation of the byte
* stream.
* * __text/\*__: Any text type is sent as a string encoded in UTF-8.
* * __image/x__: Where __x__ is one of __json__, __png__, __gif__,
* and more.
* In this case, the data passed to this.send() is assumed to be an image, as encoded
* on the host, and the image will be encoded as a byte stream in the specified
* format before sending. A received byte stream will be decoded as an image,
* if possible.
*
* This accessor requires the module webSocket.
*
* @accessor net/WebSocketServer
* @parameter {string} hostInterface The IP address or domain name of the
* network interface to listen to.
* @parameter {int} port The port to listen to for connections.
* @parameter {string} pfxKeyCertPassword If sslTls is set to true, then this option needs
* to specify the password for the pfx key-cert file specified by pfxKeyCertPath.
* @parameter {string} pfxKeyCertPath If sslTls is set to true, then this option needs to
* specify the fully qualified filename for the file that stores the private key and certificate
* that this server will use to identify itself. This path can be any of those understood by the
* Ptolemy host, e.g. paths beginning with $CLASSPATH/.
* @parameter {string} receiveType The MIME type for incoming messages,
* which defaults to 'application/json'.
* @parameter {string} sendType The MIME type for outgoing messages,
* which defaults to 'application/json'.
* @parameter {boolean} sslTls Whether SSL/TLS is enabled. This defaults to false.
* @input toSend The data to be sent to open sockets.
* If this is an object with 'socketID' field and a 'message' field,
* then send the value of the message field to the socket identified
* by the socketID field. If the input has any other form, then the
* message is broadcast to all open socket connections.
* @output {int} listening When the server is listening for connections, this output
* will produce the port number that the server is listening on
* @output connection An output produced when a connection opens or closes.
* The output is an object with two fields, a 'socketID',
* which is a unique ID for this client connection, and a 'status' field,
* which is the string 'open' or 'closed'.
* @output received A message received a client in the form of an object with two fields,
* a 'socketID', which is a unique ID for this client connection, and a 'message' field,
* which is the message received from the client.
* @author Hokeun Kim, Edward Lee
* @version $$Id$$
*/
// Stop extra messages from jslint and jshint. Note that there should
// be no space between the / and the * and global. See
// https://chess.eecs.berkeley.edu/ptexternal/wiki/Main/JSHint */
/*globals console, error, exports, require */
/*jshint globalstrict: true*/
'use strict';
/*jslint plusplus: true */
var WebSocket = require('@accessors-modules/web-socket-server');
var server = null;
var running = false;
var debug = false;
/** Sets up the accessor by defining inputs and outputs. */
exports.setup = function () {
this.parameter('hostInterface', {
value: "localhost",
type: "string"
});
this.parameter('port', {
value: 8080,
type: "int"
});
this.parameter('pfxKeyCertPassword', {
value: '',
type: 'string'
});
this.parameter('pfxKeyCertPath', {
value: '',
type: 'string'
});
this.parameter('receiveType', {
type: 'string',
value: 'application/json'
});
this.parameter('sendType', {
type: 'string',
value: 'application/json'
});
this.parameter('sslTls', {
type: 'boolean',
value: false
});
this.input('toSend');
this.output('received', {
'spontaneous': true
});
this.output('listening', {
'type': 'int',
'spontaneous': true
});
this.output('connection', {
'spontaneous': true
});
// Attempt to add a list of options for types, but do not error out
// if the socket module is not supported by the host.
try {
this.parameter('receiveType', {
options: WebSocket.supportedReceiveTypes()
});
this.parameter('sendType', {
options: WebSocket.supportedSendTypes()
});
} catch (err) {
error(err);
}
};
//var sockets = [];
/** Starts the web socket and attaches functions to inputs and outputs.
* Adds an input handler on toSend that sends the input received to the right socket. */
exports.initialize = function () {
var self = this;
self.sockets = [];
if (!server) {
server = new WebSocket.Server({
'port': this.getParameter('port'),
'hostInterface': this.getParameter('hostInterface'),
'pfxKeyCertPassword': this.getParameter('pfxKeyCertPassword'),
'pfxKeyCertPath': this.getParameter('pfxKeyCertPath'),
'receiveType': this.getParameter('receiveType'),
'sendType': this.getParameter('sendType'),
'sslTls': this.getParameter('sslTls')
});
// Using 'this.exports' rather than just 'exports' in the following allows
// these functions to be overridden in derived accessors.
server.on('listening', this.exports.onListening.bind(this));
server.on('connection', this.exports.onConnection.bind(this));
server.on('error', function (message) {
self.error(message);
});
server.start();
}
running = true;
this.addInputHandler('toSend', function () {
var data = self.get('toSend'),
id;
// Careful: Don't do if (data) because if data === 0, then data is false.
if (data !== null) {
// JSHint WARNING: Do not change dataSocketID != null to
// data.socketID !== null because it will cause
// org/terraswarm/accessor/test/auto/WebSocketClient.xml
// to fail upon reloading. See
// org/terraswarm/accessor/test/WebSocketClientTest.tcl
if ((data.socketID != null) && (data.message !== null)) {
// data has the right form for a point-to-point send.
if (self.sockets[data.socketID] && self.sockets[data.socketID].isOpen()) {
// id matches this socket.
/*
console.log(self.accessorName + ': WebSocketServer: Sending to socket id ' +
data.socketID +
" message: " +
data.message);
*/
self.sockets[data.socketID].send(data.message);
} else {
console.log(self.accessorName + ': WebSocketServer.js: Socket with ID ' + data.socketID +
' is not open. Discarding message.');
}
} else {
// No socketID or message, so this is a broadcast message.
// var success = false;
for (id = 0; id < self.sockets.length; id += 1) {
if (self.sockets[id].isOpen()) {
// console.log(self.accessorName + 'WebSocketServer.js: Broadcasting to socket id ' + id
// + ' message: ' + data);
self.sockets[id].send(data);
// success = true;
}
}
// if (!success) {
// console.log(self.accessorName + 'WebSocketServer.js: No open sockets. Discarding message: ' + data.message);
// }
}
}
});
};
exports.onListening = function () {
if (debug) {
console.log(this.accessorName + 'WebSocketServer.js: Listening for socket connection requests.');
}
this.send('listening', this.getParameter('port'));
};
/** Executes when a connection has been established.<br>
* Triggers an output on <code>'connection'</code>.
* Adds an event listener to the socket. */
exports.onConnection = function (socket) {
// socketID is the index of the socket in the sockets array.
var self = this,
socketID = self.sockets.length;
if (debug) {
console.log(this.accessorName + 'WebSocketServer.js: new socket established with ID: ' + socketID);
}
this.send('connection', {
'socketID': socketID,
'status': 'open'
});
self.sockets.push(socket);
self.sockets[socketID].on('message', function (message) {
if(debug){
console.log('WebSocketServer message(): ' + message + ', typeof message: ' + typeof message);
}
// For some reason, under the Node Host, the message is an
// object. Under CapeCode, it is a string?
var isObjectWithQuotes = (typeof message === 'object' && message.toString().startsWith('"') && message.toString().endsWith('"'));
// If message is a string, strip leading and trailing "
if (typeof message === 'string' || isObjectWithQuotes) {
if (isObjectWithQuotes) {
message = message.toString();
}
message = message.replace(/^"(.*)"$/, '$1');
}
var filteredMessage = self.exports.filterReceived.call(self, message, socketID);
self.send('received', {
'message': filteredMessage,
'socketID': socketID
});
});
self.sockets[socketID].on('close', function () {
self.exports.notifyClose.call(self, socketID);
self.send('connection', {
'socketID': socketID,
'status': 'closed'
});
});
self.sockets[socketID].on('error', function (message) {
console.log(self.accessorName + ': WebSocketServer.js: error ' + message);
self.error(message);
});
};
/** Filter the received message. This base class just returns the argument
* unmodified, but derived classes can override this to extract
* a portion of the message, for example.
* @param message The message.
*/
exports.filterReceived = function (message, socketID) {
return message;
};
/** Notify that the socketID is now closed. This base class does nothing,
* but derived classes can override this to be notified of a closing socket,
* for example.
*/
exports.notifyClose = function(socketID){
return;
}
/** Removes all inputHandlers from sockets.<br>
* Unregisters event listeners from sockets.<br>
* Closes server.
*/
exports.wrapup = function () {
this.sockets = [];
if (server !== null) {
server.stop();
server = null;
}
};