1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/services/common/rest.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,726 @@ 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 +#ifndef MERGED_COMPARTMENT 1.9 + 1.10 +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; 1.11 + 1.12 +this.EXPORTED_SYMBOLS = [ 1.13 + "RESTRequest", 1.14 + "RESTResponse", 1.15 + "TokenAuthenticatedRESTRequest", 1.16 +]; 1.17 + 1.18 +#endif 1.19 + 1.20 +Cu.import("resource://gre/modules/Preferences.jsm"); 1.21 +Cu.import("resource://gre/modules/Services.jsm"); 1.22 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.23 +Cu.import("resource://gre/modules/Log.jsm"); 1.24 +Cu.import("resource://services-common/utils.js"); 1.25 + 1.26 +XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils", 1.27 + "resource://services-crypto/utils.js"); 1.28 + 1.29 +const Prefs = new Preferences("services.common.rest."); 1.30 + 1.31 +/** 1.32 + * Single use HTTP requests to RESTish resources. 1.33 + * 1.34 + * @param uri 1.35 + * URI for the request. This can be an nsIURI object or a string 1.36 + * that can be used to create one. An exception will be thrown if 1.37 + * the string is not a valid URI. 1.38 + * 1.39 + * Examples: 1.40 + * 1.41 + * (1) Quick GET request: 1.42 + * 1.43 + * new RESTRequest("http://server/rest/resource").get(function (error) { 1.44 + * if (error) { 1.45 + * // Deal with a network error. 1.46 + * processNetworkErrorCode(error.result); 1.47 + * return; 1.48 + * } 1.49 + * if (!this.response.success) { 1.50 + * // Bail out if we're not getting an HTTP 2xx code. 1.51 + * processHTTPError(this.response.status); 1.52 + * return; 1.53 + * } 1.54 + * processData(this.response.body); 1.55 + * }); 1.56 + * 1.57 + * (2) Quick PUT request (non-string data is automatically JSONified) 1.58 + * 1.59 + * new RESTRequest("http://server/rest/resource").put(data, function (error) { 1.60 + * ... 1.61 + * }); 1.62 + * 1.63 + * (3) Streaming GET 1.64 + * 1.65 + * let request = new RESTRequest("http://server/rest/resource"); 1.66 + * request.setHeader("Accept", "application/newlines"); 1.67 + * request.onComplete = function (error) { 1.68 + * if (error) { 1.69 + * // Deal with a network error. 1.70 + * processNetworkErrorCode(error.result); 1.71 + * return; 1.72 + * } 1.73 + * callbackAfterRequestHasCompleted() 1.74 + * }); 1.75 + * request.onProgress = function () { 1.76 + * if (!this.response.success) { 1.77 + * // Bail out if we're not getting an HTTP 2xx code. 1.78 + * return; 1.79 + * } 1.80 + * // Process body data and reset it so we don't process the same data twice. 1.81 + * processIncrementalData(this.response.body); 1.82 + * this.response.body = ""; 1.83 + * }); 1.84 + * request.get(); 1.85 + */ 1.86 +this.RESTRequest = function RESTRequest(uri) { 1.87 + this.status = this.NOT_SENT; 1.88 + 1.89 + // If we don't have an nsIURI object yet, make one. This will throw if 1.90 + // 'uri' isn't a valid URI string. 1.91 + if (!(uri instanceof Ci.nsIURI)) { 1.92 + uri = Services.io.newURI(uri, null, null); 1.93 + } 1.94 + this.uri = uri; 1.95 + 1.96 + this._headers = {}; 1.97 + this._log = Log.repository.getLogger(this._logName); 1.98 + this._log.level = 1.99 + Log.Level[Prefs.get("log.logger.rest.request")]; 1.100 +} 1.101 +RESTRequest.prototype = { 1.102 + 1.103 + _logName: "Services.Common.RESTRequest", 1.104 + 1.105 + QueryInterface: XPCOMUtils.generateQI([ 1.106 + Ci.nsIBadCertListener2, 1.107 + Ci.nsIInterfaceRequestor, 1.108 + Ci.nsIChannelEventSink 1.109 + ]), 1.110 + 1.111 + /*** Public API: ***/ 1.112 + 1.113 + /** 1.114 + * URI for the request (an nsIURI object). 1.115 + */ 1.116 + uri: null, 1.117 + 1.118 + /** 1.119 + * HTTP method (e.g. "GET") 1.120 + */ 1.121 + method: null, 1.122 + 1.123 + /** 1.124 + * RESTResponse object 1.125 + */ 1.126 + response: null, 1.127 + 1.128 + /** 1.129 + * nsIRequest load flags. Don't do any caching by default. Don't send user 1.130 + * cookies and such over the wire (Bug 644734). 1.131 + */ 1.132 + loadFlags: Ci.nsIRequest.LOAD_BYPASS_CACHE | Ci.nsIRequest.INHIBIT_CACHING | Ci.nsIRequest.LOAD_ANONYMOUS, 1.133 + 1.134 + /** 1.135 + * nsIHttpChannel 1.136 + */ 1.137 + channel: null, 1.138 + 1.139 + /** 1.140 + * Flag to indicate the status of the request. 1.141 + * 1.142 + * One of NOT_SENT, SENT, IN_PROGRESS, COMPLETED, ABORTED. 1.143 + */ 1.144 + status: null, 1.145 + 1.146 + NOT_SENT: 0, 1.147 + SENT: 1, 1.148 + IN_PROGRESS: 2, 1.149 + COMPLETED: 4, 1.150 + ABORTED: 8, 1.151 + 1.152 + /** 1.153 + * HTTP status text of response 1.154 + */ 1.155 + statusText: null, 1.156 + 1.157 + /** 1.158 + * Request timeout (in seconds, though decimal values can be used for 1.159 + * up to millisecond granularity.) 1.160 + * 1.161 + * 0 for no timeout. 1.162 + */ 1.163 + timeout: null, 1.164 + 1.165 + /** 1.166 + * The encoding with which the response to this request must be treated. 1.167 + * If a charset parameter is available in the HTTP Content-Type header for 1.168 + * this response, that will always be used, and this value is ignored. We 1.169 + * default to UTF-8 because that is a reasonable default. 1.170 + */ 1.171 + charset: "utf-8", 1.172 + 1.173 + /** 1.174 + * Called when the request has been completed, including failures and 1.175 + * timeouts. 1.176 + * 1.177 + * @param error 1.178 + * Error that occurred while making the request, null if there 1.179 + * was no error. 1.180 + */ 1.181 + onComplete: function onComplete(error) { 1.182 + }, 1.183 + 1.184 + /** 1.185 + * Called whenever data is being received on the channel. If this throws an 1.186 + * exception, the request is aborted and the exception is passed as the 1.187 + * error to onComplete(). 1.188 + */ 1.189 + onProgress: function onProgress() { 1.190 + }, 1.191 + 1.192 + /** 1.193 + * Set a request header. 1.194 + */ 1.195 + setHeader: function setHeader(name, value) { 1.196 + this._headers[name.toLowerCase()] = value; 1.197 + }, 1.198 + 1.199 + /** 1.200 + * Perform an HTTP GET. 1.201 + * 1.202 + * @param onComplete 1.203 + * Short-circuit way to set the 'onComplete' method. Optional. 1.204 + * @param onProgress 1.205 + * Short-circuit way to set the 'onProgress' method. Optional. 1.206 + * 1.207 + * @return the request object. 1.208 + */ 1.209 + get: function get(onComplete, onProgress) { 1.210 + return this.dispatch("GET", null, onComplete, onProgress); 1.211 + }, 1.212 + 1.213 + /** 1.214 + * Perform an HTTP PUT. 1.215 + * 1.216 + * @param data 1.217 + * Data to be used as the request body. If this isn't a string 1.218 + * it will be JSONified automatically. 1.219 + * @param onComplete 1.220 + * Short-circuit way to set the 'onComplete' method. Optional. 1.221 + * @param onProgress 1.222 + * Short-circuit way to set the 'onProgress' method. Optional. 1.223 + * 1.224 + * @return the request object. 1.225 + */ 1.226 + put: function put(data, onComplete, onProgress) { 1.227 + return this.dispatch("PUT", data, onComplete, onProgress); 1.228 + }, 1.229 + 1.230 + /** 1.231 + * Perform an HTTP POST. 1.232 + * 1.233 + * @param data 1.234 + * Data to be used as the request body. If this isn't a string 1.235 + * it will be JSONified automatically. 1.236 + * @param onComplete 1.237 + * Short-circuit way to set the 'onComplete' method. Optional. 1.238 + * @param onProgress 1.239 + * Short-circuit way to set the 'onProgress' method. Optional. 1.240 + * 1.241 + * @return the request object. 1.242 + */ 1.243 + post: function post(data, onComplete, onProgress) { 1.244 + return this.dispatch("POST", data, onComplete, onProgress); 1.245 + }, 1.246 + 1.247 + /** 1.248 + * Perform an HTTP DELETE. 1.249 + * 1.250 + * @param onComplete 1.251 + * Short-circuit way to set the 'onComplete' method. Optional. 1.252 + * @param onProgress 1.253 + * Short-circuit way to set the 'onProgress' method. Optional. 1.254 + * 1.255 + * @return the request object. 1.256 + */ 1.257 + delete: function delete_(onComplete, onProgress) { 1.258 + return this.dispatch("DELETE", null, onComplete, onProgress); 1.259 + }, 1.260 + 1.261 + /** 1.262 + * Abort an active request. 1.263 + */ 1.264 + abort: function abort() { 1.265 + if (this.status != this.SENT && this.status != this.IN_PROGRESS) { 1.266 + throw "Can only abort a request that has been sent."; 1.267 + } 1.268 + 1.269 + this.status = this.ABORTED; 1.270 + this.channel.cancel(Cr.NS_BINDING_ABORTED); 1.271 + 1.272 + if (this.timeoutTimer) { 1.273 + // Clear the abort timer now that the channel is done. 1.274 + this.timeoutTimer.clear(); 1.275 + } 1.276 + }, 1.277 + 1.278 + /*** Implementation stuff ***/ 1.279 + 1.280 + dispatch: function dispatch(method, data, onComplete, onProgress) { 1.281 + if (this.status != this.NOT_SENT) { 1.282 + throw "Request has already been sent!"; 1.283 + } 1.284 + 1.285 + this.method = method; 1.286 + if (onComplete) { 1.287 + this.onComplete = onComplete; 1.288 + } 1.289 + if (onProgress) { 1.290 + this.onProgress = onProgress; 1.291 + } 1.292 + 1.293 + // Create and initialize HTTP channel. 1.294 + let channel = Services.io.newChannelFromURI(this.uri, null, null) 1.295 + .QueryInterface(Ci.nsIRequest) 1.296 + .QueryInterface(Ci.nsIHttpChannel); 1.297 + this.channel = channel; 1.298 + channel.loadFlags |= this.loadFlags; 1.299 + channel.notificationCallbacks = this; 1.300 + 1.301 + // Set request headers. 1.302 + let headers = this._headers; 1.303 + for (let key in headers) { 1.304 + if (key == 'authorization') { 1.305 + this._log.trace("HTTP Header " + key + ": ***** (suppressed)"); 1.306 + } else { 1.307 + this._log.trace("HTTP Header " + key + ": " + headers[key]); 1.308 + } 1.309 + channel.setRequestHeader(key, headers[key], false); 1.310 + } 1.311 + 1.312 + // Set HTTP request body. 1.313 + if (method == "PUT" || method == "POST") { 1.314 + // Convert non-string bodies into JSON. 1.315 + if (typeof data != "string") { 1.316 + data = JSON.stringify(data); 1.317 + } 1.318 + 1.319 + this._log.debug(method + " Length: " + data.length); 1.320 + if (this._log.level <= Log.Level.Trace) { 1.321 + this._log.trace(method + " Body: " + data); 1.322 + } 1.323 + 1.324 + let stream = Cc["@mozilla.org/io/string-input-stream;1"] 1.325 + .createInstance(Ci.nsIStringInputStream); 1.326 + stream.setData(data, data.length); 1.327 + 1.328 + let type = headers["content-type"] || "text/plain"; 1.329 + channel.QueryInterface(Ci.nsIUploadChannel); 1.330 + channel.setUploadStream(stream, type, data.length); 1.331 + } 1.332 + // We must set this after setting the upload stream, otherwise it 1.333 + // will always be 'PUT'. Yeah, I know. 1.334 + channel.requestMethod = method; 1.335 + 1.336 + // Before opening the channel, set the charset that serves as a hint 1.337 + // as to what the response might be encoded as. 1.338 + channel.contentCharset = this.charset; 1.339 + 1.340 + // Blast off! 1.341 + try { 1.342 + channel.asyncOpen(this, null); 1.343 + } catch (ex) { 1.344 + // asyncOpen can throw in a bunch of cases -- e.g., a forbidden port. 1.345 + this._log.warn("Caught an error in asyncOpen: " + CommonUtils.exceptionStr(ex)); 1.346 + CommonUtils.nextTick(onComplete.bind(this, ex)); 1.347 + } 1.348 + this.status = this.SENT; 1.349 + this.delayTimeout(); 1.350 + return this; 1.351 + }, 1.352 + 1.353 + /** 1.354 + * Create or push back the abort timer that kills this request. 1.355 + */ 1.356 + delayTimeout: function delayTimeout() { 1.357 + if (this.timeout) { 1.358 + CommonUtils.namedTimer(this.abortTimeout, this.timeout * 1000, this, 1.359 + "timeoutTimer"); 1.360 + } 1.361 + }, 1.362 + 1.363 + /** 1.364 + * Abort the request based on a timeout. 1.365 + */ 1.366 + abortTimeout: function abortTimeout() { 1.367 + this.abort(); 1.368 + let error = Components.Exception("Aborting due to channel inactivity.", 1.369 + Cr.NS_ERROR_NET_TIMEOUT); 1.370 + if (!this.onComplete) { 1.371 + this._log.error("Unexpected error: onComplete not defined in " + 1.372 + "abortTimeout.") 1.373 + return; 1.374 + } 1.375 + this.onComplete(error); 1.376 + }, 1.377 + 1.378 + /*** nsIStreamListener ***/ 1.379 + 1.380 + onStartRequest: function onStartRequest(channel) { 1.381 + if (this.status == this.ABORTED) { 1.382 + this._log.trace("Not proceeding with onStartRequest, request was aborted."); 1.383 + return; 1.384 + } 1.385 + 1.386 + try { 1.387 + channel.QueryInterface(Ci.nsIHttpChannel); 1.388 + } catch (ex) { 1.389 + this._log.error("Unexpected error: channel is not a nsIHttpChannel!"); 1.390 + this.status = this.ABORTED; 1.391 + channel.cancel(Cr.NS_BINDING_ABORTED); 1.392 + return; 1.393 + } 1.394 + 1.395 + this.status = this.IN_PROGRESS; 1.396 + 1.397 + this._log.trace("onStartRequest: " + channel.requestMethod + " " + 1.398 + channel.URI.spec); 1.399 + 1.400 + // Create a response object and fill it with some data. 1.401 + let response = this.response = new RESTResponse(); 1.402 + response.request = this; 1.403 + response.body = ""; 1.404 + 1.405 + this.delayTimeout(); 1.406 + }, 1.407 + 1.408 + onStopRequest: function onStopRequest(channel, context, statusCode) { 1.409 + if (this.timeoutTimer) { 1.410 + // Clear the abort timer now that the channel is done. 1.411 + this.timeoutTimer.clear(); 1.412 + } 1.413 + 1.414 + // We don't want to do anything for a request that's already been aborted. 1.415 + if (this.status == this.ABORTED) { 1.416 + this._log.trace("Not proceeding with onStopRequest, request was aborted."); 1.417 + return; 1.418 + } 1.419 + 1.420 + try { 1.421 + channel.QueryInterface(Ci.nsIHttpChannel); 1.422 + } catch (ex) { 1.423 + this._log.error("Unexpected error: channel not nsIHttpChannel!"); 1.424 + this.status = this.ABORTED; 1.425 + return; 1.426 + } 1.427 + this.status = this.COMPLETED; 1.428 + 1.429 + let statusSuccess = Components.isSuccessCode(statusCode); 1.430 + let uri = channel && channel.URI && channel.URI.spec || "<unknown>"; 1.431 + this._log.trace("Channel for " + channel.requestMethod + " " + uri + 1.432 + " returned status code " + statusCode); 1.433 + 1.434 + if (!this.onComplete) { 1.435 + this._log.error("Unexpected error: onComplete not defined in " + 1.436 + "abortRequest."); 1.437 + this.onProgress = null; 1.438 + return; 1.439 + } 1.440 + 1.441 + // Throw the failure code and stop execution. Use Components.Exception() 1.442 + // instead of Error() so the exception is QI-able and can be passed across 1.443 + // XPCOM borders while preserving the status code. 1.444 + if (!statusSuccess) { 1.445 + let message = Components.Exception("", statusCode).name; 1.446 + let error = Components.Exception(message, statusCode); 1.447 + this.onComplete(error); 1.448 + this.onComplete = this.onProgress = null; 1.449 + return; 1.450 + } 1.451 + 1.452 + this._log.debug(this.method + " " + uri + " " + this.response.status); 1.453 + 1.454 + // Additionally give the full response body when Trace logging. 1.455 + if (this._log.level <= Log.Level.Trace) { 1.456 + this._log.trace(this.method + " body: " + this.response.body); 1.457 + } 1.458 + 1.459 + delete this._inputStream; 1.460 + 1.461 + this.onComplete(null); 1.462 + this.onComplete = this.onProgress = null; 1.463 + }, 1.464 + 1.465 + onDataAvailable: function onDataAvailable(channel, cb, stream, off, count) { 1.466 + // We get an nsIRequest, which doesn't have contentCharset. 1.467 + try { 1.468 + channel.QueryInterface(Ci.nsIHttpChannel); 1.469 + } catch (ex) { 1.470 + this._log.error("Unexpected error: channel not nsIHttpChannel!"); 1.471 + this.abort(); 1.472 + 1.473 + if (this.onComplete) { 1.474 + this.onComplete(ex); 1.475 + } 1.476 + 1.477 + this.onComplete = this.onProgress = null; 1.478 + return; 1.479 + } 1.480 + 1.481 + if (channel.contentCharset) { 1.482 + this.response.charset = channel.contentCharset; 1.483 + 1.484 + if (!this._converterStream) { 1.485 + this._converterStream = Cc["@mozilla.org/intl/converter-input-stream;1"] 1.486 + .createInstance(Ci.nsIConverterInputStream); 1.487 + } 1.488 + 1.489 + this._converterStream.init(stream, channel.contentCharset, 0, 1.490 + this._converterStream.DEFAULT_REPLACEMENT_CHARACTER); 1.491 + 1.492 + try { 1.493 + let str = {}; 1.494 + let num = this._converterStream.readString(count, str); 1.495 + if (num != 0) { 1.496 + this.response.body += str.value; 1.497 + } 1.498 + } catch (ex) { 1.499 + this._log.warn("Exception thrown reading " + count + " bytes from " + 1.500 + "the channel."); 1.501 + this._log.warn(CommonUtils.exceptionStr(ex)); 1.502 + throw ex; 1.503 + } 1.504 + } else { 1.505 + this.response.charset = null; 1.506 + 1.507 + if (!this._inputStream) { 1.508 + this._inputStream = Cc["@mozilla.org/scriptableinputstream;1"] 1.509 + .createInstance(Ci.nsIScriptableInputStream); 1.510 + } 1.511 + 1.512 + this._inputStream.init(stream); 1.513 + 1.514 + this.response.body += this._inputStream.read(count); 1.515 + } 1.516 + 1.517 + try { 1.518 + this.onProgress(); 1.519 + } catch (ex) { 1.520 + this._log.warn("Got exception calling onProgress handler, aborting " + 1.521 + this.method + " " + channel.URI.spec); 1.522 + this._log.debug("Exception: " + CommonUtils.exceptionStr(ex)); 1.523 + this.abort(); 1.524 + 1.525 + if (!this.onComplete) { 1.526 + this._log.error("Unexpected error: onComplete not defined in " + 1.527 + "onDataAvailable."); 1.528 + this.onProgress = null; 1.529 + return; 1.530 + } 1.531 + 1.532 + this.onComplete(ex); 1.533 + this.onComplete = this.onProgress = null; 1.534 + return; 1.535 + } 1.536 + 1.537 + this.delayTimeout(); 1.538 + }, 1.539 + 1.540 + /*** nsIInterfaceRequestor ***/ 1.541 + 1.542 + getInterface: function(aIID) { 1.543 + return this.QueryInterface(aIID); 1.544 + }, 1.545 + 1.546 + /*** nsIBadCertListener2 ***/ 1.547 + 1.548 + notifyCertProblem: function notifyCertProblem(socketInfo, sslStatus, targetHost) { 1.549 + this._log.warn("Invalid HTTPS certificate encountered!"); 1.550 + // Suppress invalid HTTPS certificate warnings in the UI. 1.551 + // (The request will still fail.) 1.552 + return true; 1.553 + }, 1.554 + 1.555 + /** 1.556 + * Returns true if headers from the old channel should be 1.557 + * copied to the new channel. Invoked when a channel redirect 1.558 + * is in progress. 1.559 + */ 1.560 + shouldCopyOnRedirect: function shouldCopyOnRedirect(oldChannel, newChannel, flags) { 1.561 + let isInternal = !!(flags & Ci.nsIChannelEventSink.REDIRECT_INTERNAL); 1.562 + let isSameURI = newChannel.URI.equals(oldChannel.URI); 1.563 + this._log.debug("Channel redirect: " + oldChannel.URI.spec + ", " + 1.564 + newChannel.URI.spec + ", internal = " + isInternal); 1.565 + return isInternal && isSameURI; 1.566 + }, 1.567 + 1.568 + /*** nsIChannelEventSink ***/ 1.569 + asyncOnChannelRedirect: 1.570 + function asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) { 1.571 + 1.572 + try { 1.573 + newChannel.QueryInterface(Ci.nsIHttpChannel); 1.574 + } catch (ex) { 1.575 + this._log.error("Unexpected error: channel not nsIHttpChannel!"); 1.576 + callback.onRedirectVerifyCallback(Cr.NS_ERROR_NO_INTERFACE); 1.577 + return; 1.578 + } 1.579 + 1.580 + // For internal redirects, copy the headers that our caller set. 1.581 + try { 1.582 + if (this.shouldCopyOnRedirect(oldChannel, newChannel, flags)) { 1.583 + this._log.trace("Copying headers for safe internal redirect."); 1.584 + for (let key in this._headers) { 1.585 + newChannel.setRequestHeader(key, this._headers[key], false); 1.586 + } 1.587 + } 1.588 + } catch (ex) { 1.589 + this._log.error("Error copying headers: " + CommonUtils.exceptionStr(ex)); 1.590 + } 1.591 + 1.592 + this.channel = newChannel; 1.593 + 1.594 + // We let all redirects proceed. 1.595 + callback.onRedirectVerifyCallback(Cr.NS_OK); 1.596 + } 1.597 +}; 1.598 + 1.599 +/** 1.600 + * Response object for a RESTRequest. This will be created automatically by 1.601 + * the RESTRequest. 1.602 + */ 1.603 +this.RESTResponse = function RESTResponse() { 1.604 + this._log = Log.repository.getLogger(this._logName); 1.605 + this._log.level = 1.606 + Log.Level[Prefs.get("log.logger.rest.response")]; 1.607 +} 1.608 +RESTResponse.prototype = { 1.609 + 1.610 + _logName: "Sync.RESTResponse", 1.611 + 1.612 + /** 1.613 + * Corresponding REST request 1.614 + */ 1.615 + request: null, 1.616 + 1.617 + /** 1.618 + * HTTP status code 1.619 + */ 1.620 + get status() { 1.621 + let status; 1.622 + try { 1.623 + status = this.request.channel.responseStatus; 1.624 + } catch (ex) { 1.625 + this._log.debug("Caught exception fetching HTTP status code:" + 1.626 + CommonUtils.exceptionStr(ex)); 1.627 + return null; 1.628 + } 1.629 + delete this.status; 1.630 + return this.status = status; 1.631 + }, 1.632 + 1.633 + /** 1.634 + * HTTP status text 1.635 + */ 1.636 + get statusText() { 1.637 + let statusText; 1.638 + try { 1.639 + statusText = this.request.channel.responseStatusText; 1.640 + } catch (ex) { 1.641 + this._log.debug("Caught exception fetching HTTP status text:" + 1.642 + CommonUtils.exceptionStr(ex)); 1.643 + return null; 1.644 + } 1.645 + delete this.statusText; 1.646 + return this.statusText = statusText; 1.647 + }, 1.648 + 1.649 + /** 1.650 + * Boolean flag that indicates whether the HTTP status code is 2xx or not. 1.651 + */ 1.652 + get success() { 1.653 + let success; 1.654 + try { 1.655 + success = this.request.channel.requestSucceeded; 1.656 + } catch (ex) { 1.657 + this._log.debug("Caught exception fetching HTTP success flag:" + 1.658 + CommonUtils.exceptionStr(ex)); 1.659 + return null; 1.660 + } 1.661 + delete this.success; 1.662 + return this.success = success; 1.663 + }, 1.664 + 1.665 + /** 1.666 + * Object containing HTTP headers (keyed as lower case) 1.667 + */ 1.668 + get headers() { 1.669 + let headers = {}; 1.670 + try { 1.671 + this._log.trace("Processing response headers."); 1.672 + let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel); 1.673 + channel.visitResponseHeaders(function (header, value) { 1.674 + headers[header.toLowerCase()] = value; 1.675 + }); 1.676 + } catch (ex) { 1.677 + this._log.debug("Caught exception processing response headers:" + 1.678 + CommonUtils.exceptionStr(ex)); 1.679 + return null; 1.680 + } 1.681 + 1.682 + delete this.headers; 1.683 + return this.headers = headers; 1.684 + }, 1.685 + 1.686 + /** 1.687 + * HTTP body (string) 1.688 + */ 1.689 + body: null 1.690 + 1.691 +}; 1.692 + 1.693 +/** 1.694 + * Single use MAC authenticated HTTP requests to RESTish resources. 1.695 + * 1.696 + * @param uri 1.697 + * URI going to the RESTRequest constructor. 1.698 + * @param authToken 1.699 + * (Object) An auth token of the form {id: (string), key: (string)} 1.700 + * from which the MAC Authentication header for this request will be 1.701 + * derived. A token as obtained from 1.702 + * TokenServerClient.getTokenFromBrowserIDAssertion is accepted. 1.703 + * @param extra 1.704 + * (Object) Optional extra parameters. Valid keys are: nonce_bytes, ts, 1.705 + * nonce, and ext. See CrytoUtils.computeHTTPMACSHA1 for information on 1.706 + * the purpose of these values. 1.707 + */ 1.708 +this.TokenAuthenticatedRESTRequest = 1.709 + function TokenAuthenticatedRESTRequest(uri, authToken, extra) { 1.710 + RESTRequest.call(this, uri); 1.711 + this.authToken = authToken; 1.712 + this.extra = extra || {}; 1.713 +} 1.714 +TokenAuthenticatedRESTRequest.prototype = { 1.715 + __proto__: RESTRequest.prototype, 1.716 + 1.717 + dispatch: function dispatch(method, data, onComplete, onProgress) { 1.718 + let sig = CryptoUtils.computeHTTPMACSHA1( 1.719 + this.authToken.id, this.authToken.key, method, this.uri, this.extra 1.720 + ); 1.721 + 1.722 + this.setHeader("Authorization", sig.getHeader()); 1.723 + 1.724 + return RESTRequest.prototype.dispatch.call( 1.725 + this, method, data, onComplete, onProgress 1.726 + ); 1.727 + }, 1.728 +}; 1.729 +