summaryrefslogtreecommitdiff
path: root/components/weave/src/common
diff options
context:
space:
mode:
Diffstat (limited to 'components/weave/src/common')
-rw-r--r--components/weave/src/common/hawkclient.js346
-rw-r--r--components/weave/src/common/hawkrequest.js198
-rw-r--r--components/weave/src/common/logmanager.js331
-rw-r--r--components/weave/src/common/observers.js150
-rw-r--r--components/weave/src/common/rest.js764
-rw-r--r--components/weave/src/common/services-common.js11
-rw-r--r--components/weave/src/common/stringbundle.js203
-rw-r--r--components/weave/src/common/tokenserverclient.js459
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);
- }
-};