Bug 1852754: part 9) Add tests for dynamically loading <link rel="prefetch"> elements...
[gecko.git] / services / common / hawkrequest.sys.mjs
bloba856ef032d27b52399b9b33f45e82d07d8db7aa4
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 file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 import { Log } from "resource://gre/modules/Log.sys.mjs";
7 import { RESTRequest } from "resource://services-common/rest.sys.mjs";
8 import { CommonUtils } from "resource://services-common/utils.sys.mjs";
9 import { Credentials } from "resource://gre/modules/Credentials.sys.mjs";
11 const lazy = {};
13 ChromeUtils.defineESModuleGetters(lazy, {
14   CryptoUtils: "resource://services-crypto/utils.sys.mjs",
15 });
17 /**
18  * Single-use HAWK-authenticated HTTP requests to RESTish resources.
19  *
20  * @param uri
21  *        (String) URI for the RESTRequest constructor
22  *
23  * @param credentials
24  *        (Object) Optional credentials for computing HAWK authentication
25  *        header.
26  *
27  * @param payloadObj
28  *        (Object) Optional object to be converted to JSON payload
29  *
30  * @param extra
31  *        (Object) Optional extra params for HAWK header computation.
32  *        Valid properties are:
33  *
34  *          now:                 <current time in milliseconds>,
35  *          localtimeOffsetMsec: <local clock offset vs server>,
36  *          headers:             <An object with header/value pairs to be sent
37  *                                as headers on the request>
38  *
39  * extra.localtimeOffsetMsec is the value in milliseconds that must be added to
40  * the local clock to make it agree with the server's clock.  For instance, if
41  * the local clock is two minutes ahead of the server, the time offset in
42  * milliseconds will be -120000.
43  */
45 export var HAWKAuthenticatedRESTRequest = function HawkAuthenticatedRESTRequest(
46   uri,
47   credentials,
48   extra = {}
49 ) {
50   RESTRequest.call(this, uri);
52   this.credentials = credentials;
53   this.now = extra.now || Date.now();
54   this.localtimeOffsetMsec = extra.localtimeOffsetMsec || 0;
55   this._log.trace(
56     "local time, offset: " + this.now + ", " + this.localtimeOffsetMsec
57   );
58   this.extraHeaders = extra.headers || {};
60   // Expose for testing
61   this._intl = getIntl();
64 HAWKAuthenticatedRESTRequest.prototype = {
65   async dispatch(method, data) {
66     let contentType = "text/plain";
67     if (method == "POST" || method == "PUT" || method == "PATCH") {
68       contentType = "application/json";
69     }
70     if (this.credentials) {
71       let options = {
72         now: this.now,
73         localtimeOffsetMsec: this.localtimeOffsetMsec,
74         credentials: this.credentials,
75         payload: (data && JSON.stringify(data)) || "",
76         contentType,
77       };
78       let header = await lazy.CryptoUtils.computeHAWK(
79         this.uri,
80         method,
81         options
82       );
83       this.setHeader("Authorization", header.field);
84     }
86     for (let header in this.extraHeaders) {
87       this.setHeader(header, this.extraHeaders[header]);
88     }
90     this.setHeader("Content-Type", contentType);
92     this.setHeader("Accept-Language", this._intl.accept_languages);
94     return super.dispatch(method, data);
95   },
98 Object.setPrototypeOf(
99   HAWKAuthenticatedRESTRequest.prototype,
100   RESTRequest.prototype
104  * Generic function to derive Hawk credentials.
106  * Hawk credentials are derived using shared secrets, which depend on the token
107  * in use.
109  * @param tokenHex
110  *        The current session token encoded in hex
111  * @param context
112  *        A context for the credentials. A protocol version will be prepended
113  *        to the context, see Credentials.keyWord for more information.
114  * @param size
115  *        The size in bytes of the expected derived buffer,
116  *        defaults to 3 * 32.
117  * @return credentials
118  *        Returns an object:
119  *        {
120  *          id: the Hawk id (from the first 32 bytes derived)
121  *          key: the Hawk key (from bytes 32 to 64)
122  *          extra: size - 64 extra bytes (if size > 64)
123  *        }
124  */
125 export async function deriveHawkCredentials(tokenHex, context, size = 96) {
126   let token = CommonUtils.hexToBytes(tokenHex);
127   let out = await lazy.CryptoUtils.hkdfLegacy(
128     token,
129     undefined,
130     Credentials.keyWord(context),
131     size
132   );
134   let result = {
135     key: out.slice(32, 64),
136     id: CommonUtils.bytesAsHex(out.slice(0, 32)),
137   };
138   if (size > 64) {
139     result.extra = out.slice(64);
140   }
142   return result;
145 // With hawk request, we send the user's accepted-languages with each request.
146 // To keep the number of times we read this pref at a minimum, maintain the
147 // preference in a stateful object that notices and updates itself when the
148 // pref is changed.
149 function Intl() {
150   // We won't actually query the pref until the first time we need it
151   this._accepted = "";
152   this._everRead = false;
153   this.init();
156 Intl.prototype = {
157   init() {
158     Services.prefs.addObserver("intl.accept_languages", this);
159   },
161   uninit() {
162     Services.prefs.removeObserver("intl.accept_languages", this);
163   },
165   observe(subject, topic, data) {
166     this.readPref();
167   },
169   readPref() {
170     this._everRead = true;
171     try {
172       this._accepted = Services.prefs.getComplexValue(
173         "intl.accept_languages",
174         Ci.nsIPrefLocalizedString
175       ).data;
176     } catch (err) {
177       let log = Log.repository.getLogger("Services.Common.RESTRequest");
178       log.error("Error reading intl.accept_languages pref", err);
179     }
180   },
182   get accept_languages() {
183     if (!this._everRead) {
184       this.readPref();
185     }
186     return this._accepted;
187   },
190 // Singleton getter for Intl, creating an instance only when we first need it.
191 var intl = null;
192 function getIntl() {
193   if (!intl) {
194     intl = new Intl();
195   }
196   return intl;