tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

FxAccountsProfileClient.sys.mjs (8109B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /**
      6 * A client to fetch profile information for a Firefox Account.
      7 */
      8 
      9 import {
     10  ERRNO_NETWORK,
     11  ERRNO_PARSE,
     12  ERRNO_UNKNOWN_ERROR,
     13  ERROR_CODE_METHOD_NOT_ALLOWED,
     14  ERROR_MSG_METHOD_NOT_ALLOWED,
     15  ERROR_NETWORK,
     16  ERROR_PARSE,
     17  ERROR_UNKNOWN,
     18  log,
     19  SCOPE_PROFILE,
     20  SCOPE_PROFILE_WRITE,
     21 } from "resource://gre/modules/FxAccountsCommon.sys.mjs";
     22 
     23 import { getFxAccountsSingleton } from "resource://gre/modules/FxAccounts.sys.mjs";
     24 
     25 const fxAccounts = getFxAccountsSingleton();
     26 import { RESTRequest } from "resource://services-common/rest.sys.mjs";
     27 
     28 /**
     29 * Create a new FxAccountsProfileClient to be able to fetch Firefox Account profile information.
     30 *
     31 * @param {object} options Options
     32 *   @param {string} options.serverURL
     33 *   The URL of the profile server to query.
     34 *   Example: https://profile.accounts.firefox.com/v1
     35 * @class
     36 */
     37 export var FxAccountsProfileClient = function (options) {
     38  if (!options?.serverURL) {
     39    throw new Error("Missing 'serverURL' configuration option");
     40  }
     41 
     42  this.fxai = options.fxai || fxAccounts._internal;
     43 
     44  this.serverURL = URL.parse(options.serverURL);
     45  if (!this.serverURL) {
     46    throw new Error("Invalid 'serverURL'");
     47  }
     48  log.debug("FxAccountsProfileClient: Initialized");
     49 };
     50 
     51 FxAccountsProfileClient.prototype = {
     52  /**
     53   * {URL}
     54   * The server to fetch profile information from.
     55   */
     56  serverURL: null,
     57 
     58  /**
     59   * Interface for making remote requests.
     60   */
     61  _Request: RESTRequest,
     62 
     63  /**
     64   * Remote request helper which abstracts authentication away.
     65   *
     66   * @param {string} path
     67   *        Profile server path, i.e "/profile".
     68   * @param {string} [method]
     69   *        Type of request, e.g. "GET".
     70   * @param {string} [etag]
     71   *        Optional ETag used for caching purposes.
     72   * @param {object} [body]
     73   *        Optional request body, to be sent as application/json.
     74   * @return Promise
     75   *         Resolves: {body: Object, etag: Object} Successful response from the Profile server.
     76   *         Rejects: {FxAccountsProfileClientError} Profile client error.
     77   * @private
     78   */
     79  async _createRequest(path, method = "GET", etag = null, body = null) {
     80    method = method.toUpperCase();
     81    let token = await this._getTokenForRequest(method);
     82    try {
     83      return await this._rawRequest(path, method, token, etag, body);
     84    } catch (ex) {
     85      if (!(ex instanceof FxAccountsProfileClientError) || ex.code != 401) {
     86        throw ex;
     87      }
     88      // it's an auth error - assume our token expired and retry.
     89      log.info(
     90        "Fetching the profile returned a 401 - revoking our token and retrying"
     91      );
     92      await this.fxai.removeCachedOAuthToken({ token });
     93      token = await this._getTokenForRequest(method);
     94      // and try with the new token - if that also fails then we fail after
     95      // revoking the token.
     96      try {
     97        return await this._rawRequest(path, method, token, etag, body);
     98      } catch (ex) {
     99        if (!(ex instanceof FxAccountsProfileClientError) || ex.code != 401) {
    100          throw ex;
    101        }
    102        log.info(
    103          "Retry fetching the profile still returned a 401 - revoking our token and failing"
    104        );
    105        await this.fxai.removeCachedOAuthToken({ token });
    106        throw ex;
    107      }
    108    }
    109  },
    110 
    111  /**
    112   * Helper to get an OAuth token for a request.
    113   *
    114   * OAuth tokens are cached, so it's fine to call this for each request.
    115   *
    116   * @param {string} [method]
    117   *        Type of request, i.e "GET".
    118   * @return Promise
    119   *         Resolves: Object containing "scope", "token" and "key" properties
    120   *         Rejects: {FxAccountsProfileClientError} Profile client error.
    121   * @private
    122   */
    123  async _getTokenForRequest(method) {
    124    let scope = SCOPE_PROFILE;
    125    if (method === "POST") {
    126      scope = SCOPE_PROFILE_WRITE;
    127    }
    128    return this.fxai.getOAuthToken({ scope });
    129  },
    130 
    131  /**
    132   * Remote "raw" request helper - doesn't handle auth errors and tokens.
    133   *
    134   * @param {string} path
    135   *        Profile server path, i.e "/profile".
    136   * @param {string} method
    137   *        Type of request, i.e "GET".
    138   * @param {string} token
    139   * @param {string} etag
    140   * @param {object} payload
    141   *        The payload of the request, if any.
    142   * @return Promise
    143   *         Resolves: {body: Object, etag: Object} Successful response from the Profile server
    144                        or null if 304 is hit (same ETag).
    145   *         Rejects: {FxAccountsProfileClientError} Profile client error.
    146   * @private
    147   */
    148  async _rawRequest(path, method, token, etag = null, payload = null) {
    149    let profileDataUrl = this.serverURL + path;
    150    let request = new this._Request(profileDataUrl);
    151 
    152    request.setHeader("Authorization", "Bearer " + token);
    153    request.setHeader("Accept", "application/json");
    154    if (etag) {
    155      request.setHeader("If-None-Match", etag);
    156    }
    157 
    158    if (method != "GET" && method != "POST") {
    159      // method not supported
    160      throw new FxAccountsProfileClientError({
    161        error: ERROR_NETWORK,
    162        errno: ERRNO_NETWORK,
    163        code: ERROR_CODE_METHOD_NOT_ALLOWED,
    164        message: ERROR_MSG_METHOD_NOT_ALLOWED,
    165      });
    166    }
    167    try {
    168      await request.dispatch(method, payload);
    169    } catch (error) {
    170      throw new FxAccountsProfileClientError({
    171        error: ERROR_NETWORK,
    172        errno: ERRNO_NETWORK,
    173        message: error.toString(),
    174      });
    175    }
    176 
    177    let body = null;
    178    try {
    179      if (request.response.status == 304) {
    180        return null;
    181      }
    182      body = JSON.parse(request.response.body);
    183    } catch (e) {
    184      throw new FxAccountsProfileClientError({
    185        error: ERROR_PARSE,
    186        errno: ERRNO_PARSE,
    187        code: request.response.status,
    188        message: request.response.body,
    189      });
    190    }
    191 
    192    // "response.success" means status code is 200
    193    if (!request.response.success) {
    194      throw new FxAccountsProfileClientError({
    195        error: body.error || ERROR_UNKNOWN,
    196        errno: body.errno || ERRNO_UNKNOWN_ERROR,
    197        code: request.response.status,
    198        message: body.message || body,
    199      });
    200    }
    201    return {
    202      body,
    203      etag: request.response.headers.etag,
    204    };
    205  },
    206 
    207  /**
    208   * Retrieve user's profile from the server
    209   *
    210   * @param {string} [etag]
    211   *        Optional ETag used for caching purposes. (may generate a 304 exception)
    212   * @return Promise
    213   *         Resolves: {body: Object, etag: Object} Successful response from the '/profile' endpoint.
    214   *         Rejects: {FxAccountsProfileClientError} profile client error.
    215   */
    216  fetchProfile(etag) {
    217    log.debug("FxAccountsProfileClient: Requested profile");
    218    return this._createRequest("/profile", "GET", etag);
    219  },
    220 };
    221 
    222 /**
    223 * Normalized profile client errors
    224 *
    225 * @param {object} [details]
    226 *        Error details object
    227 *   @param {number} [details.code]
    228 *          Error code
    229 *   @param {number} [details.errno]
    230 *          Error number
    231 *   @param {string} [details.error]
    232 *          Error description
    233 *   @param {string | null} [details.message]
    234 *          Error message
    235 * @class
    236 */
    237 export var FxAccountsProfileClientError = function (details) {
    238  details = details || {};
    239 
    240  this.name = "FxAccountsProfileClientError";
    241  this.code = details.code || null;
    242  this.errno = details.errno || ERRNO_UNKNOWN_ERROR;
    243  this.error = details.error || ERROR_UNKNOWN;
    244  this.message = details.message || null;
    245 };
    246 
    247 /**
    248 * Returns error object properties
    249 *
    250 * @returns {{name: *, code: *, errno: *, error: *, message: *}}
    251 * @private
    252 */
    253 FxAccountsProfileClientError.prototype._toStringFields = function () {
    254  return {
    255    name: this.name,
    256    code: this.code,
    257    errno: this.errno,
    258    error: this.error,
    259    message: this.message,
    260  };
    261 };
    262 
    263 /**
    264 * String representation of a profile client error
    265 *
    266 * @returns {string}
    267 */
    268 FxAccountsProfileClientError.prototype.toString = function () {
    269  return this.name + "(" + JSON.stringify(this._toStringFields()) + ")";
    270 };