UNPKG

thali

Version:
392 lines (378 loc) 19.8 kB
"use strict"; var Promise = require("lie"); /** @module TCPServersManager */ /** * @file * * This is where we manage creating multiplex objects. For all intents and purposes this file should be treated as part * of {@link module:thaliMobileNative}. We have broken this functionality out here in order to make the code more * maintainable and easier to follow. * * When dealing with incoming connections this code creates a multiplex object to handle de-multiplexing the incoming * connections and in the iOS case to also send TCP/IP connections down the incoming connection (reverse the polarity * as it were). * * When dealing with discovered peers we like to advertise a port that the Thali Application can connect to in order to * talk to that peer. But for perf reasons that port is typically not connected to anything at the native layer (with * the exception of a lexically smaller peer) until someone connects to the port. The reason for this design (thanks * Ville!) is to make non-TCP and TCP peers look the same. There is an address (in this case 127.0.0.1) and a port and * you connect and there you go. This file defines all the magic needed to create the illusion that a non-TCP peer is * actually available over TCP. * * There are three different scenarios where multiplex objects can get created: * * Android * - We get an incoming connection from the native layer to the portNumber we submitted to * StartUpdateAdvertisingAndListenForIncomingConnections * - We create a mux that pipes to the incoming TCP/IP connection. * - We get a peerAvailabilityChanged Event * - We create a local listener and advertise nonTCPPeerAvailabilityChanged. When we get a connection to that listener * then we call native connect, create a connection to the native connect port, hook the mux to that connection on * one end and the incoming listener to the mux on the other end. * * iOS - Lexically Smaller Peer * - We get an incoming connection from the native layer to the portNumber we submitted to * StartUpdateAdvertisingAndListenForIncomingConnections * - We create a mux that pipes to the incoming TCP/IP connection. We keep track of this mux because we might need it * in the next entry. Remember, we don't know which peer made the incoming connection. * - We get a peerAvailabilityChanged Event * - Because we are lexically smaller this event will have pleaseConnect set to false. So we create a port and * advertise it on nonTCPPeerAvailabilityChanged. When we get a connection we call connect. If there is already an * incoming connection then the connect will return with the clientPort/serverPort and we will re-use the existing mux * If there is no existing incoming connection then the system will wait to trigger the lexically larger peer to create * it and once it is created and properly terminated (per the previous section) then we will find the mux via * clientPort/ServerPort. * * iOS - Lexically Larger Peer * - We get an incoming connection from the native layer to the portNumber we submitted to * StartUpdateAdvertisingAndListenForIncomingConnections * - It isn't possible. * - We get a peerAvailabilityChanged Event * - If the peerAvailabilityChanged Event has pleaseConnect set to true then baring any limitation on available * resources we should immediately issue a connect and hook in the mux to it configured to handling incoming * connections and then create a TCP listener and have it use createStream with the mux for any incoming connections. * Obviously if we already have a connection to the identified peer then we can ignore the pleaseConnect value. * - If the peerAvailabilityChanged Event has pleaseConnect set to false then we will set up a TCP listener and * advertise the port but we won't create the mux or call connect until the first connection to the TCP listener * comes in. * * We have two basic kinds of listeners. One type is for incoming connections from remote peers. In that case we * will have a TCP connection from the native layer connecting to us which we will then connect to a multiplex object. * The other listener is for connections from a Thali App to a remote peer. In that case we will create a TCP * connection to a native listener and hook our TCP connection into a multiplex object. And of course with the iOS * situation sometimes it all gets mixed up. * * But the point is that each listener has at its root a TCP connection either going out to or coming in from the * native layer. Because keeping native connections open eats battery (although this is probably a much less * significant issue with iOS due to its UDP based design) we don't want to let connections hang open unused. This is * why we put a timeout on the TCP connection under the multiplex. That connection sees all traffic in both directions * (e.g. even in the iOS case where we mux connections both ways) and so it knows if anything is happening. If all is * quiet then it knows it can kill the connection. * * We also need to deal with cleaning things up when they go wrong. Typically we will focus the cleanup code on the * multiplex object. It will first close the TCP connections with the Thali app then the multiplex streams connected * to those TCP connections then it will close the listener and any native connections before closing itself. * * Separately it is possible for individual multiplexed TCP connections to die or the individual streams they are * connected to can die. This only requires local clean up. We just have to be smart so we don't try to close things * that are already closed. So when a TCP connection gets a closed event it has to detect if it was closed by the * underlying multiplex stream or by a TCP level error. If it was closed by the multiplex stream then it shouldn't * call close on the multiplex stream it is paired with otherwise it should. The same logic applies when an individual * stream belonging to multiplex object gets closed. Was it closed by its paired TCP connecion? If so, then it's done. * Otherwise it needs to close that connection. */ /** * Maximum number of peers we support simultaneously advertising * @type {number} */ var maxPeersToAdvertise = 1000; /** * This method will call {@link module:TCPServersManager.createNativeListener} using the routerPort from the * constructor and record the returned port. * * This method is idempotent and so MUST be able to be called multiple times in a row without changing state. * * If called successfully then the object is in the start state. * * If this method is called after a call to {@link module:TCPServersManager.stop} then a "We are stopped!" Error * MUST be thrown. * * @public * @returns {Promise<Number|Error>} Returns the port to be passed to * {@link external:"Mobile('StartUpdateAdvertisingAndListenForIncomingConnections')".callNative} when the system * is ready to receive external incoming connections. */ TCPServersManager.prototype.start = function() { return Promise.resolve(); }; /** * This will cause destroy to be called on the TCP server created by * {@link module:TCPServersManager.createNativeListener} and then on all the TCP servers created by * {@link module:TCPServersManager.connectToPeerViaNativeLayer}. * * This method is idempotent and so MUST be able to be called multiple times in a row without changing state. * * If this method is called before calling start then a "Call Start!" Error MUST be thrown. * * Once called the object is in the stop state and cannot leave it. To start again this object must be disposed and * a new one created. * * @public * @returns {Null|Error} */ TCPServersManager.prototype.stop = function() { return null; }; /** * This method creates a TCP listener to handle requests from the native layer and to then pass them through a * multiplex object who will route all the multiplexed connections to routerPort, the port the system has hosted * the submitted router object on. The TCP listener will be started * on port 0 and the port it is hosted on will be returned in the promise. This is the port that MUST be submitted to * the native layer's * {@link external:"Mobile('StartUpdateAdvertisingAndListenForIncomingConnections')".callNative} command. * * If this method is called when we are not in the start state then an exception MUST be thrown because this is a * private method and something very bad just happened. * * If this method is called twice an exception MUST be thrown because this should only be called once from the * constructor. * * ## TCP Listener * * ### Connect Event * A multiplex object MUST be created and MUST be directly piped in both directions with the * TCP socket returned by the listener. We MUST set a timeout on the incoming TCP socket to a reasonable value for the * platform. The created multiplex object MUST be recorded with an index of the client port used by the incoming TCP * socket. * * ### Error Event * The error MUST be logged. * * ### Close Event * We MUST call destroy on all multiplex objects spawned by this TCP listener. * * ## Incoming TCP socket returned by the server's connect event * ### Error Event * The error MUST be logged. * * ### Timeout Event * Destroy MUST be called on the piped multiplex object. This will trigger a total cleanup. * * ### Close Event * If this close is not the result of a destroy on the multiplex object then destroy MUST be called on the multiplex * object. * * ## Multiplex Object * ## onStream Callback * The incoming stream MUST cause us to create a net.createConnection to routerPort and to then take the new TCP socket * and pipe it in both directions with the newly created stream. We MUST track the TCP socket so we can clean it up * later. Note that the TCP socket will track its associated stream and handle cleaning it up. If the TCP socket * cannot be connected to routerPort then a routerPortConnectionFailed event MUST be fired and destroy MUST * be called on the stream provided in the callback. * * ### Error Event * The error MUST be logged. * * ### Close Event * Destroy MUST first be called on all the TCP sockets we created to routerPort (the TCP sockets will then close * their associated multiplex streams). Then we MUST call Destroy on the incoming TCP socket from the native * layer. Note that in some cases one or more of these objects could already be closed before we call destroy so we * MUST be prepared to catch any exceptions. Finally we MUST remove the multiplex object from the list of multiplex * objects we are maintaining. * * ## TCP client socket created by net.createConnection call from multiplex object * ### Error Event * The error MUST be logged. * * ### Close Event * Destroy MUST be called on the stream this TCP socket is piped to assuming that it wasn't that stream that called * destroy on the TCP client socket. * * ## multiplex onStream stream * ### Error Event * The error MUST be logged. * * ### Close Event * If the close did not come from the TCP socket this stream is piped to then close MUST be called on the associated * TCP socket. * * @private * @param {Number} routerPort Port that the router object submitted to * {@link module:ThaliMobileNativeWrapper.startUpdateAdvertisingAndListenForIncomingConnections} is hosted on. This * value was passed into this object's constructor. * @returns {Promise<Number|Error>} The port that the mux is listening on for connections from the native layer or an * Error object. */ TCPServersManager.prototype.createNativeListener = function(routerPort) { return Promise.resolve(); }; /** * This creates a local TCP server to accept incoming connections from the Thali app that will be sent to the * identified peer. * * If this method is called before start is called then a "Start First!" error MUST be thrown. If this method is * called after stop is called then a "We are stopped!" error MUST be thrown. * * If there is already a TCP server listening for connections to the submitted peerIdentifier then the port for the * TCP server MUST be returned. * * If there is no existing TCP server for the specified peer then we MUST examine how many peers we are advertising * 127.0.0.1 ports for. If that number is equal to maxPeersToAdvertise then we MUST call destroy on one of those TCP * listeners before continuing with this method. That way we will never offer connections to more than * maxPeersToAdvertise peers at a time. We should exclude all TCP servers that have active multiplex objects and pick a * TCP server to close based on FIFO. Once we have closed the TCP server, if necessary, then a new TCP server MUST be * created on port 0 (e.g. any available port) and configured as follows: * * ## TCP server * If pleaseConnect is true then an immediate call MUST be made to {@link external:"Mobile('Connect')".callNative} to * connect to the specified peer. If that call fails then the error MUST be returned. Otherwise a new multiplex * object MUST be created and a new TCP connection via net.createConnection pointed at the port returned by the * connect call. The multiplex object MUST be piped in both directions with the new TCP connection. The TCP connection * MUST have setTimeout called on it and set to a reasonable value for the platform. * * ### Connection Event * #### First call to connection event when pleaseConnect is false * If pleaseConnect is false then when the first connection event occurs we MUST issue a * {@link external:"Mobile('Connect')".callNative} for the requested peer and handle the response as given in the * following sections. * * ##### Error * If we get an error then we MUST close the TCP connection and fire a {@link event:failedConnection} event with the * returned error. * * ##### listenerPort * If the response is listenerPort then we MUST perform the actions specified above for pleaseConnect is true with the * exception that if the Connect fails then we MUST call close on the TCP server since the peer is not available and * fire a {@link event:failedConnection} event with the error set to "Cannot Connect To Peer". * * ##### clientPort/serverPort * If clientPort/serverPort are not null then we MUST confirm that the * serverPort matches the port that the server created in * {@link module:TCPServersManager.createNativeListener} is listening on and if not then we MUST call destroy on * the incoming TCP connection, fire a {@link event:failedConnection} event with the error set to * "Mismatched serverPort", and act as if connection had not been called (e.g. the next connection will be treated * as the first). * * Otherwise we must then lookup the multiplex object via the clientPort. If there is no multiplex object associated * with that clientPort then we have a race condition where the incoming connection died between when the connect * response was sent and now. In that case we MUST call destroy on the incoming TCP connection, first a * {@link event:failedConnection} event with the error set to "Incoming connection died" and as previously * described treat the next connection as if it were the first. * * Otherwise we MUST configure the multiplex object with the behavior specified below. * * #### Standard connection event behavior * Each socket returned by the connection event MUST cause a call to createStream on the multiplex object and the * returned stream MUST be piped in both directions with the connection TCP socket. * * ### Error Event * The error MUST be logged. * * ### Close Event * All the TCP sockets to routerPort MUST first be destroyed. Then all the TCP sockets from the Thali * application MUST be destroyed. * * Unless destroy was called on the TCP server by the multiplex object then destroy MUST be called on the multiplex * object. * * ## Multiplex object * ### onStream callback * If a stream is received a call to net.createConnection MUST be made pointed at routerPort. If the TCP connection * cannot be successfully connected then a {@link event:routerPortConnectionFailed} MUST be fired and destroy MUST be * called on the stream. Otherwise the TCP connection and the stream MUST be piped to each other in both directions. * * Note that we will support the ability to accept incoming connections over the multiplex object even for platforms * like Android that do not need it. This is just to keep the code and testing simple and consistent. * * ### Error Event * The error MUST be logged. * * ### Close Event * If the destroy didn't come the TCP server then destroy MUST be called on the TCP server. * If the destroy didn't come from the TCP native socket then destroy MUST be called on the TCP native socket. * * ## TCP socket to native layer * ### Timeout Event * Destroy MUST be called on itself. * * ### Error Event * The error MUST be logged. * * ### Close Event * Destroy MUST be called on the multiplex object the stream is piped to. * * ## TCP socket from Thali Application * ### Error Event * The error MUST be logged. * * ### Close Event * Destroy MUST be called on the stream object the socket is piped to if that isn't the object that called destroy * on the socket. * * ## createStream Socket * ### Error Event * The error MUST be logged. * * ### Close Event * If destroy wasn't called by the TCP socket from Thali Application the stream is piped to then destroy MUST be called * on that TCP socket. * * ## TCP socket to routerPort * ### Error Event * The error MUST be logged. * * ### Close Event * Destroy MUST be called on the stream object the socket is piped to if that isn't the object that called destroy * on the socket. * * ## onStream callback stream * ### Error Event * The error MUST be logged. * * ### Close Event * If destroy wasn't called by the TCP socket to routerPort the stream is piped to then destroy MUST be called on that * TCP socket. * * @public * @param {String} peerIdentifier * @param {Boolean} [pleaseConnect] If set to true this indicates that a lexically smaller peer asked for a connection * so the lexically larger peer (the local device) will immediately call * {@link external:"Mobile('Connect')".callNative} to create a connection. If false (the default value) then the call * to {@link external:"Mobile('Connect')".callNative} will only happen on the first incoming connection to the * TCP server. * @returns {Promise<Number|Error>} */ TCPServersManager.prototype.createPeerListener = function (peerIdentifier, pleaseConnect) { return Promise.resolve(); }; /** * Notifies the listener of a failed connection attempt. This is mostly used to determine when we have hit the * local maximum connection limit but it's used any time there is a connection error since the only other hint * that a connection is failed is that the TCP/IP connection to the 127.0.0.1 port will fail. * * @public * @event failedConnection * @property {Error} error * @property {string} peerIdentifier */ /** * Notifies the listener that an attempt to connect to routerPort failed. * * @public * @event routerPortConnectionFailed * @property {Error} error * @property {number} routerPort */ /** * An instance of this class is used by {@link module:thaliMobileNativeWrapper} to create the TCP servers needed * to handle non-TCP incoming and outgoing connections. * * @public * @constructor * @param {number} routerPort The port that the system is hosting the local router instance for the Thali Application. * @fires event:routerPortConnectionFailed * @fires event:failedConnection */ function TCPServersManager(routerPort) { } module.exports = TCPServersManager;