summaryrefslogtreecommitdiff
path: root/components/weave/src/common/tokenserverclient.js
diff options
context:
space:
mode:
Diffstat (limited to 'components/weave/src/common/tokenserverclient.js')
-rw-r--r--components/weave/src/common/tokenserverclient.js459
1 files changed, 0 insertions, 459 deletions
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);
- }
-};