1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/modules/SimpleServiceDiscovery.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,286 @@ 1.4 +// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- 1.5 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.7 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +"use strict"; 1.10 + 1.11 +this.EXPORTED_SYMBOLS = ["SimpleServiceDiscovery"]; 1.12 + 1.13 +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; 1.14 + 1.15 +Cu.import("resource://gre/modules/Services.jsm"); 1.16 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.17 + 1.18 +function log(msg) { 1.19 + Services.console.logStringMessage("[SSDP] " + msg); 1.20 +} 1.21 + 1.22 +XPCOMUtils.defineLazyGetter(this, "converter", function () { 1.23 + let conv = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter); 1.24 + conv.charset = "utf8"; 1.25 + return conv; 1.26 +}); 1.27 + 1.28 +// Spec information: 1.29 +// https://tools.ietf.org/html/draft-cai-ssdp-v1-03 1.30 +// http://www.dial-multiscreen.org/dial-protocol-specification 1.31 +const SSDP_PORT = 1900; 1.32 +const SSDP_ADDRESS = "239.255.255.250"; 1.33 + 1.34 +const SSDP_DISCOVER_PACKET = 1.35 + "M-SEARCH * HTTP/1.1\r\n" + 1.36 + "HOST: " + SSDP_ADDRESS + ":" + SSDP_PORT + "\r\n" + 1.37 + "MAN: \"ssdp:discover\"\r\n" + 1.38 + "MX: 2\r\n" + 1.39 + "ST: %SEARCH_TARGET%\r\n\r\n"; 1.40 + 1.41 +const SSDP_DISCOVER_TIMEOUT = 10000; 1.42 + 1.43 +/* 1.44 + * SimpleServiceDiscovery manages any discovered SSDP services. It uses a UDP 1.45 + * broadcast to locate available services on the local network. 1.46 + */ 1.47 +var SimpleServiceDiscovery = { 1.48 + _targets: new Map(), 1.49 + _services: new Map(), 1.50 + _searchSocket: null, 1.51 + _searchInterval: 0, 1.52 + _searchTimestamp: 0, 1.53 + _searchTimeout: Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer), 1.54 + _searchRepeat: Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer), 1.55 + 1.56 + _forceTrailingSlash: function(aURL) { 1.57 + // Some devices add the trailing '/' and some don't. Let's make sure 1.58 + // it's there for consistency. 1.59 + if (!aURL.endsWith("/")) { 1.60 + aURL += "/"; 1.61 + } 1.62 + return aURL; 1.63 + }, 1.64 + 1.65 + // nsIUDPSocketListener implementation 1.66 + onPacketReceived: function(aSocket, aMessage) { 1.67 + // Listen for responses from specific targets. There could be more than one 1.68 + // available. 1.69 + let response = aMessage.data.split("\n"); 1.70 + let location; 1.71 + let target; 1.72 + let valid = false; 1.73 + response.some(function(row) { 1.74 + let header = row.toUpperCase(); 1.75 + if (header.startsWith("LOCATION")) { 1.76 + location = row.substr(10).trim(); 1.77 + } else if (header.startsWith("ST")) { 1.78 + target = row.substr(4).trim(); 1.79 + if (this._targets.has(target)) { 1.80 + valid = true; 1.81 + } 1.82 + } 1.83 + 1.84 + if (location && valid) { 1.85 + location = this._forceTrailingSlash(location); 1.86 + 1.87 + // When we find a valid response, package up the service information 1.88 + // and pass it on. 1.89 + let service = { 1.90 + location: location, 1.91 + target: target 1.92 + }; 1.93 + 1.94 + try { 1.95 + this._processService(service); 1.96 + } catch (e) {} 1.97 + 1.98 + return true; 1.99 + } 1.100 + return false; 1.101 + }.bind(this)); 1.102 + }, 1.103 + 1.104 + onStopListening: function(aSocket, aStatus) { 1.105 + // This is fired when the socket is closed expectedly or unexpectedly. 1.106 + // nsITimer.cancel() is a no-op if the timer is not active. 1.107 + this._searchTimeout.cancel(); 1.108 + this._searchSocket = null; 1.109 + }, 1.110 + 1.111 + // Start a search. Make it continuous by passing an interval (in milliseconds). 1.112 + // This will stop a current search loop because the timer resets itself. 1.113 + search: function search(aInterval) { 1.114 + if (aInterval > 0) { 1.115 + this._searchInterval = aInterval || 0; 1.116 + this._searchRepeat.initWithCallback(this._search.bind(this), this._searchInterval, Ci.nsITimer.TYPE_REPEATING_SLACK); 1.117 + } 1.118 + this._search(); 1.119 + }, 1.120 + 1.121 + // Stop the current continuous search 1.122 + stopSearch: function stopSearch() { 1.123 + this._searchRepeat.cancel(); 1.124 + }, 1.125 + 1.126 + _usingLAN: function() { 1.127 + let network = Cc["@mozilla.org/network/network-link-service;1"].getService(Ci.nsINetworkLinkService); 1.128 + return (network.linkType == Ci.nsINetworkLinkService.LINK_TYPE_WIFI || network.linkType == Ci.nsINetworkLinkService.LINK_TYPE_ETHERNET); 1.129 + }, 1.130 + 1.131 + _search: function _search() { 1.132 + // If a search is already active, shut it down. 1.133 + this._searchShutdown(); 1.134 + 1.135 + // We only search if on local network 1.136 + if (!this._usingLAN()) { 1.137 + return; 1.138 + } 1.139 + 1.140 + // Update the timestamp so we can use it to clean out stale services the 1.141 + // next time we search. 1.142 + this._searchTimestamp = Date.now(); 1.143 + 1.144 + // Look for any fixed IP targets. Some routers might be configured to block 1.145 + // UDP broadcasts, so this is a way to skip discovery. 1.146 + this._searchFixedTargets(); 1.147 + 1.148 + // Perform a UDP broadcast to search for SSDP devices 1.149 + let socket = Cc["@mozilla.org/network/udp-socket;1"].createInstance(Ci.nsIUDPSocket); 1.150 + try { 1.151 + socket.init(SSDP_PORT, false); 1.152 + socket.asyncListen(this); 1.153 + } catch (e) { 1.154 + // We were unable to create the broadcast socket. Just return, but don't 1.155 + // kill the interval timer. This might work next time. 1.156 + log("failed to start socket: " + e); 1.157 + return; 1.158 + } 1.159 + 1.160 + this._searchSocket = socket; 1.161 + this._searchTimeout.initWithCallback(this._searchShutdown.bind(this), SSDP_DISCOVER_TIMEOUT, Ci.nsITimer.TYPE_ONE_SHOT); 1.162 + 1.163 + let data = SSDP_DISCOVER_PACKET; 1.164 + for (let [key, target] of this._targets) { 1.165 + let msgData = data.replace("%SEARCH_TARGET%", target.target); 1.166 + try { 1.167 + let msgRaw = converter.convertToByteArray(msgData); 1.168 + socket.send(SSDP_ADDRESS, SSDP_PORT, msgRaw, msgRaw.length); 1.169 + } catch (e) { 1.170 + log("failed to convert to byte array: " + e); 1.171 + } 1.172 + } 1.173 + }, 1.174 + 1.175 + _searchFixedTargets: function _searchFixedTargets() { 1.176 + let fixedTargets = null; 1.177 + try { 1.178 + fixedTargets = Services.prefs.getCharPref("browser.casting.fixedTargets"); 1.179 + } catch (e) {} 1.180 + 1.181 + if (!fixedTargets) { 1.182 + return; 1.183 + } 1.184 + 1.185 + fixedTargets = JSON.parse(fixedTargets); 1.186 + for (let fixedTarget of fixedTargets) { 1.187 + // Verify we have the right data 1.188 + if (!"location" in fixedTarget || !"target" in fixedTarget) { 1.189 + continue; 1.190 + } 1.191 + 1.192 + fixedTarget.location = this._forceTrailingSlash(fixedTarget.location); 1.193 + 1.194 + let service = { 1.195 + location: fixedTarget.location, 1.196 + target: fixedTarget.target 1.197 + }; 1.198 + 1.199 + // We don't assume the fixed target is ready. We still need to ping it. 1.200 + try { 1.201 + this._processService(service); 1.202 + } catch (e) {} 1.203 + } 1.204 + }, 1.205 + 1.206 + // Called when the search timeout is hit. We use it to cleanup the socket and 1.207 + // perform some post-processing on the services list. 1.208 + _searchShutdown: function _searchShutdown() { 1.209 + if (this._searchSocket) { 1.210 + // This will call onStopListening. 1.211 + this._searchSocket.close(); 1.212 + 1.213 + // Clean out any stale services 1.214 + for (let [key, service] of this._services) { 1.215 + if (service.lastPing != this._searchTimestamp) { 1.216 + Services.obs.notifyObservers(null, "ssdp-service-lost", service.location); 1.217 + this._services.delete(service.location); 1.218 + } 1.219 + } 1.220 + } 1.221 + }, 1.222 + 1.223 + registerTarget: function registerTarget(aTarget, aAppFactory) { 1.224 + // Only add if we don't already know about this target 1.225 + if (!this._targets.has(aTarget)) { 1.226 + this._targets.set(aTarget, { target: aTarget, factory: aAppFactory }); 1.227 + } 1.228 + }, 1.229 + 1.230 + findAppForService: function findAppForService(aService, aApp) { 1.231 + if (!aService || !aService.target) { 1.232 + return null; 1.233 + } 1.234 + 1.235 + // Find the registration for the target 1.236 + if (this._targets.has(aService.target)) { 1.237 + return this._targets.get(aService.target).factory(aService, aApp); 1.238 + } 1.239 + return null; 1.240 + }, 1.241 + 1.242 + findServiceForLocation: function findServiceForLocation(aLocation) { 1.243 + if (this._services.has(aLocation)) { 1.244 + return this._services.get(aLocation); 1.245 + } 1.246 + return null; 1.247 + }, 1.248 + 1.249 + // Returns an array copy of the active services 1.250 + get services() { 1.251 + let array = []; 1.252 + for (let [key, service] of this._services) { 1.253 + array.push(service); 1.254 + } 1.255 + return array; 1.256 + }, 1.257 + 1.258 + _processService: function _processService(aService) { 1.259 + // Use the REST api to request more information about this service 1.260 + let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest); 1.261 + xhr.open("GET", aService.location, true); 1.262 + xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; 1.263 + xhr.overrideMimeType("text/xml"); 1.264 + 1.265 + xhr.addEventListener("load", (function() { 1.266 + if (xhr.status == 200) { 1.267 + let doc = xhr.responseXML; 1.268 + aService.appsURL = xhr.getResponseHeader("Application-URL"); 1.269 + if (aService.appsURL && !aService.appsURL.endsWith("/")) 1.270 + aService.appsURL += "/"; 1.271 + aService.friendlyName = doc.querySelector("friendlyName").textContent; 1.272 + aService.uuid = doc.querySelector("UDN").textContent; 1.273 + aService.manufacturer = doc.querySelector("manufacturer").textContent; 1.274 + aService.modelName = doc.querySelector("modelName").textContent; 1.275 + 1.276 + // Only add and notify if we don't already know about this service 1.277 + if (!this._services.has(aService.location)) { 1.278 + this._services.set(aService.location, aService); 1.279 + Services.obs.notifyObservers(null, "ssdp-service-found", aService.location); 1.280 + } 1.281 + 1.282 + // Make sure we remember this service is not stale 1.283 + this._services.get(aService.location).lastPing = this._searchTimestamp; 1.284 + } 1.285 + }).bind(this), false); 1.286 + 1.287 + xhr.send(null); 1.288 + } 1.289 +}