UNPKG

thali

Version:
405 lines (376 loc) 20.4 kB
"use strict"; var Promise = require("lie"); var ThaliWifiInfrastructure = require("ThaliWifiInfrastructure"); /** @module WifiBasedNativeMock */ /** * @file * * This is a mock of {@link module:thaliMobileNative}. It is intended to replicate all the capabilities of * {@link module:thaliMobileNative} so that we can build and test code intended to use {@link module:thaliMobileNative} * but on the desktop. * * We are intentionally replicating the lowest layer of the stack in order to to be able to test on the desktop * all the layers on top of it. This includes emulating behaviors unique to iOS and Android. * * For testing purposes if callNative or registerToNative do not get all the parameters they were expecting then * a "Bad Arguments" exception MUST be thrown. */ MobileCallInstance.prototype.wifiBasedNativeMock = null; MobileCallInstance.prototype.mobileMethodName = null; MobileCallInstance.prototype.platform = null; MobileCallInstance.prototype.router = null; /** * In effect this listens for SSDP:alive and SSDP:byebye messages along with the use of SSDP queries to find out who is * around. These will be translated to peer availability callbacks as specified below. This code MUST meet the same * requirements for using a unique SSDP port, syntax for requests, etc. as {@link module:ThaliWifiInfrastructure}. * * Other requirements for this method MUST match those of * {@link external:"Mobile('StartListeningForAdvertisements')".callNative} in terms of idempotency. This also means * we MUST return "Radio Turned Off" if we are emulating Bluetooth as being off. * * @public * @param {module:thaliMobileNative~ThaliMobileCallback} callBack * @returns {null} */ MobileCallInstance.prototype.StartListeningForAdvertisements = function(callBack) { return null; }; /** * This shuts down the SSDP listener/query code. It MUST otherwise behave as given for * {@link external:"Mobile('StopListeningForAdvertisements')".callNative}. * * @public * @param {module:thaliMobileNative~ThaliMobileCallback} callBack * @returns {null} */ MobileCallInstance.prototype.StopListeningForAdvertisements = function(callBack) { return null; }; /** * This method tells the system to both start advertising and to accept incoming connections. In both cases we need * to accept incoming connections. The main challenge is simulating what happens when stop is called. This is supposed * to shut down all incoming connections. So we can't just advertise our 127.0.0.1 port and let the other mocks * running on the same machine connect since stop wouldn't behave properly. To handle the stop behavior, that is to * disconnect all incoming connections, we have to introduce a TCP level proxy. The reason we need a TCP proxy is * that we are using direct SSL connections in a way that may or may not properly work through a HTTPS proxy. So * it's simpler to just introduce the TCP proxy. We will advertise the TCP proxy's listener port in SSDP and when * someone connects we will create a TCP client connection to portNumber and then pipe the two connections together. * * __Open Issue:__ If we directly pipe the TCP listener socket (from Connect) and the TCP client socket (that we * created) then will the system automatically kill the pipe if either socket is killed? We need to test this. If it * doesn't then we just need to hook the close event and close the other side of the pipe. * * __Note:__ For now we are going to not simulate the Bluetooth handshake for Android. This covers the scenario where * device A doesn't discover device B over BLE but device B discovered device A over BLE and then connected over * Bluetooth. The handshake would create a simulated discovery event but we are going to assume that the SSDP * discovery will arrive in a timely manner and so the behavior should be the same. * * For advertising we will use SSDP both to make SSDP:alive as well as to answer queries as given in * {@link module:ThaliWifiInfrastructure}. * * For incoming connections we will, as described above, just rely on everyone running on 127.0.0.1. * * Otherwise the behavior MUST be the same as defined for * (@link external:"Mobile('StartUpdateAdvertisingAndListenForIncomingConnections')".callNative}. That includes * returning the "Call Start!" error as appropriate as well as returning "Radio Turned Off" if we are emulating * Bluetooth as being off. * * @param {number} portNumber * @param {module:thaliMobileNative~ThaliMobileCallback} callBack * @returns {null} */ MobileCallInstance.prototype.StartUpdateAdvertisingAndListenForIncomingConnections = function(portNumber, callBack) { return null; }; /** * This function MUST behave like {@link module:ThaliWifiInfrastructure} and send a proper SSDP:byebye and then * stop responding to queries or sending SSDP:alive messages. Otherwise it MUST act like * (@link external:"Mobile('StopUpdateAdvertisingAndListenForIncomingConnections')".callNative} including terminating * the TCP proxy and all of its connections to simulate killing all incoming connections. * * @param {module:thaliMobileNative~ThaliMobileCallback} callBack * @returns {null} */ MobileCallInstance.prototype.StopAdvertisingAndListeningForIncomingConnections = function(callBack) { return null; }; /** * All the usual restrictions on connect apply including throwing errors if start listening isn't active, handling * consecutive calls, etc. Please see the details in {@link external:"Mobile('Connect')".callNative}. In this case * the mock MUST keep track of the advertised IP and port for each peerIdentifier and then be able to establish * a TCP/IP listener on 127.0.0.1 and use a TCP proxy to relay any connections to the 127.0.0.1 port to the * IP address and port that was advertised over SSDP. The point of all this redirection is to fully simulate the * native layer so we can run tests of the Wrapper and above with full fidelity. This lets us do fun things like * simulate turning off radios as well as properly enforce behaviors such as those below that let our local listener * only accept one connection and simulating time outs on a single peer correctly (e.g. the other side is still * available but we had no activity locally and so need to tear down). If setting up the outgoing TCP proxy is a * big enough pain we could probably figure a way around it but I'm guessing that since we need it anyway for * incoming connections it shouldn't be a big deal. * * In the case of simulating Android we just have to make sure that at any time we have exactly one outgoing * connection to any peerIdentifier. So if we get a second connect for the same peerIdentifier then we have to * return the port for the existing TCP listener we are using, even if it is connected. We also need the right * tear down behavior so that if the local app connection to the local TCP listener (that will then relay to the * remote peer's port) is torn down then we tear down the connection to the remote peer and vice versa. * * On iOS we need the same behavior as Android plus we have to deal with the MCSession * problem. This means we have to look at the peerIdentifier, compare it to the peerIdentifier that we generated * at the SSDP layer and do a lexical comparison. If we are lexically smaller then we have to simulate the trick * that iOS uses where we create a MCSession but don't establish any connections over it. The MCSession is just used * as a signaling mechanism to let the lexically larger peer know that the lexically smaller peer wants to connect. * See the sections below on /ConnectToMeForMock and /IConnectedMock for details. * * ## Making requests to /ConnectToMeForMock * After we receive a connect when we are simulating iOS and the requester is lexically smaller than the target * peerIdentifier then we MUST make a GET request to the target peer's /ConnectToMeForMock endpoint with a query * argument of the form "?port=x&peerIdentifier=y". The port is the port the current peer wishes the target peer * to connect over and the peerIdentifier is the current peer's peerIdentifier. * * If we get a 400 response then we MUST return the "Connection could not be established" error. * * If we get a 200 OK then we just have to wait for a /IConnectedMock request to come in telling us that the remote * peer has established a connection. See the section below on how we handle this. Note that the usual timeout * rules apply so if the /IConnectedMock request does not come within the timeout period the we MUST issue a * "Connection wait time out" error. * * We do not include IP addresses in the request or response because we are only running the mock amongst instances * that are all hosted on the same box and talking over 127.0.0.1. * * ## Sending responses to /ConnectToMeForMock * If we are not currently simulating an iOS device then we MUST return a 500 Server Error because something really * bad has happened. We do not currently support simulating mixed scenarios, everyone in the test run needs to be * either simulating iOS or Android. * * If we are not currently listening for incoming connections then we MUST return a 400 Bad Request. But we MUST * also log the fact that this happened since baring some nasty race conditions we really shouldn't get a call to * this endpoint unless we are listening. * * If we are listening then we MUST issue a PeerAvailabilityChanged callback and set the peerIdentifier to the value in * the query argument, peerAvailable to true and pleaseConnect to true. We MUST also record the port in the query * argument so that if we get a connect request we know what port to submit. * * In theory it's possible for us to get into a situation where we get one port for a peerIdentifier in the * /ConnectToMeForMock request and a different port in a SSDP request. We should just publish the PeerAvailablityChanged * event as they come in and for internal mapping of peerIdentifier to port we should just record whatever came in * last. And yes, this can lead to fun race conditions which is the situation in the real world too. * * ## Making requests to /IConnectedMock * If we are simulating iOS and if we are establishing a TCP connection to a remote peer then by definition we are * the lexically larger peer. However the iOS protocol shares our peerIdentifier with the remote peer, TCP does not. * To work around this anytime we are simulating iOS and have successfully established a TCP connection to a remote peer * we MUST issue a GET request to the /IConnectedMock endpoint of the remote peer with the query string * "?clientPort=x&serverPort=z&peerIdentifier=y". The clientPort and serverPort are the client port and server port * values from the TCP connection that caused us to send this request in the first place. The peerIdentifier is our * peerIdentifier. If we get a 400 response back then we MUST log this event as it really should not have happened. * * ## Sending response to /IConnectedMock * If we are not currently simulating an iOS device then we MUST return a 500 Server Error because something really * bad has happened. We do not currently support simulating mixed scenarios, everyone in the test run needs to be * either simulating iOS or Android. * * If we are not currently listening for incoming connections then we MUST return a 400 Bad Request. But we MUST * also log the fact that this happened since baring some nasty race conditions we really shouldn't have been able * to set up the TCP connection in the first place. * * Otherwise we MUST return a 200 OK. * * When we return a 200 OK we MUST issue a PeerAvailabilityChanged callback with peerIdentifier set to the submitted * peerIdentifier, peerAvailable set to true and pleaseConnect set to false. If we have an outstanding connect request * to the specified peerIdentifier then we MUST look up the specified clientPort/serverPort and see if we can match * it to any of the incoming connections to the TCP proxy. If we can then we MUST return the clientPort/serverPort * being used by the TCP proxy as the connect response with listeningPort set to null and clientPort/serverPort * set to the values the TCP proxy is using. If we cannot match the connection via the TCP proxy then this means * that the connection might have died or been killed while this request to /IConnectedMock was being sent. In that * case we should send bogus values in the connect response to simulate a situation where a peer connects but then * the connection dies before the connect callback is returned. * * @param {string} peerIdentifier * @param {module:thaliMobileNative~ConnectCallback} callback * @returns {null} */ MobileCallInstance.prototype.Connect = function(peerIdentifier, callback) { return null; }; /** * If we aren't emulating iOS then this method has to return the "Not Supported" error. If we are emulating iOS * then we have to kill all the TCP listeners we are using to handling outgoing connections and the TCP proxy * we are using to handle incoming connections. * * @public * @param {module:thaliMobileNative~ThaliMobileCallback} callback * @returns {null} */ MobileCallInstance.prototype.KillConnections = function(callback) { return null; }; /** * Handles processing callNative requests. The actual params differ based on the particular Mobile method * that is being called. * * @returns {null} */ MobileCallInstance.prototype.callNative = function() { switch (this.mobileMethodName) { case "StartListeningForAdvertisements": return this.StartListeningForAdvertisements(arguments[0]); case "StopListeningForAdvertisements": return this.StopListeningForAdvertisements(arguments[0]); case "StartUpdateAdvertisingAndListenForIncomingConnections": return this.StartUpdateAdvertisingAndListenForIncomingConnections(arguments[0], arguments[1]); case "StopAdvertisingAndListeningForIncomingConnections": return this.StopAdvertisingAndListeningForIncomingConnections(arguments[0]); case "Connect": return this.Connect(arguments[0], arguments[1]); case "KillConnections": return this.KillConnections(arguments[0]); default: throw new Error("The supplied mobileName does not have a matching callNative method: " + this.mobileMethodName); } }; /** * Anytime we are looking for advertising and we receive a SSDP:alive, SSDP:byebye or a response to one of our * periodic queries we should use it to create a PeerAvailabilityChanged call back. In practice we don't really * need to batch these messages so we can just fire them as we get them. The peerIdentifier is the USN from the * SSDP message, peerAvailable is true or false based on the SSDP response and pleaseConnect is false except for * the situation described above for /ConnectToMeforMock. * * @param {module:thaliMobileNative~peerAvailabilityChangedCallback} callback * @returns {null} */ MobileCallInstance.prototype.PeerAvailabilityChanged = function(callback) { return null; }; /** * Any time there is a call to start and stop or if Bluetooth is turned off on Android (which also MUST mean * that we have disabled both advertising and discovery) then we MUST fire this event. * * @public * @param {module:thaliMobileNative~discoveryAdvertisingStateUpdateNonTCPCallback} callback * @returns {null} */ MobileCallInstance.prototype.DiscoveryAdvertisingStateUpdateNonTCP = function(callback) { return null; }; /** * At this point this event would only fire because we called toggleBluetooth or toggleWifi. For the moment * we will treat toggleBluetooth and turning on/off both blueToothLowEnergy and blueTooth. * * __Open Issue:__ Near as I can tell both Android and iOS have a single Bluetooth switch that activates and * de-activates Bluetooth and BLE. Note however that in theory it's possible to still have one available and not * the other to a particular application because of app level permissions but that isn't an issue for the mock. * * @public * @param {module:thaliMobileNative~networkChangedCallback} callback * @returns {null} */ MobileCallInstance.prototype.NetworkChanged = function(callback) { return null; }; /** * This is used anytime the TCP proxy for incoming connections cannot connect to the portNumber set in * {@link module:WifiBasedNativeMock~MobileCallInstance.StartUpdateAdvertisingAndListenForIncomingConnections}. * * @public * @param {module:thaliMobileNative~incomingConnectionToPortNumberFailedCallback} callback * @returns {null} */ MobileCallInstance.prototype.IncomingConnectionToPortNumberFailed = function(callback) { return null; }; MobileCallInstance.prototype.registerToNative = function() { switch (this.mobileMethodName) { case "PeerAvailabilityChanged": return this.PeerAvailabilityChanged(arguments[0]); case "DiscoveryAdvertisingStateUpdateNonTCP": return this.DiscoveryAdvertisingStateUpdateNonTCP(arguments[0]); case "NetworkChanged": return this.NetworkChanged(arguments[0]); case "IncomingConnectionToPortNumberFailed": return this.IncomingConnectionToPortNumberFailed(arguments[0]); default: throw new Error("The supplied mobileName does not have a matching registerToNative method: " + this.mobileMethodName); } }; /** * This is the method that actually handles processing the native requests. In general this method just * records the arguments for later use. * * @param {string} mobileMethodName This is the name of the method that was passed in on the mobile object * @param {platformChoice} platform * @param wifiBasedNativeMock * @constructor */ function MobileCallInstance(mobileMethodName, platform, wifiBasedNativeMock) { this.mobileMethodName = mobileMethodName; this.platform = platform; this.router = router; this.wifiBasedNativeMock = wifiBasedNativeMock; } /** * Enum to describe the platforms we can simulate, this mostly controls how we handle connect * * @public * @readonly * @type {{android: string, iOS: string}} */ var platformChoice = { android: "Android", iOS: "iOS" }; /** * This simulates turning Bluetooth on and off. * * If we are emulating Android then we MUST start with Bluetooth and WiFi turned off. * * __Open Issue:__ I believe that JXCore will treat this as a NOOP if called on iOS. We need to check and emulate * their behavior. * * @param {platformChoice} platform * @param {ThaliWifiInfrastructure} wifiBasedNativeMock * @returns {Function} */ function toggleBluetooth(platform, wifiBasedNativeMock) { return function(setting, callback) { return null; } } /** * If we are on Android then then is a NOOP since we don't care (although to be good little programmers we should still * fire a network changed event). We won't be using Wifi for discovery or connectivity in the near future. * * __Open Issue:__ I believe that JXCore will treat this as a NOOP if called on iOS. We need to check and emulate * their behavior. * * @param {platformChoice} platform * @param {ThaliWifiInfrastructure} wifiBasedNativeMock * @returns {Function} */ function toggleWiFi(platform, wifiBasedNativeMock) { return function(setting, callback) { return null; } } /** * To use this mock save the current global object Mobile (if it exists) and replace it with this object. In general * this object won't exist on the desktop. * * If we are simulating iOS then we MUST add the /ConnectToMeForMock and /IConnectedMock endpoints as described above * to the router object. * * @public * @constructor * @param {platformChoice} platform * @param {Object} router This is the express router being used up in the stack. We need it here so we can add * a router to simulate the iOS case where we need to let the other peer know we want a connection. */ function WifiBasedNativeMock(platform, router) { var thaliWifiInfrastructure = new ThaliWifiInfrastructure(); var mobileHandler = function(mobileMethodName) { return new MobileCallInstance(mobileMethodName, platform, router, thaliWifiInfrastructure); }; mobileHandler.toggleBluetooth = toggleBluetooth(thaliWifiInfrastructure); mobileHandler.toggleWiFi = toggleWiFi(thaliWifiInfrastructure); return mobileHandler; } module.exports = WifiBasedNativeMock;