1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/services/sync/modules/engines/clients.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,468 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +this.EXPORTED_SYMBOLS = [ 1.9 + "ClientEngine", 1.10 + "ClientsRec" 1.11 +]; 1.12 + 1.13 +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; 1.14 + 1.15 +Cu.import("resource://services-common/stringbundle.js"); 1.16 +Cu.import("resource://services-sync/constants.js"); 1.17 +Cu.import("resource://services-sync/engines.js"); 1.18 +Cu.import("resource://services-sync/record.js"); 1.19 +Cu.import("resource://services-sync/util.js"); 1.20 + 1.21 +const CLIENTS_TTL = 1814400; // 21 days 1.22 +const CLIENTS_TTL_REFRESH = 604800; // 7 days 1.23 + 1.24 +const SUPPORTED_PROTOCOL_VERSIONS = ["1.1", "1.5"]; 1.25 + 1.26 +this.ClientsRec = function ClientsRec(collection, id) { 1.27 + CryptoWrapper.call(this, collection, id); 1.28 +} 1.29 +ClientsRec.prototype = { 1.30 + __proto__: CryptoWrapper.prototype, 1.31 + _logName: "Sync.Record.Clients", 1.32 + ttl: CLIENTS_TTL 1.33 +}; 1.34 + 1.35 +Utils.deferGetSet(ClientsRec, "cleartext", ["name", "type", "commands", "version", "protocols"]); 1.36 + 1.37 + 1.38 +this.ClientEngine = function ClientEngine(service) { 1.39 + SyncEngine.call(this, "Clients", service); 1.40 + 1.41 + // Reset the client on every startup so that we fetch recent clients 1.42 + this._resetClient(); 1.43 +} 1.44 +ClientEngine.prototype = { 1.45 + __proto__: SyncEngine.prototype, 1.46 + _storeObj: ClientStore, 1.47 + _recordObj: ClientsRec, 1.48 + _trackerObj: ClientsTracker, 1.49 + 1.50 + // Always sync client data as it controls other sync behavior 1.51 + get enabled() true, 1.52 + 1.53 + get lastRecordUpload() { 1.54 + return Svc.Prefs.get(this.name + ".lastRecordUpload", 0); 1.55 + }, 1.56 + set lastRecordUpload(value) { 1.57 + Svc.Prefs.set(this.name + ".lastRecordUpload", Math.floor(value)); 1.58 + }, 1.59 + 1.60 + // Aggregate some stats on the composition of clients on this account 1.61 + get stats() { 1.62 + let stats = { 1.63 + hasMobile: this.localType == "mobile", 1.64 + names: [this.localName], 1.65 + numClients: 1, 1.66 + }; 1.67 + 1.68 + for each (let {name, type} in this._store._remoteClients) { 1.69 + stats.hasMobile = stats.hasMobile || type == "mobile"; 1.70 + stats.names.push(name); 1.71 + stats.numClients++; 1.72 + } 1.73 + 1.74 + return stats; 1.75 + }, 1.76 + 1.77 + /** 1.78 + * Obtain information about device types. 1.79 + * 1.80 + * Returns a Map of device types to integer counts. 1.81 + */ 1.82 + get deviceTypes() { 1.83 + let counts = new Map(); 1.84 + 1.85 + counts.set(this.localType, 1); 1.86 + 1.87 + for each (let record in this._store._remoteClients) { 1.88 + let type = record.type; 1.89 + if (!counts.has(type)) { 1.90 + counts.set(type, 0); 1.91 + } 1.92 + 1.93 + counts.set(type, counts.get(type) + 1); 1.94 + } 1.95 + 1.96 + return counts; 1.97 + }, 1.98 + 1.99 + get localID() { 1.100 + // Generate a random GUID id we don't have one 1.101 + let localID = Svc.Prefs.get("client.GUID", ""); 1.102 + return localID == "" ? this.localID = Utils.makeGUID() : localID; 1.103 + }, 1.104 + set localID(value) Svc.Prefs.set("client.GUID", value), 1.105 + 1.106 + get localName() { 1.107 + let localName = Svc.Prefs.get("client.name", ""); 1.108 + if (localName != "") 1.109 + return localName; 1.110 + 1.111 + // Generate a client name if we don't have a useful one yet 1.112 + let env = Cc["@mozilla.org/process/environment;1"] 1.113 + .getService(Ci.nsIEnvironment); 1.114 + let user = env.get("USER") || env.get("USERNAME") || 1.115 + Svc.Prefs.get("account") || Svc.Prefs.get("username"); 1.116 + 1.117 + let appName; 1.118 + let brand = new StringBundle("chrome://branding/locale/brand.properties"); 1.119 + let brandName = brand.get("brandShortName"); 1.120 + try { 1.121 + let syncStrings = new StringBundle("chrome://browser/locale/sync.properties"); 1.122 + appName = syncStrings.getFormattedString("sync.defaultAccountApplication", [brandName]); 1.123 + } catch (ex) {} 1.124 + appName = appName || brandName; 1.125 + 1.126 + let system = 1.127 + // 'device' is defined on unix systems 1.128 + Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2).get("device") || 1.129 + // hostname of the system, usually assigned by the user or admin 1.130 + Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2).get("host") || 1.131 + // fall back on ua info string 1.132 + Cc["@mozilla.org/network/protocol;1?name=http"].getService(Ci.nsIHttpProtocolHandler).oscpu; 1.133 + 1.134 + return this.localName = Str.sync.get("client.name2", [user, appName, system]); 1.135 + }, 1.136 + set localName(value) Svc.Prefs.set("client.name", value), 1.137 + 1.138 + get localType() Svc.Prefs.get("client.type", "desktop"), 1.139 + set localType(value) Svc.Prefs.set("client.type", value), 1.140 + 1.141 + isMobile: function isMobile(id) { 1.142 + if (this._store._remoteClients[id]) 1.143 + return this._store._remoteClients[id].type == "mobile"; 1.144 + return false; 1.145 + }, 1.146 + 1.147 + _syncStartup: function _syncStartup() { 1.148 + // Reupload new client record periodically. 1.149 + if (Date.now() / 1000 - this.lastRecordUpload > CLIENTS_TTL_REFRESH) { 1.150 + this._tracker.addChangedID(this.localID); 1.151 + this.lastRecordUpload = Date.now() / 1000; 1.152 + } 1.153 + SyncEngine.prototype._syncStartup.call(this); 1.154 + }, 1.155 + 1.156 + // Always process incoming items because they might have commands 1.157 + _reconcile: function _reconcile() { 1.158 + return true; 1.159 + }, 1.160 + 1.161 + // Treat reset the same as wiping for locally cached clients 1.162 + _resetClient: function _resetClient() this._wipeClient(), 1.163 + 1.164 + _wipeClient: function _wipeClient() { 1.165 + SyncEngine.prototype._resetClient.call(this); 1.166 + this._store.wipe(); 1.167 + }, 1.168 + 1.169 + removeClientData: function removeClientData() { 1.170 + let res = this.service.resource(this.engineURL + "/" + this.localID); 1.171 + res.delete(); 1.172 + }, 1.173 + 1.174 + // Override the default behavior to delete bad records from the server. 1.175 + handleHMACMismatch: function handleHMACMismatch(item, mayRetry) { 1.176 + this._log.debug("Handling HMAC mismatch for " + item.id); 1.177 + 1.178 + let base = SyncEngine.prototype.handleHMACMismatch.call(this, item, mayRetry); 1.179 + if (base != SyncEngine.kRecoveryStrategy.error) 1.180 + return base; 1.181 + 1.182 + // It's a bad client record. Save it to be deleted at the end of the sync. 1.183 + this._log.debug("Bad client record detected. Scheduling for deletion."); 1.184 + this._deleteId(item.id); 1.185 + 1.186 + // Neither try again nor error; we're going to delete it. 1.187 + return SyncEngine.kRecoveryStrategy.ignore; 1.188 + }, 1.189 + 1.190 + /** 1.191 + * A hash of valid commands that the client knows about. The key is a command 1.192 + * and the value is a hash containing information about the command such as 1.193 + * number of arguments and description. 1.194 + */ 1.195 + _commands: { 1.196 + resetAll: { args: 0, desc: "Clear temporary local data for all engines" }, 1.197 + resetEngine: { args: 1, desc: "Clear temporary local data for engine" }, 1.198 + wipeAll: { args: 0, desc: "Delete all client data for all engines" }, 1.199 + wipeEngine: { args: 1, desc: "Delete all client data for engine" }, 1.200 + logout: { args: 0, desc: "Log out client" }, 1.201 + displayURI: { args: 3, desc: "Instruct a client to display a URI" }, 1.202 + }, 1.203 + 1.204 + /** 1.205 + * Remove any commands for the local client and mark it for upload. 1.206 + */ 1.207 + clearCommands: function clearCommands() { 1.208 + delete this.localCommands; 1.209 + this._tracker.addChangedID(this.localID); 1.210 + }, 1.211 + 1.212 + /** 1.213 + * Sends a command+args pair to a specific client. 1.214 + * 1.215 + * @param command Command string 1.216 + * @param args Array of arguments/data for command 1.217 + * @param clientId Client to send command to 1.218 + */ 1.219 + _sendCommandToClient: function sendCommandToClient(command, args, clientId) { 1.220 + this._log.trace("Sending " + command + " to " + clientId); 1.221 + 1.222 + let client = this._store._remoteClients[clientId]; 1.223 + if (!client) { 1.224 + throw new Error("Unknown remote client ID: '" + clientId + "'."); 1.225 + } 1.226 + 1.227 + // notDupe compares two commands and returns if they are not equal. 1.228 + let notDupe = function(other) { 1.229 + return other.command != command || !Utils.deepEquals(other.args, args); 1.230 + }; 1.231 + 1.232 + let action = { 1.233 + command: command, 1.234 + args: args, 1.235 + }; 1.236 + 1.237 + if (!client.commands) { 1.238 + client.commands = [action]; 1.239 + } 1.240 + // Add the new action if there are no duplicates. 1.241 + else if (client.commands.every(notDupe)) { 1.242 + client.commands.push(action); 1.243 + } 1.244 + // It must be a dupe. Skip. 1.245 + else { 1.246 + return; 1.247 + } 1.248 + 1.249 + this._log.trace("Client " + clientId + " got a new action: " + [command, args]); 1.250 + this._tracker.addChangedID(clientId); 1.251 + }, 1.252 + 1.253 + /** 1.254 + * Check if the local client has any remote commands and perform them. 1.255 + * 1.256 + * @return false to abort sync 1.257 + */ 1.258 + processIncomingCommands: function processIncomingCommands() { 1.259 + return this._notify("clients:process-commands", "", function() { 1.260 + let commands = this.localCommands; 1.261 + 1.262 + // Immediately clear out the commands as we've got them locally. 1.263 + this.clearCommands(); 1.264 + 1.265 + // Process each command in order. 1.266 + for each ({command: command, args: args} in commands) { 1.267 + this._log.debug("Processing command: " + command + "(" + args + ")"); 1.268 + 1.269 + let engines = [args[0]]; 1.270 + switch (command) { 1.271 + case "resetAll": 1.272 + engines = null; 1.273 + // Fallthrough 1.274 + case "resetEngine": 1.275 + this.service.resetClient(engines); 1.276 + break; 1.277 + case "wipeAll": 1.278 + engines = null; 1.279 + // Fallthrough 1.280 + case "wipeEngine": 1.281 + this.service.wipeClient(engines); 1.282 + break; 1.283 + case "logout": 1.284 + this.service.logout(); 1.285 + return false; 1.286 + case "displayURI": 1.287 + this._handleDisplayURI.apply(this, args); 1.288 + break; 1.289 + default: 1.290 + this._log.debug("Received an unknown command: " + command); 1.291 + break; 1.292 + } 1.293 + } 1.294 + 1.295 + return true; 1.296 + })(); 1.297 + }, 1.298 + 1.299 + /** 1.300 + * Validates and sends a command to a client or all clients. 1.301 + * 1.302 + * Calling this does not actually sync the command data to the server. If the 1.303 + * client already has the command/args pair, it won't receive a duplicate 1.304 + * command. 1.305 + * 1.306 + * @param command 1.307 + * Command to invoke on remote clients 1.308 + * @param args 1.309 + * Array of arguments to give to the command 1.310 + * @param clientId 1.311 + * Client ID to send command to. If undefined, send to all remote 1.312 + * clients. 1.313 + */ 1.314 + sendCommand: function sendCommand(command, args, clientId) { 1.315 + let commandData = this._commands[command]; 1.316 + // Don't send commands that we don't know about. 1.317 + if (!commandData) { 1.318 + this._log.error("Unknown command to send: " + command); 1.319 + return; 1.320 + } 1.321 + // Don't send a command with the wrong number of arguments. 1.322 + else if (!args || args.length != commandData.args) { 1.323 + this._log.error("Expected " + commandData.args + " args for '" + 1.324 + command + "', but got " + args); 1.325 + return; 1.326 + } 1.327 + 1.328 + if (clientId) { 1.329 + this._sendCommandToClient(command, args, clientId); 1.330 + } else { 1.331 + for (let id in this._store._remoteClients) { 1.332 + this._sendCommandToClient(command, args, id); 1.333 + } 1.334 + } 1.335 + }, 1.336 + 1.337 + /** 1.338 + * Send a URI to another client for display. 1.339 + * 1.340 + * A side effect is the score is increased dramatically to incur an 1.341 + * immediate sync. 1.342 + * 1.343 + * If an unknown client ID is specified, sendCommand() will throw an 1.344 + * Error object. 1.345 + * 1.346 + * @param uri 1.347 + * URI (as a string) to send and display on the remote client 1.348 + * @param clientId 1.349 + * ID of client to send the command to. If not defined, will be sent 1.350 + * to all remote clients. 1.351 + * @param title 1.352 + * Title of the page being sent. 1.353 + */ 1.354 + sendURIToClientForDisplay: function sendURIToClientForDisplay(uri, clientId, title) { 1.355 + this._log.info("Sending URI to client: " + uri + " -> " + 1.356 + clientId + " (" + title + ")"); 1.357 + this.sendCommand("displayURI", [uri, this.localID, title], clientId); 1.358 + 1.359 + this._tracker.score += SCORE_INCREMENT_XLARGE; 1.360 + }, 1.361 + 1.362 + /** 1.363 + * Handle a single received 'displayURI' command. 1.364 + * 1.365 + * Interested parties should observe the "weave:engine:clients:display-uri" 1.366 + * topic. The callback will receive an object as the subject parameter with 1.367 + * the following keys: 1.368 + * 1.369 + * uri URI (string) that is requested for display. 1.370 + * clientId ID of client that sent the command. 1.371 + * title Title of page that loaded URI (likely) corresponds to. 1.372 + * 1.373 + * The 'data' parameter to the callback will not be defined. 1.374 + * 1.375 + * @param uri 1.376 + * String URI that was received 1.377 + * @param clientId 1.378 + * ID of client that sent URI 1.379 + * @param title 1.380 + * String title of page that URI corresponds to. Older clients may not 1.381 + * send this. 1.382 + */ 1.383 + _handleDisplayURI: function _handleDisplayURI(uri, clientId, title) { 1.384 + this._log.info("Received a URI for display: " + uri + " (" + title + 1.385 + ") from " + clientId); 1.386 + 1.387 + let subject = {uri: uri, client: clientId, title: title}; 1.388 + Svc.Obs.notify("weave:engine:clients:display-uri", subject); 1.389 + } 1.390 +}; 1.391 + 1.392 +function ClientStore(name, engine) { 1.393 + Store.call(this, name, engine); 1.394 +} 1.395 +ClientStore.prototype = { 1.396 + __proto__: Store.prototype, 1.397 + 1.398 + create: function create(record) this.update(record), 1.399 + 1.400 + update: function update(record) { 1.401 + // Only grab commands from the server; local name/type always wins 1.402 + if (record.id == this.engine.localID) 1.403 + this.engine.localCommands = record.commands; 1.404 + else 1.405 + this._remoteClients[record.id] = record.cleartext; 1.406 + }, 1.407 + 1.408 + createRecord: function createRecord(id, collection) { 1.409 + let record = new ClientsRec(collection, id); 1.410 + 1.411 + // Package the individual components into a record for the local client 1.412 + if (id == this.engine.localID) { 1.413 + record.name = this.engine.localName; 1.414 + record.type = this.engine.localType; 1.415 + record.commands = this.engine.localCommands; 1.416 + record.version = Services.appinfo.version; 1.417 + record.protocols = SUPPORTED_PROTOCOL_VERSIONS; 1.418 + } 1.419 + else 1.420 + record.cleartext = this._remoteClients[id]; 1.421 + 1.422 + return record; 1.423 + }, 1.424 + 1.425 + itemExists: function itemExists(id) id in this.getAllIDs(), 1.426 + 1.427 + getAllIDs: function getAllIDs() { 1.428 + let ids = {}; 1.429 + ids[this.engine.localID] = true; 1.430 + for (let id in this._remoteClients) 1.431 + ids[id] = true; 1.432 + return ids; 1.433 + }, 1.434 + 1.435 + wipe: function wipe() { 1.436 + this._remoteClients = {}; 1.437 + }, 1.438 +}; 1.439 + 1.440 +function ClientsTracker(name, engine) { 1.441 + Tracker.call(this, name, engine); 1.442 + Svc.Obs.add("weave:engine:start-tracking", this); 1.443 + Svc.Obs.add("weave:engine:stop-tracking", this); 1.444 +} 1.445 +ClientsTracker.prototype = { 1.446 + __proto__: Tracker.prototype, 1.447 + 1.448 + _enabled: false, 1.449 + 1.450 + observe: function observe(subject, topic, data) { 1.451 + switch (topic) { 1.452 + case "weave:engine:start-tracking": 1.453 + if (!this._enabled) { 1.454 + Svc.Prefs.observe("client.name", this); 1.455 + this._enabled = true; 1.456 + } 1.457 + break; 1.458 + case "weave:engine:stop-tracking": 1.459 + if (this._enabled) { 1.460 + Svc.Prefs.ignore("clients.name", this); 1.461 + this._enabled = false; 1.462 + } 1.463 + break; 1.464 + case "nsPref:changed": 1.465 + this._log.debug("client.name preference changed"); 1.466 + this.addChangedID(Svc.Prefs.get("client.GUID")); 1.467 + this.score += SCORE_INCREMENT_XLARGE; 1.468 + break; 1.469 + } 1.470 + } 1.471 +};