Bug 1025824 - Fix mHwcLayerMap handling. r=sushil, a=2.0+
[gecko.git] / services / common / hawkclient.js
blob3d751ba97ae9a2b9f0b830316298e0775dfc8b65
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 "use strict";
7 /*
8  * HAWK is an HTTP authentication scheme using a message authentication code
9  * (MAC) algorithm to provide partial HTTP request cryptographic verification.
10  *
11  * For details, see: https://github.com/hueniverse/hawk
12  *
13  * With HAWK, it is essential that the clocks on clients and server not have an
14  * absolute delta of greater than one minute, as the HAWK protocol uses
15  * timestamps to reduce the possibility of replay attacks.  However, it is
16  * likely that some clients' clocks will be more than a little off, especially
17  * in mobile devices, which would break HAWK-based services (like sync and
18  * firefox accounts) for those clients.
19  *
20  * This library provides a stateful HAWK client that calculates (roughly) the
21  * clock delta on the client vs the server.  The library provides an interface
22  * for deriving HAWK credentials and making HAWK-authenticated REST requests to
23  * a single remote server.  Therefore, callers who want to interact with
24  * multiple HAWK services should instantiate one HawkClient per service.
25  */
27 this.EXPORTED_SYMBOLS = ["HawkClient"];
29 const {interfaces: Ci, utils: Cu} = Components;
31 Cu.import("resource://services-common/utils.js");
32 Cu.import("resource://services-crypto/utils.js");
33 Cu.import("resource://services-common/hawkrequest.js");
34 Cu.import("resource://services-common/observers.js");
35 Cu.import("resource://gre/modules/Promise.jsm");
36 Cu.import("resource://gre/modules/Log.jsm");
37 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
38 Cu.import("resource://gre/modules/Services.jsm");
40 // loglevel should be one of "Fatal", "Error", "Warn", "Info", "Config",
41 // "Debug", "Trace" or "All". If none is specified, "Error" will be used by
42 // default.
43 const PREF_LOG_LEVEL = "services.hawk.loglevel";
45 // A pref that can be set so "sensitive" information (eg, personally
46 // identifiable info, credentials, etc) will be logged.
47 const PREF_LOG_SENSITIVE_DETAILS = "services.hawk.log.sensitive";
49 XPCOMUtils.defineLazyGetter(this, "log", function() {
50   let log = Log.repository.getLogger("Hawk");
51   log.addAppender(new Log.DumpAppender());
52   log.level = Log.Level.Error;
53   try {
54     let level =
55       Services.prefs.getPrefType(PREF_LOG_LEVEL) == Ci.nsIPrefBranch.PREF_STRING
56       && Services.prefs.getCharPref(PREF_LOG_LEVEL);
57     log.level = Log.Level[level] || Log.Level.Error;
58   } catch (e) {
59     log.error(e);
60   }
62   return log;
63 });
65 // A boolean to indicate if personally identifiable information (or anything
66 // else sensitive, such as credentials) should be logged.
67 XPCOMUtils.defineLazyGetter(this, 'logPII', function() {
68   try {
69     return Services.prefs.getBoolPref(PREF_LOG_SENSITIVE_DETAILS);
70   } catch (_) {
71     return false;
72   }
73 });
76  * A general purpose client for making HAWK authenticated requests to a single
77  * host.  Keeps track of the clock offset between the client and the host for
78  * computation of the timestamp in the HAWK Authorization header.
79  *
80  * Clients should create one HawkClient object per each server they wish to
81  * interact with.
82  *
83  * @param host
84  *        The url of the host
85  */
86 this.HawkClient = function(host) {
87   this.host = host;
89   // Clock offset in milliseconds between our client's clock and the date
90   // reported in responses from our host.
91   this._localtimeOffsetMsec = 0;
94 this.HawkClient.prototype = {
96   /*
97    * Construct an error message for a response.  Private.
98    *
99    * @param restResponse
100    *        A RESTResponse object from a RESTRequest
101    *
102    * @param errorString
103    *        A string describing the error
104    */
105   _constructError: function(restResponse, errorString) {
106     let errorObj = {
107       error: errorString,
108       message: restResponse.statusText,
109       code: restResponse.status,
110       errno: restResponse.status
111     };
112     let retryAfter = restResponse.headers && restResponse.headers["retry-after"];
113     retryAfter = retryAfter ? parseInt(retryAfter) : retryAfter;
114     if (retryAfter) {
115       errorObj.retryAfter = retryAfter;
116       // and notify observers of the retry interval
117       if (this.observerPrefix) {
118         Observers.notify(this.observerPrefix + ":backoff:interval", retryAfter);
119       }
120     }
121     return errorObj;
122   },
124   /*
125    *
126    * Update clock offset by determining difference from date gives in the (RFC
127    * 1123) Date header of a server response.  Because HAWK tolerates a window
128    * of one minute of clock skew (so two minutes total since the skew can be
129    * positive or negative), the simple method of calculating offset here is
130    * probably good enough.  We keep the value in milliseconds to make life
131    * easier, even though the value will not have millisecond accuracy.
132    *
133    * @param dateString
134    *        An RFC 1123 date string (e.g., "Mon, 13 Jan 2014 21:45:06 GMT")
135    *
136    * For HAWK clock skew and replay protection, see
137    * https://github.com/hueniverse/hawk#replay-protection
138    */
139   _updateClockOffset: function(dateString) {
140     try {
141       let serverDateMsec = Date.parse(dateString);
142       this._localtimeOffsetMsec = serverDateMsec - this.now();
143       log.debug("Clock offset vs " + this.host + ": " + this._localtimeOffsetMsec);
144     } catch(err) {
145       log.warn("Bad date header in server response: " + dateString);
146     }
147   },
149   /*
150    * Get the current clock offset in milliseconds.
151    *
152    * The offset is the number of milliseconds that must be added to the client
153    * clock to make it equal to the server clock.  For example, if the client is
154    * five minutes ahead of the server, the localtimeOffsetMsec will be -300000.
155    */
156   get localtimeOffsetMsec() {
157     return this._localtimeOffsetMsec;
158   },
160   /*
161    * return current time in milliseconds
162    */
163   now: function() {
164     return Date.now();
165   },
167   /* A general method for sending raw RESTRequest calls authorized using HAWK
168    *
169    * @param path
170    *        API endpoint path
171    * @param method
172    *        The HTTP request method
173    * @param credentials
174    *        Hawk credentials
175    * @param payloadObj
176    *        An object that can be encodable as JSON as the payload of the
177    *        request
178    * @return Promise
179    *        Returns a promise that resolves to the text response of the API call,
180    *        or is rejected with an error.  If the server response can be parsed
181    *        as JSON and contains an 'error' property, the promise will be
182    *        rejected with this JSON-parsed response.
183    */
184   request: function(path, method, credentials=null, payloadObj={}, retryOK=true) {
185     method = method.toLowerCase();
187     let deferred = Promise.defer();
188     let uri = this.host + path;
189     let self = this;
191     function _onComplete(error) {
192       let restResponse = this.response;
193       let status = restResponse.status;
195       log.debug("(Response) " + path + ": code: " + status +
196                 " - Status text: " + restResponse.statusText);
197       if (logPII) {
198         log.debug("Response text: " + restResponse.body);
199       }
201       // All responses may have backoff headers, which are a server-side safety
202       // valve to allow slowing down clients without hurting performance.
203       self._maybeNotifyBackoff(restResponse, "x-weave-backoff");
204       self._maybeNotifyBackoff(restResponse, "x-backoff");
206       if (error) {
207         // When things really blow up, reconstruct an error object that follows
208         // the general format of the server on error responses.
209         return deferred.reject(self._constructError(restResponse, error));
210       }
212       self._updateClockOffset(restResponse.headers["date"]);
214       if (status === 401 && retryOK && !("retry-after" in restResponse.headers)) {
215         // Retry once if we were rejected due to a bad timestamp.
216         // Clock offset is adjusted already in the top of this function.
217         log.debug("Received 401 for " + path + ": retrying");
218         return deferred.resolve(
219             self.request(path, method, credentials, payloadObj, false));
220       }
222       // If the server returned a json error message, use it in the rejection
223       // of the promise.
224       //
225       // In the case of a 401, in which we are probably being rejected for a
226       // bad timestamp, retry exactly once, during which time clock offset will
227       // be adjusted.
229       let jsonResponse = {};
230       try {
231         jsonResponse = JSON.parse(restResponse.body);
232       } catch(notJSON) {}
234       let okResponse = (200 <= status && status < 300);
235       if (!okResponse || jsonResponse.error) {
236         if (jsonResponse.error) {
237           return deferred.reject(jsonResponse);
238         }
239         return deferred.reject(self._constructError(restResponse, "Request failed"));
240       }
241       // It's up to the caller to know how to decode the response.
242       // We just return the raw text.
243       deferred.resolve(this.response.body);
244     };
246     function onComplete(error) {
247       try {
248         // |this| is the RESTRequest object and we need to ensure _onComplete
249         // gets the same one.
250         _onComplete.call(this, error);
251       } catch (ex) {
252         log.error("Unhandled exception processing response:" +
253                   CommonUtils.exceptionStr(ex));
254         deferred.reject(ex);
255       }
256     }
258     let extra = {
259       now: this.now(),
260       localtimeOffsetMsec: this.localtimeOffsetMsec,
261     };
263     let request = this.newHAWKAuthenticatedRESTRequest(uri, credentials, extra);
264     if (method == "post" || method == "put") {
265       request[method](payloadObj, onComplete);
266     } else {
267       request[method](onComplete);
268     }
270     return deferred.promise;
271   },
273   /*
274    * The prefix used for all notifications sent by this module.  This
275    * allows the handler of notifications to be sure they are handling
276    * notifications for the service they expect.
277    *
278    * If not set, no notifications will be sent.
279    */
280   observerPrefix: null,
282   // Given an optional header value, notify that a backoff has been requested.
283   _maybeNotifyBackoff: function (response, headerName) {
284     if (!this.observerPrefix || !response.headers) {
285       return;
286     }
287     let headerVal = response.headers[headerName];
288     if (!headerVal) {
289       return;
290     }
291     let backoffInterval;
292     try {
293       backoffInterval = parseInt(headerVal, 10);
294     } catch (ex) {
295       log.error("hawkclient response had invalid backoff value in '" +
296                 headerName + "' header: " + headerVal);
297       return;
298     }
299     Observers.notify(this.observerPrefix + ":backoff:interval", backoffInterval);
300   },
302   // override points for testing.
303   newHAWKAuthenticatedRESTRequest: function(uri, credentials, extra) {
304     return new HAWKAuthenticatedRESTRequest(uri, credentials, extra);
305   },