michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; michael@0: michael@0: this.EXPORTED_SYMBOLS = [ michael@0: "HAWKAuthenticatedRESTRequest", michael@0: ]; michael@0: michael@0: Cu.import("resource://gre/modules/Preferences.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Log.jsm"); michael@0: Cu.import("resource://services-common/rest.js"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils", michael@0: "resource://services-crypto/utils.js"); michael@0: michael@0: const Prefs = new Preferences("services.common.rest."); michael@0: michael@0: /** michael@0: * Single-use HAWK-authenticated HTTP requests to RESTish resources. michael@0: * michael@0: * @param uri michael@0: * (String) URI for the RESTRequest constructor michael@0: * michael@0: * @param credentials michael@0: * (Object) Optional credentials for computing HAWK authentication michael@0: * header. michael@0: * michael@0: * @param payloadObj michael@0: * (Object) Optional object to be converted to JSON payload michael@0: * michael@0: * @param extra michael@0: * (Object) Optional extra params for HAWK header computation. michael@0: * Valid properties are: michael@0: * michael@0: * now: , michael@0: * localtimeOffsetMsec: michael@0: * michael@0: * extra.localtimeOffsetMsec is the value in milliseconds that must be added to michael@0: * the local clock to make it agree with the server's clock. For instance, if michael@0: * the local clock is two minutes ahead of the server, the time offset in michael@0: * milliseconds will be -120000. michael@0: */ michael@0: michael@0: this.HAWKAuthenticatedRESTRequest = michael@0: function HawkAuthenticatedRESTRequest(uri, credentials, extra={}) { michael@0: RESTRequest.call(this, uri); michael@0: michael@0: this.credentials = credentials; michael@0: this.now = extra.now || Date.now(); michael@0: this.localtimeOffsetMsec = extra.localtimeOffsetMsec || 0; michael@0: this._log.trace("local time, offset: " + this.now + ", " + (this.localtimeOffsetMsec)); michael@0: michael@0: // Expose for testing michael@0: this._intl = getIntl(); michael@0: }; michael@0: HAWKAuthenticatedRESTRequest.prototype = { michael@0: __proto__: RESTRequest.prototype, michael@0: michael@0: dispatch: function dispatch(method, data, onComplete, onProgress) { michael@0: let contentType = "text/plain"; michael@0: if (method == "POST" || method == "PUT") { michael@0: contentType = "application/json"; michael@0: } michael@0: if (this.credentials) { michael@0: let options = { michael@0: now: this.now, michael@0: localtimeOffsetMsec: this.localtimeOffsetMsec, michael@0: credentials: this.credentials, michael@0: payload: data && JSON.stringify(data) || "", michael@0: contentType: contentType, michael@0: }; michael@0: let header = CryptoUtils.computeHAWK(this.uri, method, options); michael@0: this.setHeader("Authorization", header.field); michael@0: this._log.trace("hawk auth header: " + header.field); michael@0: } michael@0: michael@0: this.setHeader("Content-Type", contentType); michael@0: michael@0: this.setHeader("Accept-Language", this._intl.accept_languages); michael@0: michael@0: return RESTRequest.prototype.dispatch.call( michael@0: this, method, data, onComplete, onProgress michael@0: ); michael@0: } michael@0: }; michael@0: michael@0: // With hawk request, we send the user's accepted-languages with each request. michael@0: // To keep the number of times we read this pref at a minimum, maintain the michael@0: // preference in a stateful object that notices and updates itself when the michael@0: // pref is changed. michael@0: this.Intl = function Intl() { michael@0: // We won't actually query the pref until the first time we need it michael@0: this._accepted = ""; michael@0: this._everRead = false; michael@0: this._log = Log.repository.getLogger("Services.common.RESTRequest"); michael@0: this._log.level = Log.Level[Prefs.get("log.logger.rest.request")]; michael@0: this.init(); michael@0: }; michael@0: michael@0: this.Intl.prototype = { michael@0: init: function() { michael@0: Services.prefs.addObserver("intl.accept_languages", this, false); michael@0: }, michael@0: michael@0: uninit: function() { michael@0: Services.prefs.removeObserver("intl.accept_languages", this); michael@0: }, michael@0: michael@0: observe: function(subject, topic, data) { michael@0: this.readPref(); michael@0: }, michael@0: michael@0: readPref: function() { michael@0: this._everRead = true; michael@0: try { michael@0: this._accepted = Services.prefs.getComplexValue( michael@0: "intl.accept_languages", Ci.nsIPrefLocalizedString).data; michael@0: } catch (err) { michael@0: this._log.error("Error reading intl.accept_languages pref: " + CommonUtils.exceptionStr(err)); michael@0: } michael@0: }, michael@0: michael@0: get accept_languages() { michael@0: if (!this._everRead) { michael@0: this.readPref(); michael@0: } michael@0: return this._accepted; michael@0: }, michael@0: }; michael@0: michael@0: // Singleton getter for Intl, creating an instance only when we first need it. michael@0: let intl = null; michael@0: function getIntl() { michael@0: if (!intl) { michael@0: intl = new Intl(); michael@0: } michael@0: return intl; michael@0: } michael@0: