Source: api/WSS_Tunnel.js

/**
* @file WebSocket tunneling using Sessions connectivity. Once established, the tunneled
* connections may send and retreive arbitrary data (not just JSON-RPC).
*
* @version 0.4.1
*/
async function WSS_Tunnel (sessionObj) {
   if ((namespace.wss == null) || (namespace.wss == undefined)) {
      sendError(JSONRPC_ERRORS.INTERNAL_ERROR, "No WebSocket Session server defined.", sessionObj);
      return (false);
   }
   var requestData = sessionObj.requestObj;
   var requestParams = requestData.params;
   if ((requestParams.server_token == undefined) || (requestParams.server_token == null) || (requestParams.server_token == "")) {
      sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Invalid server token.", sessionObj);
      return(false);
   }
   if ((requestParams.user_token == undefined) || (requestParams.user_token == null) || (requestParams.user_token == "")) {
      sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Invalid user token.", sessionObj);
      return(false);
   }
   var responseObj = new Object();
   var connectionID = namespace.wss.makeConnectionID(sessionObj); //makeConnectionID defined in WSS_Handshake.js
   var privateID = namespace.wss.getPrivateID(sessionObj); //getPrivateID defined in WSS_Handshake.js
   if (privateID == null) {
      //must have active WSS session!
      sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Session not established.", sessionObj);
      return(false);
   }
   var connectionObject = namespace.wss.getConnectionByCID(connectionID, requestParams.user_token, requestParams.server_token);
   if (connectionObject == null) {
      //internal sessions data may be corrupted
      sendError(JSONRPC_ERRORS.INTERNAL_ERROR, "State object for connection not found.", sessionObj);
      return(false);
   }
   var userTunnels = connectionObject.tunnels;
   var numUserTunnels = userTunnels.length;
   var responseObj = new Object();
   if (typeof(requestParams.action) == "string") {
      switch (requestParams.action) {
         case "open":
            //open a new tunnel
            if (numUserTunnels >= rpc_options.max_tunnels_per_connection) {
               sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Maximum number of tunnels ("+String(numUserTunnels)+") already open.", sessionObj);
               return(false);
            }
            if (typeof(requestParams.allowPID) != "string") {
               requestParams.allowPID = "*";
            }
            if (typeof(requestParams.tunnelServerMessages) != "boolean") {
               requestParams.tunnelServerMessages = false;
            }
            connectionObject.tunnel = new Object();
            connectionObject.tunnelServerMessages = requestParams.tunnelServerMessages;
            connectionObject.tunnel.endpointPID = privateID;
            connectionObject.tunnel.allowPID = requestParams.allowPID;
            connectionObject.tunnel.alias = requestParams.alias;
            responseObj.type = "tunnel";
            responseObj.status = "open";
            break;
         case "joinAlias":
            //join an existing tunnel
            var endpointConnObj = namespace.wss.tunnel.getConnectionByAlias(requestParams.alias);
            if (endpointConnObj == null) {
               sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "No matching tunnel.", sessionObj);
               return (false);
            }
            if (((endpointConnObj.tunnel.allowPID == "*") || (endpointConnObj.tunnel.allowPID == privateID)) &&
               ((endpointConnObj.tunnel.joinedPID == undefined) || (endpointConnObj.tunnel.joinedPID == null))) {
               endpointConnObj.tunnel.joinedPID = privateID;
            } else {
               sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Tunnel not available.", sessionObj);
               return (false);
            }
            if (typeof(requestParams.tunnelServerMessages) != "boolean") {
               requestParams.tunnelServerMessages = false;
            }
            connectionObject.tunnel = new Object();
            connectionObject.tunnelServerMessages = requestParams.tunnelServerMessages
            connectionObject.tunnel.joinedPID = privateID;
            connectionObject.tunnel.endpointPID = requestParams.endpointPID;
            connectionObject.tunnel.alias = endpointConnObj.tunnel.alias;
            await connectTunnel(endpointConnObj, connectionObject);
            var joinMessage = buildJSONRPC();
            joinMessage.result.type = "tunnel";
            joinMessage.result.status = "joined";
            joinMessage.result.joinedPID = privateID;
            await sendTunnelMessage(endpointConnObj, JSON.stringify(joinMessage));
            responseObj.type = "tunnel";
            responseObj.status = "joined";
            responseObj.endpointPID = requestParams.endpointPID;
            break;
         case "close":
            //close a tunnel -- not yet supported (future version?)
            sendError(JSONRPC_ERRORS.ACTION_DISALLOWED, "Manual tunnel closure not supported.", sessionObj);
            return(false);
            break;
         default:
            sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Invalid \"action\" parameter.", sessionObj);
            return(false);
            break;
      }
   } else {
      //no "action" specified so send
      if (requestParams.data == undefined) {
         //null is allowed
         sendError(JSONRPC_ERRORS.INVALID_PARAMS_ERROR, "Missing \"data\" parameter.", sessionObj);
         return(false);
      }
   }
   var requestData = sessionObj.requestObj;
   var responseData = buildJSONRPC();
   //copy id from request to maintain session continuity
   if ((requestData["id"] == null) || (requestData["id"] == undefined)) {
      responseData.id = null;
   } else {
      responseData.id = requestData.id;
   }
   responseData.result = responseObj;
   //use sendTunnelMessage instead of sendResult to ensure that message always gets to recipient, even
   //after a tunnel is established and communication is being intercepted.
   await sendTunnelMessage(connectionObject, JSON.stringify(responseData));
   return(true);
}

/**
* Returns a connection object from <code>namespace.wss.connections</code>
* based on a tunnel alias.
*
* @param {String} alias The tunnel alias for which to retrieve the
* connection object.
*
* @return {Object} The connection object matching the tunnel alias or <code>null</code>
* if no such connection exists. In the unlikely event that more than one matchibng
* connection object exists, only the first one is returned.
*/
function getConnectionByAlias(alias) {
   if ((namespace.wss.connections == undefined) || (namespace.wss.connections == null)) {
      namespace.wss.connections = new Object();
      return (null);
   }
   for (var connectionID in namespace.wss.connections) {
      var connectionsArr = namespace.wss.connections[connectionID];
      for (var count=0; count < connectionsArr.length; count++) {
         var connectionObj = connectionsArr[count];
         if (connectionObj.tunnel != undefined) {
            if (connectionObj.tunnel.alias == alias) {
               return (connectionObj);
            }
         }
      }
   }
   return (null);
}

/**
* Connects two ends of a tunnel by intercepting message sending, receiving, and
* socket closure at the WebSocket level.
*
* @param {Object} connectionObject1 The object containing a <code>socket</code> reference,
* <code>private_id</code>, <code>tunnel</code> object, and other information to connect
* to <code>connectionObject2</code>.
* @param {Object} connectionObject2 The object containing a <code>socket</code> reference,
* <code>private_id</code>, <code>tunnel</code> object, and other information to connect
* to <code>connectionObject1</code>.
*
* @async
*/
async function connectTunnel(connectionObject1, connectionObject2) {
   connectionObject1.socket._send = connectionObject1.socket.send;
   connectionObject1.socket.send = (data) => {
      sendIntercept(connectionObject1, data);
   }
   connectionObject2.socket._send = connectionObject2.socket.send;
   connectionObject2.socket.send = (data) => {
      sendIntercept(connectionObject2, data);
   }
   var msgListeners = connectionObject1.socket.rawListeners("message");
   var closeListeners = connectionObject1.socket.rawListeners("close");
   for (var count=0; count < msgListeners.length; count++) {
      var listenerFunc = msgListeners[count];
      //swap listeners
      connectionObject1.socket.off ("message", listenerFunc);
      connectionObject1.socket.on ("message", (message) => {
         onTunnelMessage(connectionObject1, connectionObject2, message);
      });
   }
   listenerFunc = closeListeners[0];
   connectionObject1.socket.off ("close", listenerFunc);
   connectionObject1.socket.on ("close", (closeEvent) => {
      onTunnelClose(connectionObject1, connectionObject2, listenerFunc, closeEvent);
   });
   msgListeners = connectionObject2.socket.rawListeners("message");
   closeListeners = connectionObject2.socket.rawListeners("close");
   for (count=0; count < msgListeners.length; count++) {
      var listenerFunc = msgListeners[count];
      //swap listeners
      connectionObject2.socket.off ("message", listenerFunc);
      connectionObject2.socket.on ("message", (message) => {
         onTunnelMessage(connectionObject2, connectionObject1, message);
      });
   }
   listenerFunc = closeListeners[0];
   connectionObject2.socket.off ("close", listenerFunc);
   connectionObject2.socket.on ("close", (closeEvent) => {
      onTunnelClose(connectionObject2, connectionObject1, listenerFunc, closeEvent);
   });
   return (true);
}

/**
* Disconnects a single tunnel end, optionally keeping it open for a future
* connection.
*
* @param {Object} connectionObject The connection object containing a <code>socket</code>
* reference, <code>private_id</code>, <code>tunnel</code> information object,
* <code>user_token</code>, <code>server_token</code>, and other information
* for the tunneled connection.
* @param {Boolean} [remain=true] If false, the connection will no longer be available
* for a new connection. A client connection should almost never remain but
* an endpoint may.
*
* @async
*/
async function disconnectTunnel(connectionObject, remain=true) {
   connectionObject.socket.send = connectionObject.socket._send;
   if (remain == false) {
      var msgListeners = connectionObject.socket.rawListeners("message");
      var closeListeners = connectionObject.socket.rawListeners("close");
      for (var count=0; count < msgListeners.length; count++) {
         var listenerFunc = msgListeners[count];
         //swap listeners
         connectionObject.socket.off ("message", listenerFunc);
      }
      listenerFunc = closeListeners[0];
      connectionObject.socket.off ("close", listenerFunc);
   }
   return (true);
}

/**
* A proxy or intercept function for a standard WebSocket <code>send</code> when the
* socket is being tunneled.<br/>
* Typically this function is swapped into the WebSocket instance in
* {@link connectTunnel} where the extra parameter is introduced.
*
* @param {Object} connectionObj The connection object containing a <code>socket</code>
* reference, <code>private_id</code>, <code>tunnel</code> information object,
* <code>tunnelServerMessages</code>, and other information for the tunneled connection.
* @param {*} data The message to send to the tunneled socket.
*
* @async
*/
async function sendIntercept(connectionObj, data) {
   if (connectionObj.tunnelServerMessages == true) {
      connectionObj.socket._send(data);
   }
}

/**
* Sends a message directly to a tunneled WebSocket. Used when the
* message would otherwuise be intercepted after the tunnel has already
* been established (for example, a tunnel "close" message.)
*
* @param {Object} connectionObject The tunneled connection object containing a
* <code>socket</code> reference, <code>tunnel</code> information object,
* and other information for the tunneled connection to send to.
* @param {*} data The data to send to the tunneled connection.
*
* @async
*/
async function sendTunnelMessage(connectionObject, data) {
   if (typeof(connectionObject.socket._send) == "function") {
      //send function intercepted
      connectionObject.socket._send(data);
   } else {
      //send function not intercepted
      connectionObject.socket.send(data);
   }
}

/**
* A proxy or intercept function for a standard WebSocket "message" event when
* the socket is being tunneled that automatically forwards the receiving data to
* the other end of the tunnel.<br/>
* Typically this handler is swapped into the WebSocket instance in
* {@link connectTunnel} where the extra parameter is introduced.
*
* @param {Object} connectionSourceObj The tunneled sending connection object containing a
* <code>socket</code> reference, <code>private_id</code>, <code>tunnel</code> information
* object, <code>tunnelServerMessages</code>, and other information for the tunneled
* connection.
* @param {Object} connectionDestObj The tunneled receiving connection object containing a
* <code>socket</code> reference, <code>private_id</code>, <code>tunnel</code> information
* object, <code>tunnelServerMessages</code>, and other information for the tunneled
* connection.
* @param {*} message The message received from <code>connectionSourceObj</code> to send
* to <code>connectionDestObj</code>.
*
* @async
*/
async function onTunnelMessage(connectionSourceObj, connectionDestObj, message) {
   connectionDestObj.socket._send(message);
}

/**
* A proxy or intercept function for a standard WebSocket "close" event when
* the socket has been closed. The other end of the tunnel is notified and the tunnel
* is either completely removed if closed by the endpoint or re-opened if closed
* by a client. Typically the "close" handler is swapped into the WebSocket instance in
* {@link connectTunnel}.
*
* @param {Object} closedConnObj The tunneled connection that has just closed.
* @param {Object} otherConnObj The connection at the other end of the tunnel.
* @param {Function} nextListener A reference to the next "close" event listener
* registered in the event chain for the closed socket.
* @param {Object} closeEvent The event included in the original "close" dispatch.
*
* @async
*/
async function onTunnelClose(closedConnObj, otherConnObj, nextListener, closeEvent) {
   var closeMessage = buildJSONRPC();
   closeMessage.result.type = "tunnel";
   closeMessage.result.status = "close";
   closeMessage.result.joinedPID = otherConnObj.tunnel.joinedPID;
   closeMessage.result.endpointPID = otherConnObj.tunnel.endpointPID;
   if (closedConnObj.private_id == otherConnObj.tunnel.joinedPID) {
      //tunnel remains available for next connection
      otherConnObj.tunnel.joinedPID = null;
      delete otherConnObj.tunnel.joinedPID;
      this.disconnectTunnel(closedConnObj, false);
      this.disconnectTunnel(otherConnObj, true);
   } else if (closedConnObj.private_id == otherConnObj.tunnel.endpointPID) {
      //tunnel fully closed
      otherConnObj.tunnel.endpointPID = null;
      delete otherConnObj.tunnel.endpointPID;
      otherConnObj.tunnel.joinedPID = null;
      delete otherConnObj.tunnel.joinedPID;
      this.disconnectTunnel(closedConnObj, false);
      this.disconnectTunnel(otherConnObj, false);
   }
   try {
      await sendTunnelMessage(otherConnObj, JSON.stringify(closeMessage));
   } catch (err) {
   }
   nextListener(closeEvent);
}

if (namespace.wss == undefined) {
   namespace.wss = new Object();
}
if (namespace.wss.tunnel == undefined) {
   namespace.wss.tunnel = new Object();
}

namespace.wss.tunnel.getConnectionByAlias = getConnectionByAlias;