mobile/android/modules/SimpleServiceDiscovery.jsm

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

michael@0 1 // -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
michael@0 2 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 3 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 4 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 5
michael@0 6 "use strict";
michael@0 7
michael@0 8 this.EXPORTED_SYMBOLS = ["SimpleServiceDiscovery"];
michael@0 9
michael@0 10 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
michael@0 11
michael@0 12 Cu.import("resource://gre/modules/Services.jsm");
michael@0 13 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 14
michael@0 15 function log(msg) {
michael@0 16 Services.console.logStringMessage("[SSDP] " + msg);
michael@0 17 }
michael@0 18
michael@0 19 XPCOMUtils.defineLazyGetter(this, "converter", function () {
michael@0 20 let conv = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter);
michael@0 21 conv.charset = "utf8";
michael@0 22 return conv;
michael@0 23 });
michael@0 24
michael@0 25 // Spec information:
michael@0 26 // https://tools.ietf.org/html/draft-cai-ssdp-v1-03
michael@0 27 // http://www.dial-multiscreen.org/dial-protocol-specification
michael@0 28 const SSDP_PORT = 1900;
michael@0 29 const SSDP_ADDRESS = "239.255.255.250";
michael@0 30
michael@0 31 const SSDP_DISCOVER_PACKET =
michael@0 32 "M-SEARCH * HTTP/1.1\r\n" +
michael@0 33 "HOST: " + SSDP_ADDRESS + ":" + SSDP_PORT + "\r\n" +
michael@0 34 "MAN: \"ssdp:discover\"\r\n" +
michael@0 35 "MX: 2\r\n" +
michael@0 36 "ST: %SEARCH_TARGET%\r\n\r\n";
michael@0 37
michael@0 38 const SSDP_DISCOVER_TIMEOUT = 10000;
michael@0 39
michael@0 40 /*
michael@0 41 * SimpleServiceDiscovery manages any discovered SSDP services. It uses a UDP
michael@0 42 * broadcast to locate available services on the local network.
michael@0 43 */
michael@0 44 var SimpleServiceDiscovery = {
michael@0 45 _targets: new Map(),
michael@0 46 _services: new Map(),
michael@0 47 _searchSocket: null,
michael@0 48 _searchInterval: 0,
michael@0 49 _searchTimestamp: 0,
michael@0 50 _searchTimeout: Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer),
michael@0 51 _searchRepeat: Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer),
michael@0 52
michael@0 53 _forceTrailingSlash: function(aURL) {
michael@0 54 // Some devices add the trailing '/' and some don't. Let's make sure
michael@0 55 // it's there for consistency.
michael@0 56 if (!aURL.endsWith("/")) {
michael@0 57 aURL += "/";
michael@0 58 }
michael@0 59 return aURL;
michael@0 60 },
michael@0 61
michael@0 62 // nsIUDPSocketListener implementation
michael@0 63 onPacketReceived: function(aSocket, aMessage) {
michael@0 64 // Listen for responses from specific targets. There could be more than one
michael@0 65 // available.
michael@0 66 let response = aMessage.data.split("\n");
michael@0 67 let location;
michael@0 68 let target;
michael@0 69 let valid = false;
michael@0 70 response.some(function(row) {
michael@0 71 let header = row.toUpperCase();
michael@0 72 if (header.startsWith("LOCATION")) {
michael@0 73 location = row.substr(10).trim();
michael@0 74 } else if (header.startsWith("ST")) {
michael@0 75 target = row.substr(4).trim();
michael@0 76 if (this._targets.has(target)) {
michael@0 77 valid = true;
michael@0 78 }
michael@0 79 }
michael@0 80
michael@0 81 if (location && valid) {
michael@0 82 location = this._forceTrailingSlash(location);
michael@0 83
michael@0 84 // When we find a valid response, package up the service information
michael@0 85 // and pass it on.
michael@0 86 let service = {
michael@0 87 location: location,
michael@0 88 target: target
michael@0 89 };
michael@0 90
michael@0 91 try {
michael@0 92 this._processService(service);
michael@0 93 } catch (e) {}
michael@0 94
michael@0 95 return true;
michael@0 96 }
michael@0 97 return false;
michael@0 98 }.bind(this));
michael@0 99 },
michael@0 100
michael@0 101 onStopListening: function(aSocket, aStatus) {
michael@0 102 // This is fired when the socket is closed expectedly or unexpectedly.
michael@0 103 // nsITimer.cancel() is a no-op if the timer is not active.
michael@0 104 this._searchTimeout.cancel();
michael@0 105 this._searchSocket = null;
michael@0 106 },
michael@0 107
michael@0 108 // Start a search. Make it continuous by passing an interval (in milliseconds).
michael@0 109 // This will stop a current search loop because the timer resets itself.
michael@0 110 search: function search(aInterval) {
michael@0 111 if (aInterval > 0) {
michael@0 112 this._searchInterval = aInterval || 0;
michael@0 113 this._searchRepeat.initWithCallback(this._search.bind(this), this._searchInterval, Ci.nsITimer.TYPE_REPEATING_SLACK);
michael@0 114 }
michael@0 115 this._search();
michael@0 116 },
michael@0 117
michael@0 118 // Stop the current continuous search
michael@0 119 stopSearch: function stopSearch() {
michael@0 120 this._searchRepeat.cancel();
michael@0 121 },
michael@0 122
michael@0 123 _usingLAN: function() {
michael@0 124 let network = Cc["@mozilla.org/network/network-link-service;1"].getService(Ci.nsINetworkLinkService);
michael@0 125 return (network.linkType == Ci.nsINetworkLinkService.LINK_TYPE_WIFI || network.linkType == Ci.nsINetworkLinkService.LINK_TYPE_ETHERNET);
michael@0 126 },
michael@0 127
michael@0 128 _search: function _search() {
michael@0 129 // If a search is already active, shut it down.
michael@0 130 this._searchShutdown();
michael@0 131
michael@0 132 // We only search if on local network
michael@0 133 if (!this._usingLAN()) {
michael@0 134 return;
michael@0 135 }
michael@0 136
michael@0 137 // Update the timestamp so we can use it to clean out stale services the
michael@0 138 // next time we search.
michael@0 139 this._searchTimestamp = Date.now();
michael@0 140
michael@0 141 // Look for any fixed IP targets. Some routers might be configured to block
michael@0 142 // UDP broadcasts, so this is a way to skip discovery.
michael@0 143 this._searchFixedTargets();
michael@0 144
michael@0 145 // Perform a UDP broadcast to search for SSDP devices
michael@0 146 let socket = Cc["@mozilla.org/network/udp-socket;1"].createInstance(Ci.nsIUDPSocket);
michael@0 147 try {
michael@0 148 socket.init(SSDP_PORT, false);
michael@0 149 socket.asyncListen(this);
michael@0 150 } catch (e) {
michael@0 151 // We were unable to create the broadcast socket. Just return, but don't
michael@0 152 // kill the interval timer. This might work next time.
michael@0 153 log("failed to start socket: " + e);
michael@0 154 return;
michael@0 155 }
michael@0 156
michael@0 157 this._searchSocket = socket;
michael@0 158 this._searchTimeout.initWithCallback(this._searchShutdown.bind(this), SSDP_DISCOVER_TIMEOUT, Ci.nsITimer.TYPE_ONE_SHOT);
michael@0 159
michael@0 160 let data = SSDP_DISCOVER_PACKET;
michael@0 161 for (let [key, target] of this._targets) {
michael@0 162 let msgData = data.replace("%SEARCH_TARGET%", target.target);
michael@0 163 try {
michael@0 164 let msgRaw = converter.convertToByteArray(msgData);
michael@0 165 socket.send(SSDP_ADDRESS, SSDP_PORT, msgRaw, msgRaw.length);
michael@0 166 } catch (e) {
michael@0 167 log("failed to convert to byte array: " + e);
michael@0 168 }
michael@0 169 }
michael@0 170 },
michael@0 171
michael@0 172 _searchFixedTargets: function _searchFixedTargets() {
michael@0 173 let fixedTargets = null;
michael@0 174 try {
michael@0 175 fixedTargets = Services.prefs.getCharPref("browser.casting.fixedTargets");
michael@0 176 } catch (e) {}
michael@0 177
michael@0 178 if (!fixedTargets) {
michael@0 179 return;
michael@0 180 }
michael@0 181
michael@0 182 fixedTargets = JSON.parse(fixedTargets);
michael@0 183 for (let fixedTarget of fixedTargets) {
michael@0 184 // Verify we have the right data
michael@0 185 if (!"location" in fixedTarget || !"target" in fixedTarget) {
michael@0 186 continue;
michael@0 187 }
michael@0 188
michael@0 189 fixedTarget.location = this._forceTrailingSlash(fixedTarget.location);
michael@0 190
michael@0 191 let service = {
michael@0 192 location: fixedTarget.location,
michael@0 193 target: fixedTarget.target
michael@0 194 };
michael@0 195
michael@0 196 // We don't assume the fixed target is ready. We still need to ping it.
michael@0 197 try {
michael@0 198 this._processService(service);
michael@0 199 } catch (e) {}
michael@0 200 }
michael@0 201 },
michael@0 202
michael@0 203 // Called when the search timeout is hit. We use it to cleanup the socket and
michael@0 204 // perform some post-processing on the services list.
michael@0 205 _searchShutdown: function _searchShutdown() {
michael@0 206 if (this._searchSocket) {
michael@0 207 // This will call onStopListening.
michael@0 208 this._searchSocket.close();
michael@0 209
michael@0 210 // Clean out any stale services
michael@0 211 for (let [key, service] of this._services) {
michael@0 212 if (service.lastPing != this._searchTimestamp) {
michael@0 213 Services.obs.notifyObservers(null, "ssdp-service-lost", service.location);
michael@0 214 this._services.delete(service.location);
michael@0 215 }
michael@0 216 }
michael@0 217 }
michael@0 218 },
michael@0 219
michael@0 220 registerTarget: function registerTarget(aTarget, aAppFactory) {
michael@0 221 // Only add if we don't already know about this target
michael@0 222 if (!this._targets.has(aTarget)) {
michael@0 223 this._targets.set(aTarget, { target: aTarget, factory: aAppFactory });
michael@0 224 }
michael@0 225 },
michael@0 226
michael@0 227 findAppForService: function findAppForService(aService, aApp) {
michael@0 228 if (!aService || !aService.target) {
michael@0 229 return null;
michael@0 230 }
michael@0 231
michael@0 232 // Find the registration for the target
michael@0 233 if (this._targets.has(aService.target)) {
michael@0 234 return this._targets.get(aService.target).factory(aService, aApp);
michael@0 235 }
michael@0 236 return null;
michael@0 237 },
michael@0 238
michael@0 239 findServiceForLocation: function findServiceForLocation(aLocation) {
michael@0 240 if (this._services.has(aLocation)) {
michael@0 241 return this._services.get(aLocation);
michael@0 242 }
michael@0 243 return null;
michael@0 244 },
michael@0 245
michael@0 246 // Returns an array copy of the active services
michael@0 247 get services() {
michael@0 248 let array = [];
michael@0 249 for (let [key, service] of this._services) {
michael@0 250 array.push(service);
michael@0 251 }
michael@0 252 return array;
michael@0 253 },
michael@0 254
michael@0 255 _processService: function _processService(aService) {
michael@0 256 // Use the REST api to request more information about this service
michael@0 257 let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
michael@0 258 xhr.open("GET", aService.location, true);
michael@0 259 xhr.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
michael@0 260 xhr.overrideMimeType("text/xml");
michael@0 261
michael@0 262 xhr.addEventListener("load", (function() {
michael@0 263 if (xhr.status == 200) {
michael@0 264 let doc = xhr.responseXML;
michael@0 265 aService.appsURL = xhr.getResponseHeader("Application-URL");
michael@0 266 if (aService.appsURL && !aService.appsURL.endsWith("/"))
michael@0 267 aService.appsURL += "/";
michael@0 268 aService.friendlyName = doc.querySelector("friendlyName").textContent;
michael@0 269 aService.uuid = doc.querySelector("UDN").textContent;
michael@0 270 aService.manufacturer = doc.querySelector("manufacturer").textContent;
michael@0 271 aService.modelName = doc.querySelector("modelName").textContent;
michael@0 272
michael@0 273 // Only add and notify if we don't already know about this service
michael@0 274 if (!this._services.has(aService.location)) {
michael@0 275 this._services.set(aService.location, aService);
michael@0 276 Services.obs.notifyObservers(null, "ssdp-service-found", aService.location);
michael@0 277 }
michael@0 278
michael@0 279 // Make sure we remember this service is not stale
michael@0 280 this._services.get(aService.location).lastPing = this._searchTimestamp;
michael@0 281 }
michael@0 282 }).bind(this), false);
michael@0 283
michael@0 284 xhr.send(null);
michael@0 285 }
michael@0 286 }

mercurial