Source: org/terraswarm/accessor/accessors/web/net/SecureCommServer.js

// Copyright (c) 2016 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 is used for accessing authorization service provided by
 *  a local authorization entity, Auth (https://github.com/iotauth/iotauth),
 *  and for secure communication with a SecureCommClient.
 *
 *  Specifically, this access listens to secure communication requests from
 *  clients and handles the client requests.
 *
 *  This accessor internally manages the credentials (cryptographic keys)
 *  for communication with remote Auth and remote client.
 *
 *  All the messages to/from remote Auth and client are protected using
 *  the credentials, while input/output data of this accessor is in plain text.
 *
 *  This accessor requires the 'iotAuth', and 'dataConverter' modules.
 *
 *  @accessor net/SecureCommServer
 *
 *  @input toSend
 *  @input {int} toSendID
 *
 *  @output {int} listening
 *  @output connection Includes information of the remote client
 *  @output received
 *  @output receivedID
 *
 *  @parameter {string} serverName The server's unique name in string.
 *  @parameter {int} serverPort Server's port number.
 *  @parameter {string} authHost Auth's IP address or domain name.
 *  @parameter {int} authPort Auth's port number.
 *
 *  @parameter {string} authCertPath The path for the X.509 certificate file (in pem format)
 *   of Auth with which the server is registered.
 *  @parameter {string} serverPrivateKeyPath The path for the pem format private key of
 *   the server.
 *
 *  @parameter {string} publicKeyCryptoSpec The specification for the public cryptography
 *   algorithms to be used for communication with Auth
 *  @parameter {string} distributionCryptoSpec The specification for the symmetric cryptography
 *   algorithms to be used for communication with Auth
 *  @parameter {string} sessionCryptoSpec The specification for the symmetric cryptography
 *   algorithms to be used for communication with the client
 *
 *  @parameter {string} receiveType Data type of the received data from client.
 *  @parameter {string} sendType Data type of the sent data to client.
 *
 *  @author Hokeun Kim
 *  @version $$Id$$
 */

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

var iotAuth = require('@accessors-modules/iot-auth');
//var dataConverter = require('dataConverter');
var msgType = iotAuth.msgType;

exports.setup = function () {
    // Inputs and outputs
    this.input('toSend');
    this.input('toSendID', {
        type: 'int',
        value: -1
    });
    this.output('listening', {
        type: 'int'
    });
    this.output('connection');
    this.output('received', {
        spontaneous: true
    });
    this.output('receivedID', {
        type: 'int',
        spontaneous: true
    });
    // Server information
    this.parameter('serverName', {
        value: '',
        type: 'string'
    });
    this.parameter('serverPort', {
        type: 'int',
        value: 4000
    });
    // For communication with Auth
    this.parameter('authHost', {
        type: 'string',
        value: 'localhost'
    });
    this.parameter('authPort', {
        value: -1,
        type: 'int'
    });
    this.parameter('authCertPath', {
        value: '',
        type: 'string'
    });
    this.parameter('serverPrivateKeyPath', {
        value: '',
        type: 'string'
    });
    // Spec for communication with Auth
    this.parameter('publicKeyCryptoSpec', {
        type: 'string',
        options: iotAuth.publicKeyCryptoSpecs
    });
    this.parameter('distributionCryptoSpec', {
        type: 'string',
        options: iotAuth.symmetricCryptoSpecs
    });
    // For communication with client
    this.parameter('sessionCryptoSpec', {
        type: 'string',
        options: iotAuth.symmetricCryptoSpecs
    });
    // Send/receive type
    this.parameter('receiveType', {
        type: 'string',
        value: 'string',
        options: ['string', 'image', 'byteArray']
    });
    this.parameter('sendType', {
        type: 'string',
        value: 'string',
        options: ['string', 'image', 'byteArray']
    });
};


// local variables
var self;
var receiveType;
var sendType;
var authPublicKey;
var serverPrivateKey;
var currentDistributionKey = null;
var server = null;
var sockets = [];
var currentSessionKey = null;

function outputError(errorMessage) {
    console.log(errorMessage);
    self.error(errorMessage);
}

/*
  callbackParameters = {
  keyId,
  sendHandshake2Callback,
  handshake1Payload,
  serverSocket
  }
*/
function sessionKeyResponseCallback(status, distributionKey, sessionKeyList, callbackParameters) {
    if (status.error) {
        console.log('session key request failed: ' + status.error);
        return;
    }
    console.log('session key request succeeded');

    if (distributionKey) {
        console.log('Updating to a new distribution key key');
        currentDistributionKey = distributionKey;
    }

    console.log('received ' + sessionKeyList.length + ' session keys');
    var receivedSessionKey;
    for (var i = 0; i < sessionKeyList.length; i++) {
        receivedSessionKey = sessionKeyList[i];
    }
    console.log('Session key arrived');
    if (receivedSessionKey.id == callbackParameters.keyId) {
        console.log('Session key id is as expected');
        currentSessionKey = receivedSessionKey;
        callbackParameters.sendHandshake2Callback(callbackParameters.handshake1Payload,
            callbackParameters.serverSocket, receivedSessionKey);
    } else {
        outputError('Session key id is NOT as expected');
    }
}

// event handlers for the listening server
function onServerListening(listeningPort) {
    console.log('Server: Listening for socket connection requests on port ' + listeningPort);
    self.send('listening', listeningPort);
}

function onServerError(message) {
    outputError('Error in server - details: ' + message);
}

function onClientRequest(handshake1Payload, serverSocket, sendHandshake2Callback) {
    var keyId = handshake1Payload.readUIntBE(0, iotAuth.SESSION_KEY_ID_SIZE);
    if (currentSessionKey !== null && currentSessionKey.id === keyId) {
        sendHandshake2Callback(handshake1Payload, serverSocket, currentSessionKey);
    } else {
        console.log('session key NOT found! sending session key id to AuthService');
        var options = {
            authHost: self.getParameter('authHost'),
            authPort: self.getParameter('authPort'),
            entityName: self.getParameter('serverName'),
            numKeysPerRequest: 1,
            purpose: {
                keyId: keyId
            },
            distributionKey: currentDistributionKey,
            distributionCryptoSpec: self.getParameter('distributionCryptoSpec'),
            publicKeyCryptoSpec: self.getParameter('publicKeyCryptoSpec'),
            authPublicKey: authPublicKey,
            entityPrivateKey: serverPrivateKey
        };
        var callbackParameters = {
            keyId: keyId,
            sendHandshake2Callback: sendHandshake2Callback,
            handshake1Payload: handshake1Payload,
            serverSocket: serverSocket
        };
        iotAuth.sendSessionKeyRequest(options, sessionKeyResponseCallback, callbackParameters);
    }
}

// Event handlers for individual sockets.
function onClose(socketID) {
    console.log('secure connection with the client closed.');
    sockets[socketID] = null;
    self.send('connection', 'socket #' + socketID + ' closed');
}

function onError(message, socketID) {
    outputError('Error in secure server socket #' + socketID +
        ' details: ' + message);
}

function onConnection(socketInstance, entityServerSocket) {
    console.log('secure connection with the client established.');
    self.send('connection', socketInstance);
    sockets[socketInstance.id] = entityServerSocket;
}

function onData(data, socketID) {
    console.log('data received from server via secure communication');

    if (receiveType == 'string') {
        self.send('received', data.toString());
    } else if (receiveType == 'image') {
        self.send('received', dataConverter.jsArrayToImage(data.getArray()));
    } else if (receiveType == 'byteArray') {
        self.send('received', data.getArray());
    }
    self.send('receivedID', socketID);
}

exports.toSendInputHandler = function () {
    var toSend = this.get('toSend');
    if (sendType == 'image') {
        toSend = dataConverter.imageToJSArray(toSend);
    }

    var toSendID = this.get('toSendID');
    // broadcasting
    if (toSendID < 0) {
        for (var i = 0; i < sockets.length; i++) {
            if (!sockets[i]) {
                continue;
            }
            if (!sockets[i].checkSessionKeyValidity()) {
                outputError('session key expired!');
            } else if (!sockets[i].send(toSend)) {
                outputError('Error in sending data');
            }
        }
    } else if (sockets[toSendID]) {
        sockets[toSendID].send(toSend);
    } else {
        console.log('Socket with ID ' + idToSendTo + ' is not open. Discarding data.');
    }
};

exports.initialize = function () {
    currentSessionKey = null;
    authPublicKey = iotAuth.loadPublicKey(this.getParameter('authCertPath'));
    serverPrivateKey = iotAuth.loadPrivateKey(this.getParameter('serverPrivateKeyPath'));
    receiveType = this.getParameter('receiveType');
    sendType = this.getParameter('sendType');

    self = this;
    var options = {
        serverPort: this.getParameter('serverPort'),
        sessionCryptoSpec: this.getParameter('sessionCryptoSpec')
    };
    var eventHandlers = {
        onServerError: onServerError,
        onServerListening: onServerListening,
        onClientRequest: onClientRequest,

        onData: onData,
        onClose: onClose,
        onError: onError,
        onConnection: onConnection
    };
    server = iotAuth.initializeSecureServer(options, eventHandlers);

    this.addInputHandler('toSend', exports.toSendInputHandler.bind(this));
};

exports.wrapup = function () {
    sockets = [];
    if (server !== null) {
        server.stop();
        server = null;
    }
};