diff options
Diffstat (limited to 'components/weave/src/common')
-rw-r--r-- | components/weave/src/common/hawkclient.js | 346 | ||||
-rw-r--r-- | components/weave/src/common/hawkrequest.js | 198 | ||||
-rw-r--r-- | components/weave/src/common/logmanager.js | 331 | ||||
-rw-r--r-- | components/weave/src/common/observers.js | 150 | ||||
-rw-r--r-- | components/weave/src/common/rest.js | 764 | ||||
-rw-r--r-- | components/weave/src/common/services-common.js | 11 | ||||
-rw-r--r-- | components/weave/src/common/stringbundle.js | 203 | ||||
-rw-r--r-- | components/weave/src/common/tokenserverclient.js | 459 |
8 files changed, 0 insertions, 2462 deletions
diff --git a/components/weave/src/common/hawkclient.js b/components/weave/src/common/hawkclient.js deleted file mode 100644 index 88e9c2f2d..000000000 --- a/components/weave/src/common/hawkclient.js +++ /dev/null @@ -1,346 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -/* - * HAWK is an HTTP authentication scheme using a message authentication code - * (MAC) algorithm to provide partial HTTP request cryptographic verification. - * - * For details, see: https://github.com/hueniverse/hawk - * - * With HAWK, it is essential that the clocks on clients and server not have an - * absolute delta of greater than one minute, as the HAWK protocol uses - * timestamps to reduce the possibility of replay attacks. However, it is - * likely that some clients' clocks will be more than a little off, especially - * in mobile devices, which would break HAWK-based services (like sync and - * firefox accounts) for those clients. - * - * This library provides a stateful HAWK client that calculates (roughly) the - * clock delta on the client vs the server. The library provides an interface - * for deriving HAWK credentials and making HAWK-authenticated REST requests to - * a single remote server. Therefore, callers who want to interact with - * multiple HAWK services should instantiate one HawkClient per service. - */ - -this.EXPORTED_SYMBOLS = ["HawkClient"]; - -var {interfaces: Ci, utils: Cu} = Components; - -Cu.import("resource://services-crypto/utils.js"); -Cu.import("resource://services-common/hawkrequest.js"); -Cu.import("resource://services-common/observers.js"); -Cu.import("resource://gre/modules/Promise.jsm"); -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); - -// log.appender.dump should be one of "Fatal", "Error", "Warn", "Info", "Config", -// "Debug", "Trace" or "All". If none is specified, "Error" will be used by -// default. -// Note however that Sync will also add this log to *its* DumpAppender, so -// in a Sync context it shouldn't be necessary to adjust this - however, that -// also means error logs are likely to be dump'd twice but that's OK. -const PREF_LOG_LEVEL = "services.common.hawk.log.appender.dump"; - -// A pref that can be set so "sensitive" information (eg, personally -// identifiable info, credentials, etc) will be logged. -const PREF_LOG_SENSITIVE_DETAILS = "services.common.hawk.log.sensitive"; - -XPCOMUtils.defineLazyGetter(this, "log", function() { - let log = Log.repository.getLogger("Hawk"); - // We set the log itself to "debug" and set the level from the preference to - // the appender. This allows other things to send the logs to different - // appenders, while still allowing the pref to control what is seen via dump() - log.level = Log.Level.Debug; - let appender = new Log.DumpAppender(); - log.addAppender(appender); - appender.level = Log.Level.Error; - try { - let level = - Services.prefs.getPrefType(PREF_LOG_LEVEL) == Ci.nsIPrefBranch.PREF_STRING - && Services.prefs.getCharPref(PREF_LOG_LEVEL); - appender.level = Log.Level[level] || Log.Level.Error; - } catch (e) { - log.error(e); - } - - return log; -}); - -// A boolean to indicate if personally identifiable information (or anything -// else sensitive, such as credentials) should be logged. -XPCOMUtils.defineLazyGetter(this, 'logPII', function() { - try { - return Services.prefs.getBoolPref(PREF_LOG_SENSITIVE_DETAILS); - } catch (_) { - return false; - } -}); - -/* - * A general purpose client for making HAWK authenticated requests to a single - * host. Keeps track of the clock offset between the client and the host for - * computation of the timestamp in the HAWK Authorization header. - * - * Clients should create one HawkClient object per each server they wish to - * interact with. - * - * @param host - * The url of the host - */ -this.HawkClient = function(host) { - this.host = host; - - // Clock offset in milliseconds between our client's clock and the date - // reported in responses from our host. - this._localtimeOffsetMsec = 0; -} - -this.HawkClient.prototype = { - - /* - * A boolean for feature detection. - */ - willUTF8EncodeRequests: HAWKAuthenticatedRESTRequest.prototype.willUTF8EncodeObjectRequests, - - /* - * Construct an error message for a response. Private. - * - * @param restResponse - * A RESTResponse object from a RESTRequest - * - * @param error - * A string or object describing the error - */ - _constructError: function(restResponse, error) { - let errorObj = { - error: error, - // This object is likely to be JSON.stringify'd, but neither Error() - // objects nor Components.Exception objects do the right thing there, - // so we add a new element which is simply the .toString() version of - // the error object, so it does appear in JSON'd values. - errorString: error.toString(), - message: restResponse.statusText, - code: restResponse.status, - errno: restResponse.status, - toString() { - return this.code + ": " + this.message; - }, - }; - let retryAfter = restResponse.headers && restResponse.headers["retry-after"]; - retryAfter = retryAfter ? parseInt(retryAfter) : retryAfter; - if (retryAfter) { - errorObj.retryAfter = retryAfter; - // and notify observers of the retry interval - if (this.observerPrefix) { - Observers.notify(this.observerPrefix + ":backoff:interval", retryAfter); - } - } - return errorObj; - }, - - /* - * - * Update clock offset by determining difference from date gives in the (RFC - * 1123) Date header of a server response. Because HAWK tolerates a window - * of one minute of clock skew (so two minutes total since the skew can be - * positive or negative), the simple method of calculating offset here is - * probably good enough. We keep the value in milliseconds to make life - * easier, even though the value will not have millisecond accuracy. - * - * @param dateString - * An RFC 1123 date string (e.g., "Mon, 13 Jan 2014 21:45:06 GMT") - * - * For HAWK clock skew and replay protection, see - * https://github.com/hueniverse/hawk#replay-protection - */ - _updateClockOffset: function(dateString) { - try { - let serverDateMsec = Date.parse(dateString); - this._localtimeOffsetMsec = serverDateMsec - this.now(); - log.debug("Clock offset vs " + this.host + ": " + this._localtimeOffsetMsec); - } catch(err) { - log.warn("Bad date header in server response: " + dateString); - } - }, - - /* - * Get the current clock offset in milliseconds. - * - * The offset is the number of milliseconds that must be added to the client - * clock to make it equal to the server clock. For example, if the client is - * five minutes ahead of the server, the localtimeOffsetMsec will be -300000. - */ - get localtimeOffsetMsec() { - return this._localtimeOffsetMsec; - }, - - /* - * return current time in milliseconds - */ - now: function() { - return Date.now(); - }, - - /* A general method for sending raw RESTRequest calls authorized using HAWK - * - * @param path - * API endpoint path - * @param method - * The HTTP request method - * @param credentials - * Hawk credentials - * @param payloadObj - * An object that can be encodable as JSON as the payload of the - * request - * @param extraHeaders - * An object with header/value pairs to send with the request. - * @return Promise - * Returns a promise that resolves to the response of the API call, - * or is rejected with an error. If the server response can be parsed - * as JSON and contains an 'error' property, the promise will be - * rejected with this JSON-parsed response. - */ - request: function(path, method, credentials=null, payloadObj={}, extraHeaders = {}, - retryOK=true) { - method = method.toLowerCase(); - - let deferred = Promise.defer(); - let uri = this.host + path; - let self = this; - - function _onComplete(error) { - // |error| can be either a normal caught error or an explicitly created - // Components.Exception() error. Log it now as it might not end up - // correctly in the logs by the time it's passed through _constructError. - if (error) { - log.warn("hawk request error", error); - } - // If there's no response there's nothing else to do. - if (!this.response) { - deferred.reject(error); - return; - } - let restResponse = this.response; - let status = restResponse.status; - - log.debug("(Response) " + path + ": code: " + status + - " - Status text: " + restResponse.statusText); - if (logPII) { - log.debug("Response text: " + restResponse.body); - } - - // All responses may have backoff headers, which are a server-side safety - // valve to allow slowing down clients without hurting performance. - self._maybeNotifyBackoff(restResponse, "x-weave-backoff"); - self._maybeNotifyBackoff(restResponse, "x-backoff"); - - if (error) { - // When things really blow up, reconstruct an error object that follows - // the general format of the server on error responses. - return deferred.reject(self._constructError(restResponse, error)); - } - - self._updateClockOffset(restResponse.headers["date"]); - - if (status === 401 && retryOK && !("retry-after" in restResponse.headers)) { - // Retry once if we were rejected due to a bad timestamp. - // Clock offset is adjusted already in the top of this function. - log.debug("Received 401 for " + path + ": retrying"); - return deferred.resolve( - self.request(path, method, credentials, payloadObj, extraHeaders, false)); - } - - // If the server returned a json error message, use it in the rejection - // of the promise. - // - // In the case of a 401, in which we are probably being rejected for a - // bad timestamp, retry exactly once, during which time clock offset will - // be adjusted. - - let jsonResponse = {}; - try { - jsonResponse = JSON.parse(restResponse.body); - } catch(notJSON) {} - - let okResponse = (200 <= status && status < 300); - if (!okResponse || jsonResponse.error) { - if (jsonResponse.error) { - return deferred.reject(jsonResponse); - } - return deferred.reject(self._constructError(restResponse, "Request failed")); - } - // It's up to the caller to know how to decode the response. - // We just return the whole response. - deferred.resolve(this.response); - }; - - function onComplete(error) { - try { - // |this| is the RESTRequest object and we need to ensure _onComplete - // gets the same one. - _onComplete.call(this, error); - } catch (ex) { - log.error("Unhandled exception processing response", ex); - deferred.reject(ex); - } - } - - let extra = { - now: this.now(), - localtimeOffsetMsec: this.localtimeOffsetMsec, - headers: extraHeaders - }; - - let request = this.newHAWKAuthenticatedRESTRequest(uri, credentials, extra); - try { - if (method == "post" || method == "put" || method == "patch") { - request[method](payloadObj, onComplete); - } else { - request[method](onComplete); - } - } catch (ex) { - log.error("Failed to make hawk request", ex); - deferred.reject(ex); - } - - return deferred.promise; - }, - - /* - * The prefix used for all notifications sent by this module. This - * allows the handler of notifications to be sure they are handling - * notifications for the service they expect. - * - * If not set, no notifications will be sent. - */ - observerPrefix: null, - - // Given an optional header value, notify that a backoff has been requested. - _maybeNotifyBackoff: function (response, headerName) { - if (!this.observerPrefix || !response.headers) { - return; - } - let headerVal = response.headers[headerName]; - if (!headerVal) { - return; - } - let backoffInterval; - try { - backoffInterval = parseInt(headerVal, 10); - } catch (ex) { - log.error("hawkclient response had invalid backoff value in '" + - headerName + "' header: " + headerVal); - return; - } - Observers.notify(this.observerPrefix + ":backoff:interval", backoffInterval); - }, - - // override points for testing. - newHAWKAuthenticatedRESTRequest: function(uri, credentials, extra) { - return new HAWKAuthenticatedRESTRequest(uri, credentials, extra); - }, - -} diff --git a/components/weave/src/common/hawkrequest.js b/components/weave/src/common/hawkrequest.js deleted file mode 100644 index ecedb0147..000000000 --- a/components/weave/src/common/hawkrequest.js +++ /dev/null @@ -1,198 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -this.EXPORTED_SYMBOLS = [ - "HAWKAuthenticatedRESTRequest", - "deriveHawkCredentials" -]; - -Cu.import("resource://gre/modules/Preferences.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://services-common/rest.js"); -Cu.import("resource://gre/CommonUtils.jsm"); -Cu.import("resource://gre/modules/Credentials.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils", - "resource://services-crypto/utils.js"); - -const Prefs = new Preferences("services.common.rest."); - -/** - * Single-use HAWK-authenticated HTTP requests to RESTish resources. - * - * @param uri - * (String) URI for the RESTRequest constructor - * - * @param credentials - * (Object) Optional credentials for computing HAWK authentication - * header. - * - * @param payloadObj - * (Object) Optional object to be converted to JSON payload - * - * @param extra - * (Object) Optional extra params for HAWK header computation. - * Valid properties are: - * - * now: <current time in milliseconds>, - * localtimeOffsetMsec: <local clock offset vs server>, - * headers: <An object with header/value pairs to be sent - * as headers on the request> - * - * extra.localtimeOffsetMsec is the value in milliseconds that must be added to - * the local clock to make it agree with the server's clock. For instance, if - * the local clock is two minutes ahead of the server, the time offset in - * milliseconds will be -120000. - */ - -this.HAWKAuthenticatedRESTRequest = - function HawkAuthenticatedRESTRequest(uri, credentials, extra={}) { - RESTRequest.call(this, uri); - - this.credentials = credentials; - this.now = extra.now || Date.now(); - this.localtimeOffsetMsec = extra.localtimeOffsetMsec || 0; - this._log.trace("local time, offset: " + this.now + ", " + (this.localtimeOffsetMsec)); - this.extraHeaders = extra.headers || {}; - - // Expose for testing - this._intl = getIntl(); -}; -HAWKAuthenticatedRESTRequest.prototype = { - __proto__: RESTRequest.prototype, - - dispatch: function dispatch(method, data, onComplete, onProgress) { - let contentType = "text/plain"; - if (method == "POST" || method == "PUT" || method == "PATCH") { - contentType = "application/json"; - } - if (this.credentials) { - let options = { - now: this.now, - localtimeOffsetMsec: this.localtimeOffsetMsec, - credentials: this.credentials, - payload: data && JSON.stringify(data) || "", - contentType: contentType, - }; - let header = CryptoUtils.computeHAWK(this.uri, method, options); - this.setHeader("Authorization", header.field); - this._log.trace("hawk auth header: " + header.field); - } - - for (let header in this.extraHeaders) { - this.setHeader(header, this.extraHeaders[header]); - } - - this.setHeader("Content-Type", contentType); - - this.setHeader("Accept-Language", this._intl.accept_languages); - - return RESTRequest.prototype.dispatch.call( - this, method, data, onComplete, onProgress - ); - } -}; - - -/** - * Generic function to derive Hawk credentials. - * - * Hawk credentials are derived using shared secrets, which depend on the token - * in use. - * - * @param tokenHex - * The current session token encoded in hex - * @param context - * A context for the credentials. A protocol version will be prepended - * to the context, see Credentials.keyWord for more information. - * @param size - * The size in bytes of the expected derived buffer, - * defaults to 3 * 32. - * @return credentials - * Returns an object: - * { - * algorithm: sha256 - * id: the Hawk id (from the first 32 bytes derived) - * key: the Hawk key (from bytes 32 to 64) - * extra: size - 64 extra bytes (if size > 64) - * } - */ -this.deriveHawkCredentials = function deriveHawkCredentials(tokenHex, - context, - size = 96, - hexKey = false) { - let token = CommonUtils.hexToBytes(tokenHex); - let out = CryptoUtils.hkdf(token, undefined, Credentials.keyWord(context), size); - - let result = { - algorithm: "sha256", - key: hexKey ? CommonUtils.bytesAsHex(out.slice(32, 64)) : out.slice(32, 64), - id: CommonUtils.bytesAsHex(out.slice(0, 32)) - }; - if (size > 64) { - result.extra = out.slice(64); - } - - return result; -} - -// With hawk request, we send the user's accepted-languages with each request. -// To keep the number of times we read this pref at a minimum, maintain the -// preference in a stateful object that notices and updates itself when the -// pref is changed. -this.Intl = function Intl() { - // We won't actually query the pref until the first time we need it - this._accepted = ""; - this._everRead = false; - this._log = Log.repository.getLogger("Services.common.RESTRequest"); - this._log.level = Log.Level[Prefs.get("log.logger.rest.request")]; - this.init(); -}; - -this.Intl.prototype = { - init: function() { - Services.prefs.addObserver("intl.accept_languages", this, false); - }, - - uninit: function() { - Services.prefs.removeObserver("intl.accept_languages", this); - }, - - observe: function(subject, topic, data) { - this.readPref(); - }, - - readPref: function() { - this._everRead = true; - try { - this._accepted = Services.prefs.getComplexValue( - "intl.accept_languages", Ci.nsIPrefLocalizedString).data; - } catch (err) { - this._log.error("Error reading intl.accept_languages pref", err); - } - }, - - get accept_languages() { - if (!this._everRead) { - this.readPref(); - } - return this._accepted; - }, -}; - -// Singleton getter for Intl, creating an instance only when we first need it. -var intl = null; -function getIntl() { - if (!intl) { - intl = new Intl(); - } - return intl; -} - diff --git a/components/weave/src/common/logmanager.js b/components/weave/src/common/logmanager.js deleted file mode 100644 index c501229a9..000000000 --- a/components/weave/src/common/logmanager.js +++ /dev/null @@ -1,331 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -"use strict;" - -var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Services", - "resource://gre/modules/Services.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", - "resource://gre/modules/FileUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Log", - "resource://gre/modules/Log.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "OS", - "resource://gre/modules/osfile.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils", - "resource://gre/CommonUtils.jsm"); - -Cu.import("resource://gre/modules/Preferences.jsm"); -Cu.import("resource://gre/modules/Task.jsm"); - -this.EXPORTED_SYMBOLS = [ - "LogManager", -]; - -const DEFAULT_MAX_ERROR_AGE = 20 * 24 * 60 * 60; // 20 days - -// "shared" logs (ie, where the same log name is used by multiple LogManager -// instances) are a fact of life here - eg, FirefoxAccounts logs are used by -// both Sync and Reading List. -// However, different instances have different pref branches, so we need to -// handle when one pref branch says "Debug" and the other says "Error" -// So we (a) keep singleton console and dump appenders and (b) keep track -// of the minimum (ie, most verbose) level and use that. -// This avoids (a) the most recent setter winning (as that is indeterminate) -// and (b) multiple dump/console appenders being added to the same log multiple -// times, which would cause messages to appear twice. - -// Singletons used by each instance. -var formatter; -var dumpAppender; -var consoleAppender; - -// A set of all preference roots used by all instances. -var allBranches = new Set(); - -// A storage appender that is flushable to a file on disk. Policies for -// when to flush, to what file, log rotation etc are up to the consumer -// (although it does maintain a .sawError property to help the consumer decide -// based on its policies) -function FlushableStorageAppender(formatter) { - Log.StorageStreamAppender.call(this, formatter); - this.sawError = false; -} - -FlushableStorageAppender.prototype = { - __proto__: Log.StorageStreamAppender.prototype, - - append(message) { - if (message.level >= Log.Level.Error) { - this.sawError = true; - } - Log.StorageStreamAppender.prototype.append.call(this, message); - }, - - reset() { - Log.StorageStreamAppender.prototype.reset.call(this); - this.sawError = false; - }, - - // Flush the current stream to a file. Somewhat counter-intuitively, you - // must pass a log which will be written to with details of the operation. - flushToFile: Task.async(function* (subdirArray, filename, log) { - let inStream = this.getInputStream(); - this.reset(); - if (!inStream) { - log.debug("Failed to flush log to a file - no input stream"); - return; - } - log.debug("Flushing file log"); - log.trace("Beginning stream copy to " + filename + ": " + Date.now()); - try { - yield this._copyStreamToFile(inStream, subdirArray, filename, log); - log.trace("onCopyComplete", Date.now()); - } catch (ex) { - log.error("Failed to copy log stream to file", ex); - } - }), - - /** - * Copy an input stream to the named file, doing everything off the main - * thread. - * subDirArray is an array of path components, relative to the profile - * directory, where the file will be created. - * outputFileName is the filename to create. - * Returns a promise that is resolved on completion or rejected with an error. - */ - _copyStreamToFile: Task.async(function* (inputStream, subdirArray, outputFileName, log) { - // The log data could be large, so we don't want to pass it all in a single - // message, so use BUFFER_SIZE chunks. - const BUFFER_SIZE = 8192; - - // get a binary stream - let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream); - binaryStream.setInputStream(inputStream); - - let outputDirectory = OS.Path.join(OS.Constants.Path.profileDir, ...subdirArray); - yield OS.File.makeDir(outputDirectory, { ignoreExisting: true, from: OS.Constants.Path.profileDir }); - let fullOutputFileName = OS.Path.join(outputDirectory, outputFileName); - let output = yield OS.File.open(fullOutputFileName, { write: true} ); - try { - while (true) { - let available = binaryStream.available(); - if (!available) { - break; - } - let chunk = binaryStream.readByteArray(Math.min(available, BUFFER_SIZE)); - yield output.write(new Uint8Array(chunk)); - } - } finally { - try { - binaryStream.close(); // inputStream is closed by the binaryStream - yield output.close(); - } catch (ex) { - log.error("Failed to close the input stream", ex); - } - } - log.trace("finished copy to", fullOutputFileName); - }), -} - -// The public LogManager object. -function LogManager(prefRoot, logNames, logFilePrefix) { - this._prefObservers = []; - this.init(prefRoot, logNames, logFilePrefix); -} - -LogManager.prototype = { - _cleaningUpFileLogs: false, - - init(prefRoot, logNames, logFilePrefix) { - if (prefRoot instanceof Preferences) { - this._prefs = prefRoot; - } else { - this._prefs = new Preferences(prefRoot); - } - - this.logFilePrefix = logFilePrefix; - if (!formatter) { - // Create a formatter and various appenders to attach to the logs. - formatter = new Log.BasicFormatter(); - consoleAppender = new Log.ConsoleAppender(formatter); - dumpAppender = new Log.DumpAppender(formatter); - } - - allBranches.add(this._prefs._branchStr); - // We create a preference observer for all our prefs so they are magically - // reflected if the pref changes after creation. - let setupAppender = (appender, prefName, defaultLevel, findSmallest = false) => { - let observer = newVal => { - let level = Log.Level[newVal] || defaultLevel; - if (findSmallest) { - // As some of our appenders have global impact (ie, there is only one - // place 'dump' goes to), we need to find the smallest value from all - // prefs controlling this appender. - // For example, if consumerA has dump=Debug then consumerB sets - // dump=Error, we need to keep dump=Debug so consumerA is respected. - for (let branch of allBranches) { - let lookPrefBranch = new Preferences(branch); - let lookVal = Log.Level[lookPrefBranch.get(prefName)]; - if (lookVal && lookVal < level) { - level = lookVal; - } - } - } - appender.level = level; - } - this._prefs.observe(prefName, observer, this); - this._prefObservers.push([prefName, observer]); - // and call the observer now with the current pref value. - observer(this._prefs.get(prefName)); - return observer; - } - - this._observeConsolePref = setupAppender(consoleAppender, "log.appender.console", Log.Level.Fatal, true); - this._observeDumpPref = setupAppender(dumpAppender, "log.appender.dump", Log.Level.Error, true); - - // The file appender doesn't get the special singleton behaviour. - let fapp = this._fileAppender = new FlushableStorageAppender(formatter); - // the stream gets a default of Debug as the user must go out of their way - // to see the stuff spewed to it. - this._observeStreamPref = setupAppender(fapp, "log.appender.file.level", Log.Level.Debug); - - // now attach the appenders to all our logs. - for (let logName of logNames) { - let log = Log.repository.getLogger(logName); - for (let appender of [fapp, dumpAppender, consoleAppender]) { - log.addAppender(appender); - } - } - // and use the first specified log as a "root" for our log. - this._log = Log.repository.getLogger(logNames[0] + ".LogManager"); - }, - - /** - * Cleanup this instance - */ - finalize() { - for (let [name, pref] of this._prefObservers) { - this._prefs.ignore(name, pref, this); - } - this._prefObservers = []; - try { - allBranches.delete(this._prefs._branchStr); - } catch (e) {} - this._prefs = null; - }, - - get _logFileSubDirectoryEntries() { - // At this point we don't allow a custom directory for the logs, nor allow - // it to be outside the profile directory. - // This returns an array of the the relative directory entries below the - // profile dir, and is the directory about:sync-log uses. - return ["weave", "logs"]; - }, - - get sawError() { - return this._fileAppender.sawError; - }, - - // Result values for resetFileLog. - SUCCESS_LOG_WRITTEN: "success-log-written", - ERROR_LOG_WRITTEN: "error-log-written", - - /** - * Possibly generate a log file for all accumulated log messages and refresh - * the input & output streams. - * Whether a "success" or "error" log is written is determined based on - * whether an "Error" log entry was written to any of the logs. - * Returns a promise that resolves on completion with either null (for no - * file written or on error), SUCCESS_LOG_WRITTEN if a "success" log was - * written, or ERROR_LOG_WRITTEN if an "error" log was written. - */ - resetFileLog: Task.async(function* () { - try { - let flushToFile; - let reasonPrefix; - let reason; - if (this._fileAppender.sawError) { - reason = this.ERROR_LOG_WRITTEN; - flushToFile = this._prefs.get("log.appender.file.logOnError", true); - reasonPrefix = "error"; - } else { - reason = this.SUCCESS_LOG_WRITTEN; - flushToFile = this._prefs.get("log.appender.file.logOnSuccess", false); - reasonPrefix = "success"; - } - - // might as well avoid creating an input stream if we aren't going to use it. - if (!flushToFile) { - this._fileAppender.reset(); - return null; - } - - // We have reasonPrefix at the start of the filename so all "error" - // logs are grouped in about:sync-log. - let filename = reasonPrefix + "-" + this.logFilePrefix + "-" + Date.now() + ".txt"; - yield this._fileAppender.flushToFile(this._logFileSubDirectoryEntries, filename, this._log); - - // It's not completely clear to markh why we only do log cleanups - // for errors, but for now the Sync semantics have been copied... - // (one theory is that only cleaning up on error makes it less - // likely old error logs would be removed, but that's not true if - // there are occasional errors - let's address this later!) - if (reason == this.ERROR_LOG_WRITTEN && !this._cleaningUpFileLogs) { - this._log.trace("Scheduling cleanup."); - // Note we don't return/yield or otherwise wait on this promise - it - // continues in the background - this.cleanupLogs().catch(err => { - this._log.error("Failed to cleanup logs", err); - }); - } - return reason; - } catch (ex) { - this._log.error("Failed to resetFileLog", ex); - return null; - } - }), - - /** - * Finds all logs older than maxErrorAge and deletes them using async I/O. - */ - cleanupLogs: Task.async(function* () { - this._cleaningUpFileLogs = true; - let logDir = FileUtils.getDir("ProfD", this._logFileSubDirectoryEntries); - let iterator = new OS.File.DirectoryIterator(logDir.path); - let maxAge = this._prefs.get("log.appender.file.maxErrorAge", DEFAULT_MAX_ERROR_AGE); - let threshold = Date.now() - 1000 * maxAge; - - this._log.debug("Log cleanup threshold time: " + threshold); - yield iterator.forEach(Task.async(function* (entry) { - // Note that we don't check this.logFilePrefix is in the name - we cleanup - // all files in this directory regardless of that prefix so old logfiles - // for prefixes no longer in use are still cleaned up. See bug 1279145. - if (!entry.name.startsWith("error-") && - !entry.name.startsWith("success-")) { - return; - } - try { - // need to call .stat() as the enumerator doesn't give that to us on *nix. - let info = yield OS.File.stat(entry.path); - if (info.lastModificationDate.getTime() >= threshold) { - return; - } - this._log.trace(" > Cleanup removing " + entry.name + - " (" + info.lastModificationDate.getTime() + ")"); - yield OS.File.remove(entry.path); - this._log.trace("Deleted " + entry.name); - } catch (ex) { - this._log.debug("Encountered error trying to clean up old log file " - + entry.name, ex); - } - }.bind(this))); - iterator.close(); - this._cleaningUpFileLogs = false; - this._log.debug("Done deleting files."); - // This notification is used only for tests. - Services.obs.notifyObservers(null, "services-tests:common:log-manager:cleanup-logs", null); - }), -} diff --git a/components/weave/src/common/observers.js b/components/weave/src/common/observers.js deleted file mode 100644 index c0b771048..000000000 --- a/components/weave/src/common/observers.js +++ /dev/null @@ -1,150 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -this.EXPORTED_SYMBOLS = ["Observers"]; - -var Cc = Components.classes; -var Ci = Components.interfaces; -var Cr = Components.results; -var Cu = Components.utils; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -/** - * A service for adding, removing and notifying observers of notifications. - * Wraps the nsIObserverService interface. - * - * @version 0.2 - */ -this.Observers = { - /** - * Register the given callback as an observer of the given topic. - * - * @param topic {String} - * the topic to observe - * - * @param callback {Object} - * the callback; an Object that implements nsIObserver or a Function - * that gets called when the notification occurs - * - * @param thisObject {Object} [optional] - * the object to use as |this| when calling a Function callback - * - * @returns the observer - */ - add: function(topic, callback, thisObject) { - let observer = new Observer(topic, callback, thisObject); - this._cache.push(observer); - this._service.addObserver(observer, topic, true); - - return observer; - }, - - /** - * Unregister the given callback as an observer of the given topic. - * - * @param topic {String} - * the topic being observed - * - * @param callback {Object} - * the callback doing the observing - * - * @param thisObject {Object} [optional] - * the object being used as |this| when calling a Function callback - */ - remove: function(topic, callback, thisObject) { - // This seems fairly inefficient, but I'm not sure how much better - // we can make it. We could index by topic, but we can't index by callback - // or thisObject, as far as I know, since the keys to JavaScript hashes - // (a.k.a. objects) can apparently only be primitive values. - let [observer] = this._cache.filter(v => v.topic == topic && - v.callback == callback && - v.thisObject == thisObject); - if (observer) { - this._service.removeObserver(observer, topic); - this._cache.splice(this._cache.indexOf(observer), 1); - } - }, - - /** - * Notify observers about something. - * - * @param topic {String} - * the topic to notify observers about - * - * @param subject {Object} [optional] - * some information about the topic; can be any JS object or primitive - * - * @param data {String} [optional] [deprecated] - * some more information about the topic; deprecated as the subject - * is sufficient to pass all needed information to the JS observers - * that this module targets; if you have multiple values to pass to - * the observer, wrap them in an object and pass them via the subject - * parameter (i.e.: { foo: 1, bar: "some string", baz: myObject }) - */ - notify: function(topic, subject, data) { - subject = (typeof subject == "undefined") ? null : new Subject(subject); - data = (typeof data == "undefined") ? null : data; - this._service.notifyObservers(subject, topic, data); - }, - - _service: Cc["@mozilla.org/observer-service;1"]. - getService(Ci.nsIObserverService), - - /** - * A cache of observers that have been added. - * - * We use this to remove observers when a caller calls |remove|. - * - * XXX This might result in reference cycles, causing memory leaks, - * if we hold a reference to an observer that holds a reference to us. - * Could we fix that by making this an independent top-level object - * rather than a property of this object? - */ - _cache: [] -}; - - -function Observer(topic, callback, thisObject) { - this.topic = topic; - this.callback = callback; - this.thisObject = thisObject; -} - -Observer.prototype = { - QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]), - observe: function(subject, topic, data) { - // Extract the wrapped object for subjects that are one of our wrappers - // around a JS object. This way we support both wrapped subjects created - // using this module and those that are real XPCOM components. - if (subject && typeof subject == "object" && - ("wrappedJSObject" in subject) && - ("observersModuleSubjectWrapper" in subject.wrappedJSObject)) - subject = subject.wrappedJSObject.object; - - if (typeof this.callback == "function") { - if (this.thisObject) - this.callback.call(this.thisObject, subject, data); - else - this.callback(subject, data); - } - else // typeof this.callback == "object" (nsIObserver) - this.callback.observe(subject, topic, data); - } -} - - -function Subject(object) { - // Double-wrap the object and set a property identifying the wrappedJSObject - // as one of our wrappers to distinguish between subjects that are one of our - // wrappers (which we should unwrap when notifying our observers) and those - // that are real JS XPCOM components (which we should pass through unaltered). - this.wrappedJSObject = { observersModuleSubjectWrapper: true, object: object }; -} - -Subject.prototype = { - QueryInterface: XPCOMUtils.generateQI([]), - getScriptableHelper: function() {}, - getInterfaces: function() {} -}; diff --git a/components/weave/src/common/rest.js b/components/weave/src/common/rest.js deleted file mode 100644 index 22b2ebbba..000000000 --- a/components/weave/src/common/rest.js +++ /dev/null @@ -1,764 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -this.EXPORTED_SYMBOLS = [ - "RESTRequest", - "RESTResponse", - "TokenAuthenticatedRESTRequest", -]; - -Cu.import("resource://gre/modules/Preferences.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/NetUtil.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://gre/CommonUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils", - "resource://services-crypto/utils.js"); - -const Prefs = new Preferences("services.common."); - -/** - * Single use HTTP requests to RESTish resources. - * - * @param uri - * URI for the request. This can be an nsIURI object or a string - * that can be used to create one. An exception will be thrown if - * the string is not a valid URI. - * - * Examples: - * - * (1) Quick GET request: - * - * new RESTRequest("http://server/rest/resource").get(function (error) { - * if (error) { - * // Deal with a network error. - * processNetworkErrorCode(error.result); - * return; - * } - * if (!this.response.success) { - * // Bail out if we're not getting an HTTP 2xx code. - * processHTTPError(this.response.status); - * return; - * } - * processData(this.response.body); - * }); - * - * (2) Quick PUT request (non-string data is automatically JSONified) - * - * new RESTRequest("http://server/rest/resource").put(data, function (error) { - * ... - * }); - * - * (3) Streaming GET - * - * let request = new RESTRequest("http://server/rest/resource"); - * request.setHeader("Accept", "application/newlines"); - * request.onComplete = function (error) { - * if (error) { - * // Deal with a network error. - * processNetworkErrorCode(error.result); - * return; - * } - * callbackAfterRequestHasCompleted() - * }); - * request.onProgress = function () { - * if (!this.response.success) { - * // Bail out if we're not getting an HTTP 2xx code. - * return; - * } - * // Process body data and reset it so we don't process the same data twice. - * processIncrementalData(this.response.body); - * this.response.body = ""; - * }); - * request.get(); - */ -this.RESTRequest = function RESTRequest(uri) { - this.status = this.NOT_SENT; - - // If we don't have an nsIURI object yet, make one. This will throw if - // 'uri' isn't a valid URI string. - if (!(uri instanceof Ci.nsIURI)) { - uri = Services.io.newURI(uri, null, null); - } - this.uri = uri; - - this._headers = {}; - this._log = Log.repository.getLogger(this._logName); - this._log.level = - Log.Level[Prefs.get("log.logger.rest.request")]; -} -RESTRequest.prototype = { - - _logName: "Services.Common.RESTRequest", - - QueryInterface: XPCOMUtils.generateQI([ - Ci.nsIBadCertListener2, - Ci.nsIInterfaceRequestor, - Ci.nsIChannelEventSink - ]), - - /*** Public API: ***/ - - /** - * A constant boolean that indicates whether this object will automatically - * utf-8 encode request bodies passed as an object. Used for feature detection - * so, eg, loop can use the same source code for old and new Firefox versions. - */ - willUTF8EncodeObjectRequests: true, - - /** - * URI for the request (an nsIURI object). - */ - uri: null, - - /** - * HTTP method (e.g. "GET") - */ - method: null, - - /** - * RESTResponse object - */ - response: null, - - /** - * nsIRequest load flags. Don't do any caching by default. Don't send user - * cookies and such over the wire (Bug 644734). - */ - loadFlags: Ci.nsIRequest.LOAD_BYPASS_CACHE | Ci.nsIRequest.INHIBIT_CACHING | Ci.nsIRequest.LOAD_ANONYMOUS, - - /** - * nsIHttpChannel - */ - channel: null, - - /** - * Flag to indicate the status of the request. - * - * One of NOT_SENT, SENT, IN_PROGRESS, COMPLETED, ABORTED. - */ - status: null, - - NOT_SENT: 0, - SENT: 1, - IN_PROGRESS: 2, - COMPLETED: 4, - ABORTED: 8, - - /** - * HTTP status text of response - */ - statusText: null, - - /** - * Request timeout (in seconds, though decimal values can be used for - * up to millisecond granularity.) - * - * 0 for no timeout. - */ - timeout: null, - - /** - * The encoding with which the response to this request must be treated. - * If a charset parameter is available in the HTTP Content-Type header for - * this response, that will always be used, and this value is ignored. We - * default to UTF-8 because that is a reasonable default. - */ - charset: "utf-8", - - /** - * Called when the request has been completed, including failures and - * timeouts. - * - * @param error - * Error that occurred while making the request, null if there - * was no error. - */ - onComplete: function onComplete(error) { - }, - - /** - * Called whenever data is being received on the channel. If this throws an - * exception, the request is aborted and the exception is passed as the - * error to onComplete(). - */ - onProgress: function onProgress() { - }, - - /** - * Set a request header. - */ - setHeader: function setHeader(name, value) { - this._headers[name.toLowerCase()] = value; - }, - - /** - * Perform an HTTP GET. - * - * @param onComplete - * Short-circuit way to set the 'onComplete' method. Optional. - * @param onProgress - * Short-circuit way to set the 'onProgress' method. Optional. - * - * @return the request object. - */ - get: function get(onComplete, onProgress) { - return this.dispatch("GET", null, onComplete, onProgress); - }, - - /** - * Perform an HTTP PATCH. - * - * @param data - * Data to be used as the request body. If this isn't a string - * it will be JSONified automatically. - * @param onComplete - * Short-circuit way to set the 'onComplete' method. Optional. - * @param onProgress - * Short-circuit way to set the 'onProgress' method. Optional. - * - * @return the request object. - */ - patch: function patch(data, onComplete, onProgress) { - return this.dispatch("PATCH", data, onComplete, onProgress); - }, - - /** - * Perform an HTTP PUT. - * - * @param data - * Data to be used as the request body. If this isn't a string - * it will be JSONified automatically. - * @param onComplete - * Short-circuit way to set the 'onComplete' method. Optional. - * @param onProgress - * Short-circuit way to set the 'onProgress' method. Optional. - * - * @return the request object. - */ - put: function put(data, onComplete, onProgress) { - return this.dispatch("PUT", data, onComplete, onProgress); - }, - - /** - * Perform an HTTP POST. - * - * @param data - * Data to be used as the request body. If this isn't a string - * it will be JSONified automatically. - * @param onComplete - * Short-circuit way to set the 'onComplete' method. Optional. - * @param onProgress - * Short-circuit way to set the 'onProgress' method. Optional. - * - * @return the request object. - */ - post: function post(data, onComplete, onProgress) { - return this.dispatch("POST", data, onComplete, onProgress); - }, - - /** - * Perform an HTTP DELETE. - * - * @param onComplete - * Short-circuit way to set the 'onComplete' method. Optional. - * @param onProgress - * Short-circuit way to set the 'onProgress' method. Optional. - * - * @return the request object. - */ - delete: function delete_(onComplete, onProgress) { - return this.dispatch("DELETE", null, onComplete, onProgress); - }, - - /** - * Abort an active request. - */ - abort: function abort() { - if (this.status != this.SENT && this.status != this.IN_PROGRESS) { - throw "Can only abort a request that has been sent."; - } - - this.status = this.ABORTED; - this.channel.cancel(Cr.NS_BINDING_ABORTED); - - if (this.timeoutTimer) { - // Clear the abort timer now that the channel is done. - this.timeoutTimer.clear(); - } - }, - - /*** Implementation stuff ***/ - - dispatch: function dispatch(method, data, onComplete, onProgress) { - if (this.status != this.NOT_SENT) { - throw "Request has already been sent!"; - } - - this.method = method; - if (onComplete) { - this.onComplete = onComplete; - } - if (onProgress) { - this.onProgress = onProgress; - } - - // Create and initialize HTTP channel. - let channel = NetUtil.newChannel({uri: this.uri, loadUsingSystemPrincipal: true}) - .QueryInterface(Ci.nsIRequest) - .QueryInterface(Ci.nsIHttpChannel); - this.channel = channel; - channel.loadFlags |= this.loadFlags; - channel.notificationCallbacks = this; - - this._log.debug(`${method} request to ${this.uri.spec}`); - // Set request headers. - let headers = this._headers; - for (let key in headers) { - if (key == 'authorization') { - this._log.trace("HTTP Header " + key + ": ***** (suppressed)"); - } else { - this._log.trace("HTTP Header " + key + ": " + headers[key]); - } - channel.setRequestHeader(key, headers[key], false); - } - - // Set HTTP request body. - if (method == "PUT" || method == "POST" || method == "PATCH") { - // Convert non-string bodies into JSON with utf-8 encoding. If a string - // is passed we assume they've already encoded it. - let contentType = headers["content-type"]; - if (typeof data != "string") { - data = JSON.stringify(data); - if (!contentType) { - contentType = "application/json"; - } - if (!contentType.includes("charset")) { - data = CommonUtils.encodeUTF8(data); - contentType += "; charset=utf-8"; - } else { - // If someone handed us an object but also a custom content-type - // it's probably confused. We could go to even further lengths to - // respect it, but this shouldn't happen in practice. - Cu.reportError("rest.js found an object to JSON.stringify but also a " + - "content-type header with a charset specification. " + - "This probably isn't going to do what you expect"); - } - } - if (!contentType) { - contentType = "text/plain"; - } - - this._log.debug(method + " Length: " + data.length); - if (this._log.level <= Log.Level.Trace) { - this._log.trace(method + " Body: " + data); - } - - let stream = Cc["@mozilla.org/io/string-input-stream;1"] - .createInstance(Ci.nsIStringInputStream); - stream.setData(data, data.length); - - channel.QueryInterface(Ci.nsIUploadChannel); - channel.setUploadStream(stream, contentType, data.length); - } - // We must set this after setting the upload stream, otherwise it - // will always be 'PUT'. Yeah, I know. - channel.requestMethod = method; - - // Before opening the channel, set the charset that serves as a hint - // as to what the response might be encoded as. - channel.contentCharset = this.charset; - - // Blast off! - try { - channel.asyncOpen2(this); - } catch (ex) { - // asyncOpen can throw in a bunch of cases -- e.g., a forbidden port. - this._log.warn("Caught an error in asyncOpen", ex); - CommonUtils.nextTick(onComplete.bind(this, ex)); - } - this.status = this.SENT; - this.delayTimeout(); - return this; - }, - - /** - * Create or push back the abort timer that kills this request. - */ - delayTimeout: function delayTimeout() { - if (this.timeout) { - CommonUtils.namedTimer(this.abortTimeout, this.timeout * 1000, this, - "timeoutTimer"); - } - }, - - /** - * Abort the request based on a timeout. - */ - abortTimeout: function abortTimeout() { - this.abort(); - let error = Components.Exception("Aborting due to channel inactivity.", - Cr.NS_ERROR_NET_TIMEOUT); - if (!this.onComplete) { - this._log.error("Unexpected error: onComplete not defined in " + - "abortTimeout."); - return; - } - this.onComplete(error); - }, - - /*** nsIStreamListener ***/ - - onStartRequest: function onStartRequest(channel) { - if (this.status == this.ABORTED) { - this._log.trace("Not proceeding with onStartRequest, request was aborted."); - return; - } - - try { - channel.QueryInterface(Ci.nsIHttpChannel); - } catch (ex) { - this._log.error("Unexpected error: channel is not a nsIHttpChannel!"); - this.status = this.ABORTED; - channel.cancel(Cr.NS_BINDING_ABORTED); - return; - } - - this.status = this.IN_PROGRESS; - - this._log.trace("onStartRequest: " + channel.requestMethod + " " + - channel.URI.spec); - - // Create a response object and fill it with some data. - let response = this.response = new RESTResponse(); - response.request = this; - response.body = ""; - - this.delayTimeout(); - }, - - onStopRequest: function onStopRequest(channel, context, statusCode) { - if (this.timeoutTimer) { - // Clear the abort timer now that the channel is done. - this.timeoutTimer.clear(); - } - - // We don't want to do anything for a request that's already been aborted. - if (this.status == this.ABORTED) { - this._log.trace("Not proceeding with onStopRequest, request was aborted."); - return; - } - - try { - channel.QueryInterface(Ci.nsIHttpChannel); - } catch (ex) { - this._log.error("Unexpected error: channel not nsIHttpChannel!"); - this.status = this.ABORTED; - return; - } - this.status = this.COMPLETED; - - let statusSuccess = Components.isSuccessCode(statusCode); - let uri = channel && channel.URI && channel.URI.spec || "<unknown>"; - this._log.trace("Channel for " + channel.requestMethod + " " + uri + - " returned status code " + statusCode); - - if (!this.onComplete) { - this._log.error("Unexpected error: onComplete not defined in " + - "abortRequest."); - this.onProgress = null; - return; - } - - // Throw the failure code and stop execution. Use Components.Exception() - // instead of Error() so the exception is QI-able and can be passed across - // XPCOM borders while preserving the status code. - if (!statusSuccess) { - let message = Components.Exception("", statusCode).name; - let error = Components.Exception(message, statusCode); - this._log.debug(this.method + " " + uri + " failed: " + statusCode + " - " + message); - this.onComplete(error); - this.onComplete = this.onProgress = null; - return; - } - - this._log.debug(this.method + " " + uri + " " + this.response.status); - - // Additionally give the full response body when Trace logging. - if (this._log.level <= Log.Level.Trace) { - this._log.trace(this.method + " body: " + this.response.body); - } - - delete this._inputStream; - - this.onComplete(null); - this.onComplete = this.onProgress = null; - }, - - onDataAvailable: function onDataAvailable(channel, cb, stream, off, count) { - // We get an nsIRequest, which doesn't have contentCharset. - try { - channel.QueryInterface(Ci.nsIHttpChannel); - } catch (ex) { - this._log.error("Unexpected error: channel not nsIHttpChannel!"); - this.abort(); - - if (this.onComplete) { - this.onComplete(ex); - } - - this.onComplete = this.onProgress = null; - return; - } - - if (channel.contentCharset) { - this.response.charset = channel.contentCharset; - - if (!this._converterStream) { - this._converterStream = Cc["@mozilla.org/intl/converter-input-stream;1"] - .createInstance(Ci.nsIConverterInputStream); - } - - this._converterStream.init(stream, channel.contentCharset, 0, - this._converterStream.DEFAULT_REPLACEMENT_CHARACTER); - - try { - let str = {}; - let num = this._converterStream.readString(count, str); - if (num != 0) { - this.response.body += str.value; - } - } catch (ex) { - this._log.warn("Exception thrown reading " + count + " bytes from " + - "the channel", ex); - throw ex; - } - } else { - this.response.charset = null; - - if (!this._inputStream) { - this._inputStream = Cc["@mozilla.org/scriptableinputstream;1"] - .createInstance(Ci.nsIScriptableInputStream); - } - - this._inputStream.init(stream); - - this.response.body += this._inputStream.read(count); - } - - try { - this.onProgress(); - } catch (ex) { - this._log.warn("Got exception calling onProgress handler, aborting " + - this.method + " " + channel.URI.spec, ex); - this.abort(); - - if (!this.onComplete) { - this._log.error("Unexpected error: onComplete not defined in " + - "onDataAvailable."); - this.onProgress = null; - return; - } - - this.onComplete(ex); - this.onComplete = this.onProgress = null; - return; - } - - this.delayTimeout(); - }, - - /*** nsIInterfaceRequestor ***/ - - getInterface: function(aIID) { - return this.QueryInterface(aIID); - }, - - /*** nsIBadCertListener2 ***/ - - notifyCertProblem: function notifyCertProblem(socketInfo, sslStatus, targetHost) { - this._log.warn("Invalid HTTPS certificate encountered!"); - // Suppress invalid HTTPS certificate warnings in the UI. - // (The request will still fail.) - return true; - }, - - /** - * Returns true if headers from the old channel should be - * copied to the new channel. Invoked when a channel redirect - * is in progress. - */ - shouldCopyOnRedirect: function shouldCopyOnRedirect(oldChannel, newChannel, flags) { - let isInternal = !!(flags & Ci.nsIChannelEventSink.REDIRECT_INTERNAL); - let isSameURI = newChannel.URI.equals(oldChannel.URI); - this._log.debug("Channel redirect: " + oldChannel.URI.spec + ", " + - newChannel.URI.spec + ", internal = " + isInternal); - return isInternal && isSameURI; - }, - - /*** nsIChannelEventSink ***/ - asyncOnChannelRedirect: - function asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) { - - let oldSpec = (oldChannel && oldChannel.URI) ? oldChannel.URI.spec : "<undefined>"; - let newSpec = (newChannel && newChannel.URI) ? newChannel.URI.spec : "<undefined>"; - this._log.debug("Channel redirect: " + oldSpec + ", " + newSpec + ", " + flags); - - try { - newChannel.QueryInterface(Ci.nsIHttpChannel); - } catch (ex) { - this._log.error("Unexpected error: channel not nsIHttpChannel!"); - callback.onRedirectVerifyCallback(Cr.NS_ERROR_NO_INTERFACE); - return; - } - - // For internal redirects, copy the headers that our caller set. - try { - if (this.shouldCopyOnRedirect(oldChannel, newChannel, flags)) { - this._log.trace("Copying headers for safe internal redirect."); - for (let key in this._headers) { - newChannel.setRequestHeader(key, this._headers[key], false); - } - } - } catch (ex) { - this._log.error("Error copying headers", ex); - } - - this.channel = newChannel; - - // We let all redirects proceed. - callback.onRedirectVerifyCallback(Cr.NS_OK); - } -}; - -/** - * Response object for a RESTRequest. This will be created automatically by - * the RESTRequest. - */ -this.RESTResponse = function RESTResponse() { - this._log = Log.repository.getLogger(this._logName); - this._log.level = - Log.Level[Prefs.get("log.logger.rest.response")]; -} -RESTResponse.prototype = { - - _logName: "Services.Common.RESTResponse", - - /** - * Corresponding REST request - */ - request: null, - - /** - * HTTP status code - */ - get status() { - let status; - try { - status = this.request.channel.responseStatus; - } catch (ex) { - this._log.debug("Caught exception fetching HTTP status code", ex); - return null; - } - Object.defineProperty(this, "status", {value: status}); - return status; - }, - - /** - * HTTP status text - */ - get statusText() { - let statusText; - try { - statusText = this.request.channel.responseStatusText; - } catch (ex) { - this._log.debug("Caught exception fetching HTTP status text", ex); - return null; - } - Object.defineProperty(this, "statusText", {value: statusText}); - return statusText; - }, - - /** - * Boolean flag that indicates whether the HTTP status code is 2xx or not. - */ - get success() { - let success; - try { - success = this.request.channel.requestSucceeded; - } catch (ex) { - this._log.debug("Caught exception fetching HTTP success flag", ex); - return null; - } - Object.defineProperty(this, "success", {value: success}); - return success; - }, - - /** - * Object containing HTTP headers (keyed as lower case) - */ - get headers() { - let headers = {}; - try { - this._log.trace("Processing response headers."); - let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel); - channel.visitResponseHeaders(function (header, value) { - headers[header.toLowerCase()] = value; - }); - } catch (ex) { - this._log.debug("Caught exception processing response headers", ex); - return null; - } - - Object.defineProperty(this, "headers", {value: headers}); - return headers; - }, - - /** - * HTTP body (string) - */ - body: null - -}; - -/** - * Single use MAC authenticated HTTP requests to RESTish resources. - * - * @param uri - * URI going to the RESTRequest constructor. - * @param authToken - * (Object) An auth token of the form {id: (string), key: (string)} - * from which the MAC Authentication header for this request will be - * derived. A token as obtained from - * TokenServerClient.getTokenFromBrowserIDAssertion is accepted. - * @param extra - * (Object) Optional extra parameters. Valid keys are: nonce_bytes, ts, - * nonce, and ext. See CrytoUtils.computeHTTPMACSHA1 for information on - * the purpose of these values. - */ -this.TokenAuthenticatedRESTRequest = - function TokenAuthenticatedRESTRequest(uri, authToken, extra) { - RESTRequest.call(this, uri); - this.authToken = authToken; - this.extra = extra || {}; -} -TokenAuthenticatedRESTRequest.prototype = { - __proto__: RESTRequest.prototype, - - dispatch: function dispatch(method, data, onComplete, onProgress) { - let sig = CryptoUtils.computeHTTPMACSHA1( - this.authToken.id, this.authToken.key, method, this.uri, this.extra - ); - - this.setHeader("Authorization", sig.getHeader()); - - return RESTRequest.prototype.dispatch.call( - this, method, data, onComplete, onProgress - ); - }, -}; diff --git a/components/weave/src/common/services-common.js b/components/weave/src/common/services-common.js deleted file mode 100644 index bc37d4028..000000000 --- a/components/weave/src/common/services-common.js +++ /dev/null @@ -1,11 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -// This file contains default preference values for components in -// services-common. - -pref("services.common.log.logger.rest.request", "Debug"); -pref("services.common.log.logger.rest.response", "Debug"); - -pref("services.common.log.logger.tokenserverclient", "Debug"); diff --git a/components/weave/src/common/stringbundle.js b/components/weave/src/common/stringbundle.js deleted file mode 100644 index a07fa4831..000000000 --- a/components/weave/src/common/stringbundle.js +++ /dev/null @@ -1,203 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -this.EXPORTED_SYMBOLS = ["StringBundle"]; - -var {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; - -/** - * A string bundle. - * - * This object presents two APIs: a deprecated one that is equivalent to the API - * for the stringbundle XBL binding, to make it easy to switch from that binding - * to this module, and a new one that is simpler and easier to use. - * - * The benefit of this module over the XBL binding is that it can also be used - * in JavaScript modules and components, not only in chrome JS. - * - * To use this module, import it, create a new instance of StringBundle, - * and then use the instance's |get| and |getAll| methods to retrieve strings - * (you can get both plain and formatted strings with |get|): - * - * let strings = - * new StringBundle("chrome://example/locale/strings.properties"); - * let foo = strings.get("foo"); - * let barFormatted = strings.get("bar", [arg1, arg2]); - * for (let string of strings.getAll()) - * dump (string.key + " = " + string.value + "\n"); - * - * @param url {String} - * the URL of the string bundle - */ -this.StringBundle = function StringBundle(url) { - this.url = url; -} - -StringBundle.prototype = { - /** - * the locale associated with the application - * @type nsILocale - * @private - */ - get _appLocale() { - try { - return Cc["@mozilla.org/intl/nslocaleservice;1"]. - getService(Ci.nsILocaleService). - getApplicationLocale(); - } - catch(ex) { - return null; - } - }, - - /** - * the wrapped nsIStringBundle - * @type nsIStringBundle - * @private - */ - get _stringBundle() { - let stringBundle = Cc["@mozilla.org/intl/stringbundle;1"]. - getService(Ci.nsIStringBundleService). - createBundle(this.url, this._appLocale); - this.__defineGetter__("_stringBundle", () => stringBundle); - return this._stringBundle; - }, - - - // the new API - - /** - * the URL of the string bundle - * @type String - */ - _url: null, - get url() { - return this._url; - }, - set url(newVal) { - this._url = newVal; - delete this._stringBundle; - }, - - /** - * Get a string from the bundle. - * - * @param key {String} - * the identifier of the string to get - * @param args {array} [optional] - * an array of arguments that replace occurrences of %S in the string - * - * @returns {String} the value of the string - */ - get: function(key, args) { - if (args) - return this.stringBundle.formatStringFromName(key, args, args.length); - else - return this.stringBundle.GetStringFromName(key); - }, - - /** - * Get all the strings in the bundle. - * - * @returns {Array} - * an array of objects with key and value properties - */ - getAll: function() { - let strings = []; - - // FIXME: for performance, return an enumerable array that wraps the string - // bundle's nsISimpleEnumerator (does JavaScript already support this?). - - let enumerator = this.stringBundle.getSimpleEnumeration(); - - while (enumerator.hasMoreElements()) { - // We could simply return the nsIPropertyElement objects, but I think - // it's better to return standard JS objects that behave as consumers - // expect JS objects to behave (f.e. you can modify them dynamically). - let string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement); - strings.push({ key: string.key, value: string.value }); - } - - return strings; - }, - - - // the deprecated XBL binding-compatible API - - /** - * the URL of the string bundle - * @deprecated because its name doesn't make sense outside of an XBL binding - * @type String - */ - get src() { - return this.url; - }, - set src(newVal) { - this.url = newVal; - }, - - /** - * the locale associated with the application - * @deprecated because it has never been used outside the XBL binding itself, - * and consumers should obtain it directly from the locale service anyway. - * @type nsILocale - */ - get appLocale() { - return this._appLocale; - }, - - /** - * the wrapped nsIStringBundle - * @deprecated because this module should provide all necessary functionality - * @type nsIStringBundle - * - * If you do ever need to use this, let the authors of this module know why - * so they can surface functionality for your use case in the module itself - * and you don't have to access this underlying XPCOM component. - */ - get stringBundle() { - return this._stringBundle; - }, - - /** - * Get a string from the bundle. - * @deprecated use |get| instead - * - * @param key {String} - * the identifier of the string to get - * - * @returns {String} - * the value of the string - */ - getString: function(key) { - return this.get(key); - }, - - /** - * Get a formatted string from the bundle. - * @deprecated use |get| instead - * - * @param key {string} - * the identifier of the string to get - * @param args {array} - * an array of arguments that replace occurrences of %S in the string - * - * @returns {String} - * the formatted value of the string - */ - getFormattedString: function(key, args) { - return this.get(key, args); - }, - - /** - * Get an enumeration of the strings in the bundle. - * @deprecated use |getAll| instead - * - * @returns {nsISimpleEnumerator} - * a enumeration of the strings in the bundle - */ - get strings() { - return this.stringBundle.getSimpleEnumeration(); - } -} diff --git a/components/weave/src/common/tokenserverclient.js b/components/weave/src/common/tokenserverclient.js deleted file mode 100644 index ca40f7d93..000000000 --- a/components/weave/src/common/tokenserverclient.js +++ /dev/null @@ -1,459 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = [ - "TokenServerClient", - "TokenServerClientError", - "TokenServerClientNetworkError", - "TokenServerClientServerError", -]; - -var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/Log.jsm"); -Cu.import("resource://services-common/rest.js"); -Cu.import("resource://services-common/observers.js"); - -const PREF_LOG_LEVEL = "services.common.log.logger.tokenserverclient"; - -/** - * Represents a TokenServerClient error that occurred on the client. - * - * This is the base type for all errors raised by client operations. - * - * @param message - * (string) Error message. - */ -this.TokenServerClientError = function TokenServerClientError(message) { - this.name = "TokenServerClientError"; - this.message = message || "Client error."; - // Without explicitly setting .stack, all stacks from these errors will point - // to the "new Error()" call a few lines down, which isn't helpful. - this.stack = Error().stack; -} -TokenServerClientError.prototype = new Error(); -TokenServerClientError.prototype.constructor = TokenServerClientError; -TokenServerClientError.prototype._toStringFields = function() { - return {message: this.message}; -} -TokenServerClientError.prototype.toString = function() { - return this.name + "(" + JSON.stringify(this._toStringFields()) + ")"; -} -TokenServerClientError.prototype.toJSON = function() { - let result = this._toStringFields(); - result["name"] = this.name; - return result; -} - -/** - * Represents a TokenServerClient error that occurred in the network layer. - * - * @param error - * The underlying error thrown by the network layer. - */ -this.TokenServerClientNetworkError = - function TokenServerClientNetworkError(error) { - this.name = "TokenServerClientNetworkError"; - this.error = error; - this.stack = Error().stack; -} -TokenServerClientNetworkError.prototype = new TokenServerClientError(); -TokenServerClientNetworkError.prototype.constructor = - TokenServerClientNetworkError; -TokenServerClientNetworkError.prototype._toStringFields = function() { - return {error: this.error}; -} - -/** - * Represents a TokenServerClient error that occurred on the server. - * - * This type will be encountered for all non-200 response codes from the - * server. The type of error is strongly enumerated and is stored in the - * `cause` property. This property can have the following string values: - * - * conditions-required -- The server is requesting that the client - * agree to service conditions before it can obtain a token. The - * conditions that must be presented to the user and agreed to are in - * the `urls` mapping on the instance. Keys of this mapping are - * identifiers. Values are string URLs. - * - * invalid-credentials -- A token could not be obtained because - * the credentials presented by the client were invalid. - * - * unknown-service -- The requested service was not found. - * - * malformed-request -- The server rejected the request because it - * was invalid. If you see this, code in this file is likely wrong. - * - * malformed-response -- The response from the server was not what was - * expected. - * - * general -- A general server error has occurred. Clients should - * interpret this as an opaque failure. - * - * @param message - * (string) Error message. - */ -this.TokenServerClientServerError = - function TokenServerClientServerError(message, cause="general") { - this.now = new Date().toISOString(); // may be useful to diagnose time-skew issues. - this.name = "TokenServerClientServerError"; - this.message = message || "Server error."; - this.cause = cause; - this.stack = Error().stack; -} -TokenServerClientServerError.prototype = new TokenServerClientError(); -TokenServerClientServerError.prototype.constructor = - TokenServerClientServerError; - -TokenServerClientServerError.prototype._toStringFields = function() { - let fields = { - now: this.now, - message: this.message, - cause: this.cause, - }; - if (this.response) { - fields.response_body = this.response.body; - fields.response_headers = this.response.headers; - fields.response_status = this.response.status; - } - return fields; -}; - -/** - * Represents a client to the Token Server. - * - * http://docs.services.mozilla.com/token/index.html - * - * The Token Server supports obtaining tokens for arbitrary apps by - * constructing URI paths of the form <app>/<app_version>. However, the service - * discovery mechanism emphasizes the use of full URIs and tries to not force - * the client to manipulate URIs. This client currently enforces this practice - * by not implementing an API which would perform URI manipulation. - * - * If you are tempted to implement this API in the future, consider this your - * warning that you may be doing it wrong and that you should store full URIs - * instead. - * - * Areas to Improve: - * - * - The server sends a JSON response on error. The client does not currently - * parse this. It might be convenient if it did. - * - Currently most non-200 status codes are rolled into one error type. It - * might be helpful if callers had a richer API that communicated who was - * at fault (e.g. differentiating a 503 from a 401). - */ -this.TokenServerClient = function TokenServerClient() { - this._log = Log.repository.getLogger("Common.TokenServerClient"); - let level = Services.prefs.getCharPref(PREF_LOG_LEVEL, "Debug"); - this._log.level = Log.Level[level]; -} -TokenServerClient.prototype = { - /** - * Logger instance. - */ - _log: null, - - /** - * Obtain a token from a BrowserID assertion against a specific URL. - * - * This asynchronously obtains the token. The callback receives 2 arguments: - * - * (TokenServerClientError | null) If no token could be obtained, this - * will be a TokenServerClientError instance describing why. The - * type seen defines the type of error encountered. If an HTTP response - * was seen, a RESTResponse instance will be stored in the `response` - * property of this object. If there was no error and a token is - * available, this will be null. - * - * (map | null) On success, this will be a map containing the results from - * the server. If there was an error, this will be null. The map has the - * following properties: - * - * id (string) HTTP MAC public key identifier. - * key (string) HTTP MAC shared symmetric key. - * endpoint (string) URL where service can be connected to. - * uid (string) user ID for requested service. - * duration (string) the validity duration of the issued token. - * - * Terms of Service Acceptance - * --------------------------- - * - * Some services require users to accept terms of service before they can - * obtain a token. If a service requires ToS acceptance, the error passed - * to the callback will be a `TokenServerClientServerError` with the - * `cause` property set to "conditions-required". The `urls` property of that - * instance will be a map of string keys to string URL values. The user-agent - * should prompt the user to accept the content at these URLs. - * - * Clients signify acceptance of the terms of service by sending a token - * request with additional metadata. This is controlled by the - * `conditionsAccepted` argument to this function. Clients only need to set - * this flag once per service and the server remembers acceptance. If - * the conditions for the service change, the server may request - * clients agree to terms again. Therefore, clients should always be - * prepared to handle a conditions required response. - * - * Clients should not blindly send acceptance to conditions. Instead, clients - * should set `conditionsAccepted` if and only if the server asks for - * acceptance, the conditions are displayed to the user, and the user agrees - * to them. - * - * Example Usage - * ------------- - * - * let client = new TokenServerClient(); - * let assertion = getBrowserIDAssertionFromSomewhere(); - * let url = "https://token.services.mozilla.com/1.0/sync/2.0"; - * - * client.getTokenFromBrowserIDAssertion(url, assertion, - * function onResponse(error, result) { - * if (error) { - * if (error.cause == "conditions-required") { - * promptConditionsAcceptance(error.urls, function onAccept() { - * client.getTokenFromBrowserIDAssertion(url, assertion, - * onResponse, true); - * } - * return; - * } - * - * // Do other error handling. - * return; - * } - * - * let { - * id: id, key: key, uid: uid, endpoint: endpoint, duration: duration - * } = result; - * // Do stuff with data and carry on. - * }); - * - * @param url - * (string) URL to fetch token from. - * @param assertion - * (string) BrowserID assertion to exchange token for. - * @param cb - * (function) Callback to be invoked with result of operation. - * @param conditionsAccepted - * (bool) Whether to send acceptance to service conditions. - */ - getTokenFromBrowserIDAssertion: - function getTokenFromBrowserIDAssertion(url, assertion, cb, addHeaders={}) { - if (!url) { - throw new TokenServerClientError("url argument is not valid."); - } - - if (!assertion) { - throw new TokenServerClientError("assertion argument is not valid."); - } - - if (!cb) { - throw new TokenServerClientError("cb argument is not valid."); - } - - this._log.debug("Beginning BID assertion exchange: " + url); - - let req = this.newRESTRequest(url); - req.setHeader("Accept", "application/json"); - req.setHeader("Authorization", "BrowserID " + assertion); - - for (let header in addHeaders) { - req.setHeader(header, addHeaders[header]); - } - - let client = this; - req.get(function onResponse(error) { - if (error) { - cb(new TokenServerClientNetworkError(error), null); - return; - } - - let self = this; - function callCallback(error, result) { - if (!cb) { - self._log.warn("Callback already called! Did it throw?"); - return; - } - - try { - cb(error, result); - } catch (ex) { - self._log.warn("Exception when calling user-supplied callback", ex); - } - - cb = null; - } - - try { - client._processTokenResponse(this.response, callCallback); - } catch (ex) { - this._log.warn("Error processing token server response", ex); - - let error = new TokenServerClientError(ex); - error.response = this.response; - callCallback(error, null); - } - }); - }, - - /** - * Handler to process token request responses. - * - * @param response - * RESTResponse from token HTTP request. - * @param cb - * The original callback passed to the public API. - */ - _processTokenResponse: function processTokenResponse(response, cb) { - this._log.debug("Got token response: " + response.status); - - // Responses should *always* be JSON, even in the case of 4xx and 5xx - // errors. If we don't see JSON, the server is likely very unhappy. - let ct = response.headers["content-type"] || ""; - if (ct != "application/json" && !ct.startsWith("application/json;")) { - this._log.warn("Did not receive JSON response. Misconfigured server?"); - this._log.debug("Content-Type: " + ct); - this._log.debug("Body: " + response.body); - - let error = new TokenServerClientServerError("Non-JSON response.", - "malformed-response"); - error.response = response; - cb(error, null); - return; - } - - let result; - try { - result = JSON.parse(response.body); - } catch (ex) { - this._log.warn("Invalid JSON returned by server: " + response.body); - let error = new TokenServerClientServerError("Malformed JSON.", - "malformed-response"); - error.response = response; - cb(error, null); - return; - } - - // Any response status can have X-Backoff or X-Weave-Backoff headers. - this._maybeNotifyBackoff(response, "x-weave-backoff"); - this._maybeNotifyBackoff(response, "x-backoff"); - - // The service shouldn't have any 3xx, so we don't need to handle those. - if (response.status != 200) { - // We /should/ have a Cornice error report in the JSON. We log that to - // help with debugging. - if ("errors" in result) { - // This could throw, but this entire function is wrapped in a try. If - // the server is sending something not an array of objects, it has - // failed to keep its contract with us and there is little we can do. - for (let error of result.errors) { - this._log.info("Server-reported error: " + JSON.stringify(error)); - } - } - - let error = new TokenServerClientServerError(); - error.response = response; - - if (response.status == 400) { - error.message = "Malformed request."; - error.cause = "malformed-request"; - } else if (response.status == 401) { - // Cause can be invalid-credentials, invalid-timestamp, or - // invalid-generation. - error.message = "Authentication failed."; - error.cause = result.status; - } - - // 403 should represent a "condition acceptance needed" response. - // - // The extra validation of "urls" is important. We don't want to signal - // conditions required unless we are absolutely sure that is what the - // server is asking for. - else if (response.status == 403) { - if (!("urls" in result)) { - this._log.warn("403 response without proper fields!"); - this._log.warn("Response body: " + response.body); - - error.message = "Missing JSON fields."; - error.cause = "malformed-response"; - } else if (typeof(result.urls) != "object") { - error.message = "urls field is not a map."; - error.cause = "malformed-response"; - } else { - error.message = "Conditions must be accepted."; - error.cause = "conditions-required"; - error.urls = result.urls; - } - } else if (response.status == 404) { - error.message = "Unknown service."; - error.cause = "unknown-service"; - } - - // A Retry-After header should theoretically only appear on a 503, but - // we'll look for it on any error response. - this._maybeNotifyBackoff(response, "retry-after"); - - cb(error, null); - return; - } - - for (let k of ["id", "key", "api_endpoint", "uid", "duration"]) { - if (!(k in result)) { - let error = new TokenServerClientServerError("Expected key not " + - " present in result: " + - k); - error.cause = "malformed-response"; - error.response = response; - cb(error, null); - return; - } - } - - this._log.debug("Successful token response"); - cb(null, { - id: result.id, - key: result.key, - endpoint: result.api_endpoint, - uid: result.uid, - duration: result.duration, - hashed_fxa_uid: result.hashed_fxa_uid, - }); - }, - - /* - * The prefix used for all notifications sent by this module. This - * allows the handler of notifications to be sure they are handling - * notifications for the service they expect. - * - * If not set, no notifications will be sent. - */ - observerPrefix: null, - - // Given an optional header value, notify that a backoff has been requested. - _maybeNotifyBackoff: function (response, headerName) { - if (!this.observerPrefix) { - return; - } - let headerVal = response.headers[headerName]; - if (!headerVal) { - return; - } - let backoffInterval; - try { - backoffInterval = parseInt(headerVal, 10); - } catch (ex) { - this._log.error("TokenServer response had invalid backoff value in '" + - headerName + "' header: " + headerVal); - return; - } - Observers.notify(this.observerPrefix + ":backoff:interval", backoffInterval); - }, - - // override points for testing. - newRESTRequest: function(url) { - return new RESTRequest(url); - } -}; |