diff options
Diffstat (limited to 'components/weave/src/common/tokenserverclient.js')
-rw-r--r-- | components/weave/src/common/tokenserverclient.js | 459 |
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); - } -}; |