1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/services/common/hawkrequest.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,145 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; 1.11 + 1.12 +this.EXPORTED_SYMBOLS = [ 1.13 + "HAWKAuthenticatedRESTRequest", 1.14 +]; 1.15 + 1.16 +Cu.import("resource://gre/modules/Preferences.jsm"); 1.17 +Cu.import("resource://gre/modules/Services.jsm"); 1.18 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.19 +Cu.import("resource://gre/modules/Log.jsm"); 1.20 +Cu.import("resource://services-common/rest.js"); 1.21 + 1.22 +XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils", 1.23 + "resource://services-crypto/utils.js"); 1.24 + 1.25 +const Prefs = new Preferences("services.common.rest."); 1.26 + 1.27 +/** 1.28 + * Single-use HAWK-authenticated HTTP requests to RESTish resources. 1.29 + * 1.30 + * @param uri 1.31 + * (String) URI for the RESTRequest constructor 1.32 + * 1.33 + * @param credentials 1.34 + * (Object) Optional credentials for computing HAWK authentication 1.35 + * header. 1.36 + * 1.37 + * @param payloadObj 1.38 + * (Object) Optional object to be converted to JSON payload 1.39 + * 1.40 + * @param extra 1.41 + * (Object) Optional extra params for HAWK header computation. 1.42 + * Valid properties are: 1.43 + * 1.44 + * now: <current time in milliseconds>, 1.45 + * localtimeOffsetMsec: <local clock offset vs server> 1.46 + * 1.47 + * extra.localtimeOffsetMsec is the value in milliseconds that must be added to 1.48 + * the local clock to make it agree with the server's clock. For instance, if 1.49 + * the local clock is two minutes ahead of the server, the time offset in 1.50 + * milliseconds will be -120000. 1.51 + */ 1.52 + 1.53 +this.HAWKAuthenticatedRESTRequest = 1.54 + function HawkAuthenticatedRESTRequest(uri, credentials, extra={}) { 1.55 + RESTRequest.call(this, uri); 1.56 + 1.57 + this.credentials = credentials; 1.58 + this.now = extra.now || Date.now(); 1.59 + this.localtimeOffsetMsec = extra.localtimeOffsetMsec || 0; 1.60 + this._log.trace("local time, offset: " + this.now + ", " + (this.localtimeOffsetMsec)); 1.61 + 1.62 + // Expose for testing 1.63 + this._intl = getIntl(); 1.64 +}; 1.65 +HAWKAuthenticatedRESTRequest.prototype = { 1.66 + __proto__: RESTRequest.prototype, 1.67 + 1.68 + dispatch: function dispatch(method, data, onComplete, onProgress) { 1.69 + let contentType = "text/plain"; 1.70 + if (method == "POST" || method == "PUT") { 1.71 + contentType = "application/json"; 1.72 + } 1.73 + if (this.credentials) { 1.74 + let options = { 1.75 + now: this.now, 1.76 + localtimeOffsetMsec: this.localtimeOffsetMsec, 1.77 + credentials: this.credentials, 1.78 + payload: data && JSON.stringify(data) || "", 1.79 + contentType: contentType, 1.80 + }; 1.81 + let header = CryptoUtils.computeHAWK(this.uri, method, options); 1.82 + this.setHeader("Authorization", header.field); 1.83 + this._log.trace("hawk auth header: " + header.field); 1.84 + } 1.85 + 1.86 + this.setHeader("Content-Type", contentType); 1.87 + 1.88 + this.setHeader("Accept-Language", this._intl.accept_languages); 1.89 + 1.90 + return RESTRequest.prototype.dispatch.call( 1.91 + this, method, data, onComplete, onProgress 1.92 + ); 1.93 + } 1.94 +}; 1.95 + 1.96 +// With hawk request, we send the user's accepted-languages with each request. 1.97 +// To keep the number of times we read this pref at a minimum, maintain the 1.98 +// preference in a stateful object that notices and updates itself when the 1.99 +// pref is changed. 1.100 +this.Intl = function Intl() { 1.101 + // We won't actually query the pref until the first time we need it 1.102 + this._accepted = ""; 1.103 + this._everRead = false; 1.104 + this._log = Log.repository.getLogger("Services.common.RESTRequest"); 1.105 + this._log.level = Log.Level[Prefs.get("log.logger.rest.request")]; 1.106 + this.init(); 1.107 +}; 1.108 + 1.109 +this.Intl.prototype = { 1.110 + init: function() { 1.111 + Services.prefs.addObserver("intl.accept_languages", this, false); 1.112 + }, 1.113 + 1.114 + uninit: function() { 1.115 + Services.prefs.removeObserver("intl.accept_languages", this); 1.116 + }, 1.117 + 1.118 + observe: function(subject, topic, data) { 1.119 + this.readPref(); 1.120 + }, 1.121 + 1.122 + readPref: function() { 1.123 + this._everRead = true; 1.124 + try { 1.125 + this._accepted = Services.prefs.getComplexValue( 1.126 + "intl.accept_languages", Ci.nsIPrefLocalizedString).data; 1.127 + } catch (err) { 1.128 + this._log.error("Error reading intl.accept_languages pref: " + CommonUtils.exceptionStr(err)); 1.129 + } 1.130 + }, 1.131 + 1.132 + get accept_languages() { 1.133 + if (!this._everRead) { 1.134 + this.readPref(); 1.135 + } 1.136 + return this._accepted; 1.137 + }, 1.138 +}; 1.139 + 1.140 +// Singleton getter for Intl, creating an instance only when we first need it. 1.141 +let intl = null; 1.142 +function getIntl() { 1.143 + if (!intl) { 1.144 + intl = new Intl(); 1.145 + } 1.146 + return intl; 1.147 +} 1.148 +