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: #ifndef MERGED_COMPARTMENT michael@0: michael@0: const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; michael@0: michael@0: this.EXPORTED_SYMBOLS = [ michael@0: "RESTRequest", michael@0: "RESTResponse", michael@0: "TokenAuthenticatedRESTRequest", michael@0: ]; michael@0: michael@0: #endif 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/utils.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 HTTP requests to RESTish resources. michael@0: * michael@0: * @param uri michael@0: * URI for the request. This can be an nsIURI object or a string michael@0: * that can be used to create one. An exception will be thrown if michael@0: * the string is not a valid URI. michael@0: * michael@0: * Examples: michael@0: * michael@0: * (1) Quick GET request: michael@0: * michael@0: * new RESTRequest("http://server/rest/resource").get(function (error) { michael@0: * if (error) { michael@0: * // Deal with a network error. michael@0: * processNetworkErrorCode(error.result); michael@0: * return; michael@0: * } michael@0: * if (!this.response.success) { michael@0: * // Bail out if we're not getting an HTTP 2xx code. michael@0: * processHTTPError(this.response.status); michael@0: * return; michael@0: * } michael@0: * processData(this.response.body); michael@0: * }); michael@0: * michael@0: * (2) Quick PUT request (non-string data is automatically JSONified) michael@0: * michael@0: * new RESTRequest("http://server/rest/resource").put(data, function (error) { michael@0: * ... michael@0: * }); michael@0: * michael@0: * (3) Streaming GET michael@0: * michael@0: * let request = new RESTRequest("http://server/rest/resource"); michael@0: * request.setHeader("Accept", "application/newlines"); michael@0: * request.onComplete = function (error) { michael@0: * if (error) { michael@0: * // Deal with a network error. michael@0: * processNetworkErrorCode(error.result); michael@0: * return; michael@0: * } michael@0: * callbackAfterRequestHasCompleted() michael@0: * }); michael@0: * request.onProgress = function () { michael@0: * if (!this.response.success) { michael@0: * // Bail out if we're not getting an HTTP 2xx code. michael@0: * return; michael@0: * } michael@0: * // Process body data and reset it so we don't process the same data twice. michael@0: * processIncrementalData(this.response.body); michael@0: * this.response.body = ""; michael@0: * }); michael@0: * request.get(); michael@0: */ michael@0: this.RESTRequest = function RESTRequest(uri) { michael@0: this.status = this.NOT_SENT; michael@0: michael@0: // If we don't have an nsIURI object yet, make one. This will throw if michael@0: // 'uri' isn't a valid URI string. michael@0: if (!(uri instanceof Ci.nsIURI)) { michael@0: uri = Services.io.newURI(uri, null, null); michael@0: } michael@0: this.uri = uri; michael@0: michael@0: this._headers = {}; michael@0: this._log = Log.repository.getLogger(this._logName); michael@0: this._log.level = michael@0: Log.Level[Prefs.get("log.logger.rest.request")]; michael@0: } michael@0: RESTRequest.prototype = { michael@0: michael@0: _logName: "Services.Common.RESTRequest", michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([ michael@0: Ci.nsIBadCertListener2, michael@0: Ci.nsIInterfaceRequestor, michael@0: Ci.nsIChannelEventSink michael@0: ]), michael@0: michael@0: /*** Public API: ***/ michael@0: michael@0: /** michael@0: * URI for the request (an nsIURI object). michael@0: */ michael@0: uri: null, michael@0: michael@0: /** michael@0: * HTTP method (e.g. "GET") michael@0: */ michael@0: method: null, michael@0: michael@0: /** michael@0: * RESTResponse object michael@0: */ michael@0: response: null, michael@0: michael@0: /** michael@0: * nsIRequest load flags. Don't do any caching by default. Don't send user michael@0: * cookies and such over the wire (Bug 644734). michael@0: */ michael@0: loadFlags: Ci.nsIRequest.LOAD_BYPASS_CACHE | Ci.nsIRequest.INHIBIT_CACHING | Ci.nsIRequest.LOAD_ANONYMOUS, michael@0: michael@0: /** michael@0: * nsIHttpChannel michael@0: */ michael@0: channel: null, michael@0: michael@0: /** michael@0: * Flag to indicate the status of the request. michael@0: * michael@0: * One of NOT_SENT, SENT, IN_PROGRESS, COMPLETED, ABORTED. michael@0: */ michael@0: status: null, michael@0: michael@0: NOT_SENT: 0, michael@0: SENT: 1, michael@0: IN_PROGRESS: 2, michael@0: COMPLETED: 4, michael@0: ABORTED: 8, michael@0: michael@0: /** michael@0: * HTTP status text of response michael@0: */ michael@0: statusText: null, michael@0: michael@0: /** michael@0: * Request timeout (in seconds, though decimal values can be used for michael@0: * up to millisecond granularity.) michael@0: * michael@0: * 0 for no timeout. michael@0: */ michael@0: timeout: null, michael@0: michael@0: /** michael@0: * The encoding with which the response to this request must be treated. michael@0: * If a charset parameter is available in the HTTP Content-Type header for michael@0: * this response, that will always be used, and this value is ignored. We michael@0: * default to UTF-8 because that is a reasonable default. michael@0: */ michael@0: charset: "utf-8", michael@0: michael@0: /** michael@0: * Called when the request has been completed, including failures and michael@0: * timeouts. michael@0: * michael@0: * @param error michael@0: * Error that occurred while making the request, null if there michael@0: * was no error. michael@0: */ michael@0: onComplete: function onComplete(error) { michael@0: }, michael@0: michael@0: /** michael@0: * Called whenever data is being received on the channel. If this throws an michael@0: * exception, the request is aborted and the exception is passed as the michael@0: * error to onComplete(). michael@0: */ michael@0: onProgress: function onProgress() { michael@0: }, michael@0: michael@0: /** michael@0: * Set a request header. michael@0: */ michael@0: setHeader: function setHeader(name, value) { michael@0: this._headers[name.toLowerCase()] = value; michael@0: }, michael@0: michael@0: /** michael@0: * Perform an HTTP GET. michael@0: * michael@0: * @param onComplete michael@0: * Short-circuit way to set the 'onComplete' method. Optional. michael@0: * @param onProgress michael@0: * Short-circuit way to set the 'onProgress' method. Optional. michael@0: * michael@0: * @return the request object. michael@0: */ michael@0: get: function get(onComplete, onProgress) { michael@0: return this.dispatch("GET", null, onComplete, onProgress); michael@0: }, michael@0: michael@0: /** michael@0: * Perform an HTTP PUT. michael@0: * michael@0: * @param data michael@0: * Data to be used as the request body. If this isn't a string michael@0: * it will be JSONified automatically. michael@0: * @param onComplete michael@0: * Short-circuit way to set the 'onComplete' method. Optional. michael@0: * @param onProgress michael@0: * Short-circuit way to set the 'onProgress' method. Optional. michael@0: * michael@0: * @return the request object. michael@0: */ michael@0: put: function put(data, onComplete, onProgress) { michael@0: return this.dispatch("PUT", data, onComplete, onProgress); michael@0: }, michael@0: michael@0: /** michael@0: * Perform an HTTP POST. michael@0: * michael@0: * @param data michael@0: * Data to be used as the request body. If this isn't a string michael@0: * it will be JSONified automatically. michael@0: * @param onComplete michael@0: * Short-circuit way to set the 'onComplete' method. Optional. michael@0: * @param onProgress michael@0: * Short-circuit way to set the 'onProgress' method. Optional. michael@0: * michael@0: * @return the request object. michael@0: */ michael@0: post: function post(data, onComplete, onProgress) { michael@0: return this.dispatch("POST", data, onComplete, onProgress); michael@0: }, michael@0: michael@0: /** michael@0: * Perform an HTTP DELETE. michael@0: * michael@0: * @param onComplete michael@0: * Short-circuit way to set the 'onComplete' method. Optional. michael@0: * @param onProgress michael@0: * Short-circuit way to set the 'onProgress' method. Optional. michael@0: * michael@0: * @return the request object. michael@0: */ michael@0: delete: function delete_(onComplete, onProgress) { michael@0: return this.dispatch("DELETE", null, onComplete, onProgress); michael@0: }, michael@0: michael@0: /** michael@0: * Abort an active request. michael@0: */ michael@0: abort: function abort() { michael@0: if (this.status != this.SENT && this.status != this.IN_PROGRESS) { michael@0: throw "Can only abort a request that has been sent."; michael@0: } michael@0: michael@0: this.status = this.ABORTED; michael@0: this.channel.cancel(Cr.NS_BINDING_ABORTED); michael@0: michael@0: if (this.timeoutTimer) { michael@0: // Clear the abort timer now that the channel is done. michael@0: this.timeoutTimer.clear(); michael@0: } michael@0: }, michael@0: michael@0: /*** Implementation stuff ***/ michael@0: michael@0: dispatch: function dispatch(method, data, onComplete, onProgress) { michael@0: if (this.status != this.NOT_SENT) { michael@0: throw "Request has already been sent!"; michael@0: } michael@0: michael@0: this.method = method; michael@0: if (onComplete) { michael@0: this.onComplete = onComplete; michael@0: } michael@0: if (onProgress) { michael@0: this.onProgress = onProgress; michael@0: } michael@0: michael@0: // Create and initialize HTTP channel. michael@0: let channel = Services.io.newChannelFromURI(this.uri, null, null) michael@0: .QueryInterface(Ci.nsIRequest) michael@0: .QueryInterface(Ci.nsIHttpChannel); michael@0: this.channel = channel; michael@0: channel.loadFlags |= this.loadFlags; michael@0: channel.notificationCallbacks = this; michael@0: michael@0: // Set request headers. michael@0: let headers = this._headers; michael@0: for (let key in headers) { michael@0: if (key == 'authorization') { michael@0: this._log.trace("HTTP Header " + key + ": ***** (suppressed)"); michael@0: } else { michael@0: this._log.trace("HTTP Header " + key + ": " + headers[key]); michael@0: } michael@0: channel.setRequestHeader(key, headers[key], false); michael@0: } michael@0: michael@0: // Set HTTP request body. michael@0: if (method == "PUT" || method == "POST") { michael@0: // Convert non-string bodies into JSON. michael@0: if (typeof data != "string") { michael@0: data = JSON.stringify(data); michael@0: } michael@0: michael@0: this._log.debug(method + " Length: " + data.length); michael@0: if (this._log.level <= Log.Level.Trace) { michael@0: this._log.trace(method + " Body: " + data); michael@0: } michael@0: michael@0: let stream = Cc["@mozilla.org/io/string-input-stream;1"] michael@0: .createInstance(Ci.nsIStringInputStream); michael@0: stream.setData(data, data.length); michael@0: michael@0: let type = headers["content-type"] || "text/plain"; michael@0: channel.QueryInterface(Ci.nsIUploadChannel); michael@0: channel.setUploadStream(stream, type, data.length); michael@0: } michael@0: // We must set this after setting the upload stream, otherwise it michael@0: // will always be 'PUT'. Yeah, I know. michael@0: channel.requestMethod = method; michael@0: michael@0: // Before opening the channel, set the charset that serves as a hint michael@0: // as to what the response might be encoded as. michael@0: channel.contentCharset = this.charset; michael@0: michael@0: // Blast off! michael@0: try { michael@0: channel.asyncOpen(this, null); michael@0: } catch (ex) { michael@0: // asyncOpen can throw in a bunch of cases -- e.g., a forbidden port. michael@0: this._log.warn("Caught an error in asyncOpen: " + CommonUtils.exceptionStr(ex)); michael@0: CommonUtils.nextTick(onComplete.bind(this, ex)); michael@0: } michael@0: this.status = this.SENT; michael@0: this.delayTimeout(); michael@0: return this; michael@0: }, michael@0: michael@0: /** michael@0: * Create or push back the abort timer that kills this request. michael@0: */ michael@0: delayTimeout: function delayTimeout() { michael@0: if (this.timeout) { michael@0: CommonUtils.namedTimer(this.abortTimeout, this.timeout * 1000, this, michael@0: "timeoutTimer"); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Abort the request based on a timeout. michael@0: */ michael@0: abortTimeout: function abortTimeout() { michael@0: this.abort(); michael@0: let error = Components.Exception("Aborting due to channel inactivity.", michael@0: Cr.NS_ERROR_NET_TIMEOUT); michael@0: if (!this.onComplete) { michael@0: this._log.error("Unexpected error: onComplete not defined in " + michael@0: "abortTimeout.") michael@0: return; michael@0: } michael@0: this.onComplete(error); michael@0: }, michael@0: michael@0: /*** nsIStreamListener ***/ michael@0: michael@0: onStartRequest: function onStartRequest(channel) { michael@0: if (this.status == this.ABORTED) { michael@0: this._log.trace("Not proceeding with onStartRequest, request was aborted."); michael@0: return; michael@0: } michael@0: michael@0: try { michael@0: channel.QueryInterface(Ci.nsIHttpChannel); michael@0: } catch (ex) { michael@0: this._log.error("Unexpected error: channel is not a nsIHttpChannel!"); michael@0: this.status = this.ABORTED; michael@0: channel.cancel(Cr.NS_BINDING_ABORTED); michael@0: return; michael@0: } michael@0: michael@0: this.status = this.IN_PROGRESS; michael@0: michael@0: this._log.trace("onStartRequest: " + channel.requestMethod + " " + michael@0: channel.URI.spec); michael@0: michael@0: // Create a response object and fill it with some data. michael@0: let response = this.response = new RESTResponse(); michael@0: response.request = this; michael@0: response.body = ""; michael@0: michael@0: this.delayTimeout(); michael@0: }, michael@0: michael@0: onStopRequest: function onStopRequest(channel, context, statusCode) { michael@0: if (this.timeoutTimer) { michael@0: // Clear the abort timer now that the channel is done. michael@0: this.timeoutTimer.clear(); michael@0: } michael@0: michael@0: // We don't want to do anything for a request that's already been aborted. michael@0: if (this.status == this.ABORTED) { michael@0: this._log.trace("Not proceeding with onStopRequest, request was aborted."); michael@0: return; michael@0: } michael@0: michael@0: try { michael@0: channel.QueryInterface(Ci.nsIHttpChannel); michael@0: } catch (ex) { michael@0: this._log.error("Unexpected error: channel not nsIHttpChannel!"); michael@0: this.status = this.ABORTED; michael@0: return; michael@0: } michael@0: this.status = this.COMPLETED; michael@0: michael@0: let statusSuccess = Components.isSuccessCode(statusCode); michael@0: let uri = channel && channel.URI && channel.URI.spec || ""; michael@0: this._log.trace("Channel for " + channel.requestMethod + " " + uri + michael@0: " returned status code " + statusCode); michael@0: michael@0: if (!this.onComplete) { michael@0: this._log.error("Unexpected error: onComplete not defined in " + michael@0: "abortRequest."); michael@0: this.onProgress = null; michael@0: return; michael@0: } michael@0: michael@0: // Throw the failure code and stop execution. Use Components.Exception() michael@0: // instead of Error() so the exception is QI-able and can be passed across michael@0: // XPCOM borders while preserving the status code. michael@0: if (!statusSuccess) { michael@0: let message = Components.Exception("", statusCode).name; michael@0: let error = Components.Exception(message, statusCode); michael@0: this.onComplete(error); michael@0: this.onComplete = this.onProgress = null; michael@0: return; michael@0: } michael@0: michael@0: this._log.debug(this.method + " " + uri + " " + this.response.status); michael@0: michael@0: // Additionally give the full response body when Trace logging. michael@0: if (this._log.level <= Log.Level.Trace) { michael@0: this._log.trace(this.method + " body: " + this.response.body); michael@0: } michael@0: michael@0: delete this._inputStream; michael@0: michael@0: this.onComplete(null); michael@0: this.onComplete = this.onProgress = null; michael@0: }, michael@0: michael@0: onDataAvailable: function onDataAvailable(channel, cb, stream, off, count) { michael@0: // We get an nsIRequest, which doesn't have contentCharset. michael@0: try { michael@0: channel.QueryInterface(Ci.nsIHttpChannel); michael@0: } catch (ex) { michael@0: this._log.error("Unexpected error: channel not nsIHttpChannel!"); michael@0: this.abort(); michael@0: michael@0: if (this.onComplete) { michael@0: this.onComplete(ex); michael@0: } michael@0: michael@0: this.onComplete = this.onProgress = null; michael@0: return; michael@0: } michael@0: michael@0: if (channel.contentCharset) { michael@0: this.response.charset = channel.contentCharset; michael@0: michael@0: if (!this._converterStream) { michael@0: this._converterStream = Cc["@mozilla.org/intl/converter-input-stream;1"] michael@0: .createInstance(Ci.nsIConverterInputStream); michael@0: } michael@0: michael@0: this._converterStream.init(stream, channel.contentCharset, 0, michael@0: this._converterStream.DEFAULT_REPLACEMENT_CHARACTER); michael@0: michael@0: try { michael@0: let str = {}; michael@0: let num = this._converterStream.readString(count, str); michael@0: if (num != 0) { michael@0: this.response.body += str.value; michael@0: } michael@0: } catch (ex) { michael@0: this._log.warn("Exception thrown reading " + count + " bytes from " + michael@0: "the channel."); michael@0: this._log.warn(CommonUtils.exceptionStr(ex)); michael@0: throw ex; michael@0: } michael@0: } else { michael@0: this.response.charset = null; michael@0: michael@0: if (!this._inputStream) { michael@0: this._inputStream = Cc["@mozilla.org/scriptableinputstream;1"] michael@0: .createInstance(Ci.nsIScriptableInputStream); michael@0: } michael@0: michael@0: this._inputStream.init(stream); michael@0: michael@0: this.response.body += this._inputStream.read(count); michael@0: } michael@0: michael@0: try { michael@0: this.onProgress(); michael@0: } catch (ex) { michael@0: this._log.warn("Got exception calling onProgress handler, aborting " + michael@0: this.method + " " + channel.URI.spec); michael@0: this._log.debug("Exception: " + CommonUtils.exceptionStr(ex)); michael@0: this.abort(); michael@0: michael@0: if (!this.onComplete) { michael@0: this._log.error("Unexpected error: onComplete not defined in " + michael@0: "onDataAvailable."); michael@0: this.onProgress = null; michael@0: return; michael@0: } michael@0: michael@0: this.onComplete(ex); michael@0: this.onComplete = this.onProgress = null; michael@0: return; michael@0: } michael@0: michael@0: this.delayTimeout(); michael@0: }, michael@0: michael@0: /*** nsIInterfaceRequestor ***/ michael@0: michael@0: getInterface: function(aIID) { michael@0: return this.QueryInterface(aIID); michael@0: }, michael@0: michael@0: /*** nsIBadCertListener2 ***/ michael@0: michael@0: notifyCertProblem: function notifyCertProblem(socketInfo, sslStatus, targetHost) { michael@0: this._log.warn("Invalid HTTPS certificate encountered!"); michael@0: // Suppress invalid HTTPS certificate warnings in the UI. michael@0: // (The request will still fail.) michael@0: return true; michael@0: }, michael@0: michael@0: /** michael@0: * Returns true if headers from the old channel should be michael@0: * copied to the new channel. Invoked when a channel redirect michael@0: * is in progress. michael@0: */ michael@0: shouldCopyOnRedirect: function shouldCopyOnRedirect(oldChannel, newChannel, flags) { michael@0: let isInternal = !!(flags & Ci.nsIChannelEventSink.REDIRECT_INTERNAL); michael@0: let isSameURI = newChannel.URI.equals(oldChannel.URI); michael@0: this._log.debug("Channel redirect: " + oldChannel.URI.spec + ", " + michael@0: newChannel.URI.spec + ", internal = " + isInternal); michael@0: return isInternal && isSameURI; michael@0: }, michael@0: michael@0: /*** nsIChannelEventSink ***/ michael@0: asyncOnChannelRedirect: michael@0: function asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) { michael@0: michael@0: try { michael@0: newChannel.QueryInterface(Ci.nsIHttpChannel); michael@0: } catch (ex) { michael@0: this._log.error("Unexpected error: channel not nsIHttpChannel!"); michael@0: callback.onRedirectVerifyCallback(Cr.NS_ERROR_NO_INTERFACE); michael@0: return; michael@0: } michael@0: michael@0: // For internal redirects, copy the headers that our caller set. michael@0: try { michael@0: if (this.shouldCopyOnRedirect(oldChannel, newChannel, flags)) { michael@0: this._log.trace("Copying headers for safe internal redirect."); michael@0: for (let key in this._headers) { michael@0: newChannel.setRequestHeader(key, this._headers[key], false); michael@0: } michael@0: } michael@0: } catch (ex) { michael@0: this._log.error("Error copying headers: " + CommonUtils.exceptionStr(ex)); michael@0: } michael@0: michael@0: this.channel = newChannel; michael@0: michael@0: // We let all redirects proceed. michael@0: callback.onRedirectVerifyCallback(Cr.NS_OK); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Response object for a RESTRequest. This will be created automatically by michael@0: * the RESTRequest. michael@0: */ michael@0: this.RESTResponse = function RESTResponse() { michael@0: this._log = Log.repository.getLogger(this._logName); michael@0: this._log.level = michael@0: Log.Level[Prefs.get("log.logger.rest.response")]; michael@0: } michael@0: RESTResponse.prototype = { michael@0: michael@0: _logName: "Sync.RESTResponse", michael@0: michael@0: /** michael@0: * Corresponding REST request michael@0: */ michael@0: request: null, michael@0: michael@0: /** michael@0: * HTTP status code michael@0: */ michael@0: get status() { michael@0: let status; michael@0: try { michael@0: status = this.request.channel.responseStatus; michael@0: } catch (ex) { michael@0: this._log.debug("Caught exception fetching HTTP status code:" + michael@0: CommonUtils.exceptionStr(ex)); michael@0: return null; michael@0: } michael@0: delete this.status; michael@0: return this.status = status; michael@0: }, michael@0: michael@0: /** michael@0: * HTTP status text michael@0: */ michael@0: get statusText() { michael@0: let statusText; michael@0: try { michael@0: statusText = this.request.channel.responseStatusText; michael@0: } catch (ex) { michael@0: this._log.debug("Caught exception fetching HTTP status text:" + michael@0: CommonUtils.exceptionStr(ex)); michael@0: return null; michael@0: } michael@0: delete this.statusText; michael@0: return this.statusText = statusText; michael@0: }, michael@0: michael@0: /** michael@0: * Boolean flag that indicates whether the HTTP status code is 2xx or not. michael@0: */ michael@0: get success() { michael@0: let success; michael@0: try { michael@0: success = this.request.channel.requestSucceeded; michael@0: } catch (ex) { michael@0: this._log.debug("Caught exception fetching HTTP success flag:" + michael@0: CommonUtils.exceptionStr(ex)); michael@0: return null; michael@0: } michael@0: delete this.success; michael@0: return this.success = success; michael@0: }, michael@0: michael@0: /** michael@0: * Object containing HTTP headers (keyed as lower case) michael@0: */ michael@0: get headers() { michael@0: let headers = {}; michael@0: try { michael@0: this._log.trace("Processing response headers."); michael@0: let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel); michael@0: channel.visitResponseHeaders(function (header, value) { michael@0: headers[header.toLowerCase()] = value; michael@0: }); michael@0: } catch (ex) { michael@0: this._log.debug("Caught exception processing response headers:" + michael@0: CommonUtils.exceptionStr(ex)); michael@0: return null; michael@0: } michael@0: michael@0: delete this.headers; michael@0: return this.headers = headers; michael@0: }, michael@0: michael@0: /** michael@0: * HTTP body (string) michael@0: */ michael@0: body: null michael@0: michael@0: }; michael@0: michael@0: /** michael@0: * Single use MAC authenticated HTTP requests to RESTish resources. michael@0: * michael@0: * @param uri michael@0: * URI going to the RESTRequest constructor. michael@0: * @param authToken michael@0: * (Object) An auth token of the form {id: (string), key: (string)} michael@0: * from which the MAC Authentication header for this request will be michael@0: * derived. A token as obtained from michael@0: * TokenServerClient.getTokenFromBrowserIDAssertion is accepted. michael@0: * @param extra michael@0: * (Object) Optional extra parameters. Valid keys are: nonce_bytes, ts, michael@0: * nonce, and ext. See CrytoUtils.computeHTTPMACSHA1 for information on michael@0: * the purpose of these values. michael@0: */ michael@0: this.TokenAuthenticatedRESTRequest = michael@0: function TokenAuthenticatedRESTRequest(uri, authToken, extra) { michael@0: RESTRequest.call(this, uri); michael@0: this.authToken = authToken; michael@0: this.extra = extra || {}; michael@0: } michael@0: TokenAuthenticatedRESTRequest.prototype = { michael@0: __proto__: RESTRequest.prototype, michael@0: michael@0: dispatch: function dispatch(method, data, onComplete, onProgress) { michael@0: let sig = CryptoUtils.computeHTTPMACSHA1( michael@0: this.authToken.id, this.authToken.key, method, this.uri, this.extra michael@0: ); michael@0: michael@0: this.setHeader("Authorization", sig.getHeader()); michael@0: michael@0: return RESTRequest.prototype.dispatch.call( michael@0: this, method, data, onComplete, onProgress michael@0: ); michael@0: }, michael@0: }; michael@0: