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