services/common/rest.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 #ifndef MERGED_COMPARTMENT
michael@0 6
michael@0 7 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
michael@0 8
michael@0 9 this.EXPORTED_SYMBOLS = [
michael@0 10 "RESTRequest",
michael@0 11 "RESTResponse",
michael@0 12 "TokenAuthenticatedRESTRequest",
michael@0 13 ];
michael@0 14
michael@0 15 #endif
michael@0 16
michael@0 17 Cu.import("resource://gre/modules/Preferences.jsm");
michael@0 18 Cu.import("resource://gre/modules/Services.jsm");
michael@0 19 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 20 Cu.import("resource://gre/modules/Log.jsm");
michael@0 21 Cu.import("resource://services-common/utils.js");
michael@0 22
michael@0 23 XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils",
michael@0 24 "resource://services-crypto/utils.js");
michael@0 25
michael@0 26 const Prefs = new Preferences("services.common.rest.");
michael@0 27
michael@0 28 /**
michael@0 29 * Single use HTTP requests to RESTish resources.
michael@0 30 *
michael@0 31 * @param uri
michael@0 32 * URI for the request. This can be an nsIURI object or a string
michael@0 33 * that can be used to create one. An exception will be thrown if
michael@0 34 * the string is not a valid URI.
michael@0 35 *
michael@0 36 * Examples:
michael@0 37 *
michael@0 38 * (1) Quick GET request:
michael@0 39 *
michael@0 40 * new RESTRequest("http://server/rest/resource").get(function (error) {
michael@0 41 * if (error) {
michael@0 42 * // Deal with a network error.
michael@0 43 * processNetworkErrorCode(error.result);
michael@0 44 * return;
michael@0 45 * }
michael@0 46 * if (!this.response.success) {
michael@0 47 * // Bail out if we're not getting an HTTP 2xx code.
michael@0 48 * processHTTPError(this.response.status);
michael@0 49 * return;
michael@0 50 * }
michael@0 51 * processData(this.response.body);
michael@0 52 * });
michael@0 53 *
michael@0 54 * (2) Quick PUT request (non-string data is automatically JSONified)
michael@0 55 *
michael@0 56 * new RESTRequest("http://server/rest/resource").put(data, function (error) {
michael@0 57 * ...
michael@0 58 * });
michael@0 59 *
michael@0 60 * (3) Streaming GET
michael@0 61 *
michael@0 62 * let request = new RESTRequest("http://server/rest/resource");
michael@0 63 * request.setHeader("Accept", "application/newlines");
michael@0 64 * request.onComplete = function (error) {
michael@0 65 * if (error) {
michael@0 66 * // Deal with a network error.
michael@0 67 * processNetworkErrorCode(error.result);
michael@0 68 * return;
michael@0 69 * }
michael@0 70 * callbackAfterRequestHasCompleted()
michael@0 71 * });
michael@0 72 * request.onProgress = function () {
michael@0 73 * if (!this.response.success) {
michael@0 74 * // Bail out if we're not getting an HTTP 2xx code.
michael@0 75 * return;
michael@0 76 * }
michael@0 77 * // Process body data and reset it so we don't process the same data twice.
michael@0 78 * processIncrementalData(this.response.body);
michael@0 79 * this.response.body = "";
michael@0 80 * });
michael@0 81 * request.get();
michael@0 82 */
michael@0 83 this.RESTRequest = function RESTRequest(uri) {
michael@0 84 this.status = this.NOT_SENT;
michael@0 85
michael@0 86 // If we don't have an nsIURI object yet, make one. This will throw if
michael@0 87 // 'uri' isn't a valid URI string.
michael@0 88 if (!(uri instanceof Ci.nsIURI)) {
michael@0 89 uri = Services.io.newURI(uri, null, null);
michael@0 90 }
michael@0 91 this.uri = uri;
michael@0 92
michael@0 93 this._headers = {};
michael@0 94 this._log = Log.repository.getLogger(this._logName);
michael@0 95 this._log.level =
michael@0 96 Log.Level[Prefs.get("log.logger.rest.request")];
michael@0 97 }
michael@0 98 RESTRequest.prototype = {
michael@0 99
michael@0 100 _logName: "Services.Common.RESTRequest",
michael@0 101
michael@0 102 QueryInterface: XPCOMUtils.generateQI([
michael@0 103 Ci.nsIBadCertListener2,
michael@0 104 Ci.nsIInterfaceRequestor,
michael@0 105 Ci.nsIChannelEventSink
michael@0 106 ]),
michael@0 107
michael@0 108 /*** Public API: ***/
michael@0 109
michael@0 110 /**
michael@0 111 * URI for the request (an nsIURI object).
michael@0 112 */
michael@0 113 uri: null,
michael@0 114
michael@0 115 /**
michael@0 116 * HTTP method (e.g. "GET")
michael@0 117 */
michael@0 118 method: null,
michael@0 119
michael@0 120 /**
michael@0 121 * RESTResponse object
michael@0 122 */
michael@0 123 response: null,
michael@0 124
michael@0 125 /**
michael@0 126 * nsIRequest load flags. Don't do any caching by default. Don't send user
michael@0 127 * cookies and such over the wire (Bug 644734).
michael@0 128 */
michael@0 129 loadFlags: Ci.nsIRequest.LOAD_BYPASS_CACHE | Ci.nsIRequest.INHIBIT_CACHING | Ci.nsIRequest.LOAD_ANONYMOUS,
michael@0 130
michael@0 131 /**
michael@0 132 * nsIHttpChannel
michael@0 133 */
michael@0 134 channel: null,
michael@0 135
michael@0 136 /**
michael@0 137 * Flag to indicate the status of the request.
michael@0 138 *
michael@0 139 * One of NOT_SENT, SENT, IN_PROGRESS, COMPLETED, ABORTED.
michael@0 140 */
michael@0 141 status: null,
michael@0 142
michael@0 143 NOT_SENT: 0,
michael@0 144 SENT: 1,
michael@0 145 IN_PROGRESS: 2,
michael@0 146 COMPLETED: 4,
michael@0 147 ABORTED: 8,
michael@0 148
michael@0 149 /**
michael@0 150 * HTTP status text of response
michael@0 151 */
michael@0 152 statusText: null,
michael@0 153
michael@0 154 /**
michael@0 155 * Request timeout (in seconds, though decimal values can be used for
michael@0 156 * up to millisecond granularity.)
michael@0 157 *
michael@0 158 * 0 for no timeout.
michael@0 159 */
michael@0 160 timeout: null,
michael@0 161
michael@0 162 /**
michael@0 163 * The encoding with which the response to this request must be treated.
michael@0 164 * If a charset parameter is available in the HTTP Content-Type header for
michael@0 165 * this response, that will always be used, and this value is ignored. We
michael@0 166 * default to UTF-8 because that is a reasonable default.
michael@0 167 */
michael@0 168 charset: "utf-8",
michael@0 169
michael@0 170 /**
michael@0 171 * Called when the request has been completed, including failures and
michael@0 172 * timeouts.
michael@0 173 *
michael@0 174 * @param error
michael@0 175 * Error that occurred while making the request, null if there
michael@0 176 * was no error.
michael@0 177 */
michael@0 178 onComplete: function onComplete(error) {
michael@0 179 },
michael@0 180
michael@0 181 /**
michael@0 182 * Called whenever data is being received on the channel. If this throws an
michael@0 183 * exception, the request is aborted and the exception is passed as the
michael@0 184 * error to onComplete().
michael@0 185 */
michael@0 186 onProgress: function onProgress() {
michael@0 187 },
michael@0 188
michael@0 189 /**
michael@0 190 * Set a request header.
michael@0 191 */
michael@0 192 setHeader: function setHeader(name, value) {
michael@0 193 this._headers[name.toLowerCase()] = value;
michael@0 194 },
michael@0 195
michael@0 196 /**
michael@0 197 * Perform an HTTP GET.
michael@0 198 *
michael@0 199 * @param onComplete
michael@0 200 * Short-circuit way to set the 'onComplete' method. Optional.
michael@0 201 * @param onProgress
michael@0 202 * Short-circuit way to set the 'onProgress' method. Optional.
michael@0 203 *
michael@0 204 * @return the request object.
michael@0 205 */
michael@0 206 get: function get(onComplete, onProgress) {
michael@0 207 return this.dispatch("GET", null, onComplete, onProgress);
michael@0 208 },
michael@0 209
michael@0 210 /**
michael@0 211 * Perform an HTTP PUT.
michael@0 212 *
michael@0 213 * @param data
michael@0 214 * Data to be used as the request body. If this isn't a string
michael@0 215 * it will be JSONified automatically.
michael@0 216 * @param onComplete
michael@0 217 * Short-circuit way to set the 'onComplete' method. Optional.
michael@0 218 * @param onProgress
michael@0 219 * Short-circuit way to set the 'onProgress' method. Optional.
michael@0 220 *
michael@0 221 * @return the request object.
michael@0 222 */
michael@0 223 put: function put(data, onComplete, onProgress) {
michael@0 224 return this.dispatch("PUT", data, onComplete, onProgress);
michael@0 225 },
michael@0 226
michael@0 227 /**
michael@0 228 * Perform an HTTP POST.
michael@0 229 *
michael@0 230 * @param data
michael@0 231 * Data to be used as the request body. If this isn't a string
michael@0 232 * it will be JSONified automatically.
michael@0 233 * @param onComplete
michael@0 234 * Short-circuit way to set the 'onComplete' method. Optional.
michael@0 235 * @param onProgress
michael@0 236 * Short-circuit way to set the 'onProgress' method. Optional.
michael@0 237 *
michael@0 238 * @return the request object.
michael@0 239 */
michael@0 240 post: function post(data, onComplete, onProgress) {
michael@0 241 return this.dispatch("POST", data, onComplete, onProgress);
michael@0 242 },
michael@0 243
michael@0 244 /**
michael@0 245 * Perform an HTTP DELETE.
michael@0 246 *
michael@0 247 * @param onComplete
michael@0 248 * Short-circuit way to set the 'onComplete' method. Optional.
michael@0 249 * @param onProgress
michael@0 250 * Short-circuit way to set the 'onProgress' method. Optional.
michael@0 251 *
michael@0 252 * @return the request object.
michael@0 253 */
michael@0 254 delete: function delete_(onComplete, onProgress) {
michael@0 255 return this.dispatch("DELETE", null, onComplete, onProgress);
michael@0 256 },
michael@0 257
michael@0 258 /**
michael@0 259 * Abort an active request.
michael@0 260 */
michael@0 261 abort: function abort() {
michael@0 262 if (this.status != this.SENT && this.status != this.IN_PROGRESS) {
michael@0 263 throw "Can only abort a request that has been sent.";
michael@0 264 }
michael@0 265
michael@0 266 this.status = this.ABORTED;
michael@0 267 this.channel.cancel(Cr.NS_BINDING_ABORTED);
michael@0 268
michael@0 269 if (this.timeoutTimer) {
michael@0 270 // Clear the abort timer now that the channel is done.
michael@0 271 this.timeoutTimer.clear();
michael@0 272 }
michael@0 273 },
michael@0 274
michael@0 275 /*** Implementation stuff ***/
michael@0 276
michael@0 277 dispatch: function dispatch(method, data, onComplete, onProgress) {
michael@0 278 if (this.status != this.NOT_SENT) {
michael@0 279 throw "Request has already been sent!";
michael@0 280 }
michael@0 281
michael@0 282 this.method = method;
michael@0 283 if (onComplete) {
michael@0 284 this.onComplete = onComplete;
michael@0 285 }
michael@0 286 if (onProgress) {
michael@0 287 this.onProgress = onProgress;
michael@0 288 }
michael@0 289
michael@0 290 // Create and initialize HTTP channel.
michael@0 291 let channel = Services.io.newChannelFromURI(this.uri, null, null)
michael@0 292 .QueryInterface(Ci.nsIRequest)
michael@0 293 .QueryInterface(Ci.nsIHttpChannel);
michael@0 294 this.channel = channel;
michael@0 295 channel.loadFlags |= this.loadFlags;
michael@0 296 channel.notificationCallbacks = this;
michael@0 297
michael@0 298 // Set request headers.
michael@0 299 let headers = this._headers;
michael@0 300 for (let key in headers) {
michael@0 301 if (key == 'authorization') {
michael@0 302 this._log.trace("HTTP Header " + key + ": ***** (suppressed)");
michael@0 303 } else {
michael@0 304 this._log.trace("HTTP Header " + key + ": " + headers[key]);
michael@0 305 }
michael@0 306 channel.setRequestHeader(key, headers[key], false);
michael@0 307 }
michael@0 308
michael@0 309 // Set HTTP request body.
michael@0 310 if (method == "PUT" || method == "POST") {
michael@0 311 // Convert non-string bodies into JSON.
michael@0 312 if (typeof data != "string") {
michael@0 313 data = JSON.stringify(data);
michael@0 314 }
michael@0 315
michael@0 316 this._log.debug(method + " Length: " + data.length);
michael@0 317 if (this._log.level <= Log.Level.Trace) {
michael@0 318 this._log.trace(method + " Body: " + data);
michael@0 319 }
michael@0 320
michael@0 321 let stream = Cc["@mozilla.org/io/string-input-stream;1"]
michael@0 322 .createInstance(Ci.nsIStringInputStream);
michael@0 323 stream.setData(data, data.length);
michael@0 324
michael@0 325 let type = headers["content-type"] || "text/plain";
michael@0 326 channel.QueryInterface(Ci.nsIUploadChannel);
michael@0 327 channel.setUploadStream(stream, type, data.length);
michael@0 328 }
michael@0 329 // We must set this after setting the upload stream, otherwise it
michael@0 330 // will always be 'PUT'. Yeah, I know.
michael@0 331 channel.requestMethod = method;
michael@0 332
michael@0 333 // Before opening the channel, set the charset that serves as a hint
michael@0 334 // as to what the response might be encoded as.
michael@0 335 channel.contentCharset = this.charset;
michael@0 336
michael@0 337 // Blast off!
michael@0 338 try {
michael@0 339 channel.asyncOpen(this, null);
michael@0 340 } catch (ex) {
michael@0 341 // asyncOpen can throw in a bunch of cases -- e.g., a forbidden port.
michael@0 342 this._log.warn("Caught an error in asyncOpen: " + CommonUtils.exceptionStr(ex));
michael@0 343 CommonUtils.nextTick(onComplete.bind(this, ex));
michael@0 344 }
michael@0 345 this.status = this.SENT;
michael@0 346 this.delayTimeout();
michael@0 347 return this;
michael@0 348 },
michael@0 349
michael@0 350 /**
michael@0 351 * Create or push back the abort timer that kills this request.
michael@0 352 */
michael@0 353 delayTimeout: function delayTimeout() {
michael@0 354 if (this.timeout) {
michael@0 355 CommonUtils.namedTimer(this.abortTimeout, this.timeout * 1000, this,
michael@0 356 "timeoutTimer");
michael@0 357 }
michael@0 358 },
michael@0 359
michael@0 360 /**
michael@0 361 * Abort the request based on a timeout.
michael@0 362 */
michael@0 363 abortTimeout: function abortTimeout() {
michael@0 364 this.abort();
michael@0 365 let error = Components.Exception("Aborting due to channel inactivity.",
michael@0 366 Cr.NS_ERROR_NET_TIMEOUT);
michael@0 367 if (!this.onComplete) {
michael@0 368 this._log.error("Unexpected error: onComplete not defined in " +
michael@0 369 "abortTimeout.")
michael@0 370 return;
michael@0 371 }
michael@0 372 this.onComplete(error);
michael@0 373 },
michael@0 374
michael@0 375 /*** nsIStreamListener ***/
michael@0 376
michael@0 377 onStartRequest: function onStartRequest(channel) {
michael@0 378 if (this.status == this.ABORTED) {
michael@0 379 this._log.trace("Not proceeding with onStartRequest, request was aborted.");
michael@0 380 return;
michael@0 381 }
michael@0 382
michael@0 383 try {
michael@0 384 channel.QueryInterface(Ci.nsIHttpChannel);
michael@0 385 } catch (ex) {
michael@0 386 this._log.error("Unexpected error: channel is not a nsIHttpChannel!");
michael@0 387 this.status = this.ABORTED;
michael@0 388 channel.cancel(Cr.NS_BINDING_ABORTED);
michael@0 389 return;
michael@0 390 }
michael@0 391
michael@0 392 this.status = this.IN_PROGRESS;
michael@0 393
michael@0 394 this._log.trace("onStartRequest: " + channel.requestMethod + " " +
michael@0 395 channel.URI.spec);
michael@0 396
michael@0 397 // Create a response object and fill it with some data.
michael@0 398 let response = this.response = new RESTResponse();
michael@0 399 response.request = this;
michael@0 400 response.body = "";
michael@0 401
michael@0 402 this.delayTimeout();
michael@0 403 },
michael@0 404
michael@0 405 onStopRequest: function onStopRequest(channel, context, statusCode) {
michael@0 406 if (this.timeoutTimer) {
michael@0 407 // Clear the abort timer now that the channel is done.
michael@0 408 this.timeoutTimer.clear();
michael@0 409 }
michael@0 410
michael@0 411 // We don't want to do anything for a request that's already been aborted.
michael@0 412 if (this.status == this.ABORTED) {
michael@0 413 this._log.trace("Not proceeding with onStopRequest, request was aborted.");
michael@0 414 return;
michael@0 415 }
michael@0 416
michael@0 417 try {
michael@0 418 channel.QueryInterface(Ci.nsIHttpChannel);
michael@0 419 } catch (ex) {
michael@0 420 this._log.error("Unexpected error: channel not nsIHttpChannel!");
michael@0 421 this.status = this.ABORTED;
michael@0 422 return;
michael@0 423 }
michael@0 424 this.status = this.COMPLETED;
michael@0 425
michael@0 426 let statusSuccess = Components.isSuccessCode(statusCode);
michael@0 427 let uri = channel && channel.URI && channel.URI.spec || "<unknown>";
michael@0 428 this._log.trace("Channel for " + channel.requestMethod + " " + uri +
michael@0 429 " returned status code " + statusCode);
michael@0 430
michael@0 431 if (!this.onComplete) {
michael@0 432 this._log.error("Unexpected error: onComplete not defined in " +
michael@0 433 "abortRequest.");
michael@0 434 this.onProgress = null;
michael@0 435 return;
michael@0 436 }
michael@0 437
michael@0 438 // Throw the failure code and stop execution. Use Components.Exception()
michael@0 439 // instead of Error() so the exception is QI-able and can be passed across
michael@0 440 // XPCOM borders while preserving the status code.
michael@0 441 if (!statusSuccess) {
michael@0 442 let message = Components.Exception("", statusCode).name;
michael@0 443 let error = Components.Exception(message, statusCode);
michael@0 444 this.onComplete(error);
michael@0 445 this.onComplete = this.onProgress = null;
michael@0 446 return;
michael@0 447 }
michael@0 448
michael@0 449 this._log.debug(this.method + " " + uri + " " + this.response.status);
michael@0 450
michael@0 451 // Additionally give the full response body when Trace logging.
michael@0 452 if (this._log.level <= Log.Level.Trace) {
michael@0 453 this._log.trace(this.method + " body: " + this.response.body);
michael@0 454 }
michael@0 455
michael@0 456 delete this._inputStream;
michael@0 457
michael@0 458 this.onComplete(null);
michael@0 459 this.onComplete = this.onProgress = null;
michael@0 460 },
michael@0 461
michael@0 462 onDataAvailable: function onDataAvailable(channel, cb, stream, off, count) {
michael@0 463 // We get an nsIRequest, which doesn't have contentCharset.
michael@0 464 try {
michael@0 465 channel.QueryInterface(Ci.nsIHttpChannel);
michael@0 466 } catch (ex) {
michael@0 467 this._log.error("Unexpected error: channel not nsIHttpChannel!");
michael@0 468 this.abort();
michael@0 469
michael@0 470 if (this.onComplete) {
michael@0 471 this.onComplete(ex);
michael@0 472 }
michael@0 473
michael@0 474 this.onComplete = this.onProgress = null;
michael@0 475 return;
michael@0 476 }
michael@0 477
michael@0 478 if (channel.contentCharset) {
michael@0 479 this.response.charset = channel.contentCharset;
michael@0 480
michael@0 481 if (!this._converterStream) {
michael@0 482 this._converterStream = Cc["@mozilla.org/intl/converter-input-stream;1"]
michael@0 483 .createInstance(Ci.nsIConverterInputStream);
michael@0 484 }
michael@0 485
michael@0 486 this._converterStream.init(stream, channel.contentCharset, 0,
michael@0 487 this._converterStream.DEFAULT_REPLACEMENT_CHARACTER);
michael@0 488
michael@0 489 try {
michael@0 490 let str = {};
michael@0 491 let num = this._converterStream.readString(count, str);
michael@0 492 if (num != 0) {
michael@0 493 this.response.body += str.value;
michael@0 494 }
michael@0 495 } catch (ex) {
michael@0 496 this._log.warn("Exception thrown reading " + count + " bytes from " +
michael@0 497 "the channel.");
michael@0 498 this._log.warn(CommonUtils.exceptionStr(ex));
michael@0 499 throw ex;
michael@0 500 }
michael@0 501 } else {
michael@0 502 this.response.charset = null;
michael@0 503
michael@0 504 if (!this._inputStream) {
michael@0 505 this._inputStream = Cc["@mozilla.org/scriptableinputstream;1"]
michael@0 506 .createInstance(Ci.nsIScriptableInputStream);
michael@0 507 }
michael@0 508
michael@0 509 this._inputStream.init(stream);
michael@0 510
michael@0 511 this.response.body += this._inputStream.read(count);
michael@0 512 }
michael@0 513
michael@0 514 try {
michael@0 515 this.onProgress();
michael@0 516 } catch (ex) {
michael@0 517 this._log.warn("Got exception calling onProgress handler, aborting " +
michael@0 518 this.method + " " + channel.URI.spec);
michael@0 519 this._log.debug("Exception: " + CommonUtils.exceptionStr(ex));
michael@0 520 this.abort();
michael@0 521
michael@0 522 if (!this.onComplete) {
michael@0 523 this._log.error("Unexpected error: onComplete not defined in " +
michael@0 524 "onDataAvailable.");
michael@0 525 this.onProgress = null;
michael@0 526 return;
michael@0 527 }
michael@0 528
michael@0 529 this.onComplete(ex);
michael@0 530 this.onComplete = this.onProgress = null;
michael@0 531 return;
michael@0 532 }
michael@0 533
michael@0 534 this.delayTimeout();
michael@0 535 },
michael@0 536
michael@0 537 /*** nsIInterfaceRequestor ***/
michael@0 538
michael@0 539 getInterface: function(aIID) {
michael@0 540 return this.QueryInterface(aIID);
michael@0 541 },
michael@0 542
michael@0 543 /*** nsIBadCertListener2 ***/
michael@0 544
michael@0 545 notifyCertProblem: function notifyCertProblem(socketInfo, sslStatus, targetHost) {
michael@0 546 this._log.warn("Invalid HTTPS certificate encountered!");
michael@0 547 // Suppress invalid HTTPS certificate warnings in the UI.
michael@0 548 // (The request will still fail.)
michael@0 549 return true;
michael@0 550 },
michael@0 551
michael@0 552 /**
michael@0 553 * Returns true if headers from the old channel should be
michael@0 554 * copied to the new channel. Invoked when a channel redirect
michael@0 555 * is in progress.
michael@0 556 */
michael@0 557 shouldCopyOnRedirect: function shouldCopyOnRedirect(oldChannel, newChannel, flags) {
michael@0 558 let isInternal = !!(flags & Ci.nsIChannelEventSink.REDIRECT_INTERNAL);
michael@0 559 let isSameURI = newChannel.URI.equals(oldChannel.URI);
michael@0 560 this._log.debug("Channel redirect: " + oldChannel.URI.spec + ", " +
michael@0 561 newChannel.URI.spec + ", internal = " + isInternal);
michael@0 562 return isInternal && isSameURI;
michael@0 563 },
michael@0 564
michael@0 565 /*** nsIChannelEventSink ***/
michael@0 566 asyncOnChannelRedirect:
michael@0 567 function asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
michael@0 568
michael@0 569 try {
michael@0 570 newChannel.QueryInterface(Ci.nsIHttpChannel);
michael@0 571 } catch (ex) {
michael@0 572 this._log.error("Unexpected error: channel not nsIHttpChannel!");
michael@0 573 callback.onRedirectVerifyCallback(Cr.NS_ERROR_NO_INTERFACE);
michael@0 574 return;
michael@0 575 }
michael@0 576
michael@0 577 // For internal redirects, copy the headers that our caller set.
michael@0 578 try {
michael@0 579 if (this.shouldCopyOnRedirect(oldChannel, newChannel, flags)) {
michael@0 580 this._log.trace("Copying headers for safe internal redirect.");
michael@0 581 for (let key in this._headers) {
michael@0 582 newChannel.setRequestHeader(key, this._headers[key], false);
michael@0 583 }
michael@0 584 }
michael@0 585 } catch (ex) {
michael@0 586 this._log.error("Error copying headers: " + CommonUtils.exceptionStr(ex));
michael@0 587 }
michael@0 588
michael@0 589 this.channel = newChannel;
michael@0 590
michael@0 591 // We let all redirects proceed.
michael@0 592 callback.onRedirectVerifyCallback(Cr.NS_OK);
michael@0 593 }
michael@0 594 };
michael@0 595
michael@0 596 /**
michael@0 597 * Response object for a RESTRequest. This will be created automatically by
michael@0 598 * the RESTRequest.
michael@0 599 */
michael@0 600 this.RESTResponse = function RESTResponse() {
michael@0 601 this._log = Log.repository.getLogger(this._logName);
michael@0 602 this._log.level =
michael@0 603 Log.Level[Prefs.get("log.logger.rest.response")];
michael@0 604 }
michael@0 605 RESTResponse.prototype = {
michael@0 606
michael@0 607 _logName: "Sync.RESTResponse",
michael@0 608
michael@0 609 /**
michael@0 610 * Corresponding REST request
michael@0 611 */
michael@0 612 request: null,
michael@0 613
michael@0 614 /**
michael@0 615 * HTTP status code
michael@0 616 */
michael@0 617 get status() {
michael@0 618 let status;
michael@0 619 try {
michael@0 620 status = this.request.channel.responseStatus;
michael@0 621 } catch (ex) {
michael@0 622 this._log.debug("Caught exception fetching HTTP status code:" +
michael@0 623 CommonUtils.exceptionStr(ex));
michael@0 624 return null;
michael@0 625 }
michael@0 626 delete this.status;
michael@0 627 return this.status = status;
michael@0 628 },
michael@0 629
michael@0 630 /**
michael@0 631 * HTTP status text
michael@0 632 */
michael@0 633 get statusText() {
michael@0 634 let statusText;
michael@0 635 try {
michael@0 636 statusText = this.request.channel.responseStatusText;
michael@0 637 } catch (ex) {
michael@0 638 this._log.debug("Caught exception fetching HTTP status text:" +
michael@0 639 CommonUtils.exceptionStr(ex));
michael@0 640 return null;
michael@0 641 }
michael@0 642 delete this.statusText;
michael@0 643 return this.statusText = statusText;
michael@0 644 },
michael@0 645
michael@0 646 /**
michael@0 647 * Boolean flag that indicates whether the HTTP status code is 2xx or not.
michael@0 648 */
michael@0 649 get success() {
michael@0 650 let success;
michael@0 651 try {
michael@0 652 success = this.request.channel.requestSucceeded;
michael@0 653 } catch (ex) {
michael@0 654 this._log.debug("Caught exception fetching HTTP success flag:" +
michael@0 655 CommonUtils.exceptionStr(ex));
michael@0 656 return null;
michael@0 657 }
michael@0 658 delete this.success;
michael@0 659 return this.success = success;
michael@0 660 },
michael@0 661
michael@0 662 /**
michael@0 663 * Object containing HTTP headers (keyed as lower case)
michael@0 664 */
michael@0 665 get headers() {
michael@0 666 let headers = {};
michael@0 667 try {
michael@0 668 this._log.trace("Processing response headers.");
michael@0 669 let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel);
michael@0 670 channel.visitResponseHeaders(function (header, value) {
michael@0 671 headers[header.toLowerCase()] = value;
michael@0 672 });
michael@0 673 } catch (ex) {
michael@0 674 this._log.debug("Caught exception processing response headers:" +
michael@0 675 CommonUtils.exceptionStr(ex));
michael@0 676 return null;
michael@0 677 }
michael@0 678
michael@0 679 delete this.headers;
michael@0 680 return this.headers = headers;
michael@0 681 },
michael@0 682
michael@0 683 /**
michael@0 684 * HTTP body (string)
michael@0 685 */
michael@0 686 body: null
michael@0 687
michael@0 688 };
michael@0 689
michael@0 690 /**
michael@0 691 * Single use MAC authenticated HTTP requests to RESTish resources.
michael@0 692 *
michael@0 693 * @param uri
michael@0 694 * URI going to the RESTRequest constructor.
michael@0 695 * @param authToken
michael@0 696 * (Object) An auth token of the form {id: (string), key: (string)}
michael@0 697 * from which the MAC Authentication header for this request will be
michael@0 698 * derived. A token as obtained from
michael@0 699 * TokenServerClient.getTokenFromBrowserIDAssertion is accepted.
michael@0 700 * @param extra
michael@0 701 * (Object) Optional extra parameters. Valid keys are: nonce_bytes, ts,
michael@0 702 * nonce, and ext. See CrytoUtils.computeHTTPMACSHA1 for information on
michael@0 703 * the purpose of these values.
michael@0 704 */
michael@0 705 this.TokenAuthenticatedRESTRequest =
michael@0 706 function TokenAuthenticatedRESTRequest(uri, authToken, extra) {
michael@0 707 RESTRequest.call(this, uri);
michael@0 708 this.authToken = authToken;
michael@0 709 this.extra = extra || {};
michael@0 710 }
michael@0 711 TokenAuthenticatedRESTRequest.prototype = {
michael@0 712 __proto__: RESTRequest.prototype,
michael@0 713
michael@0 714 dispatch: function dispatch(method, data, onComplete, onProgress) {
michael@0 715 let sig = CryptoUtils.computeHTTPMACSHA1(
michael@0 716 this.authToken.id, this.authToken.key, method, this.uri, this.extra
michael@0 717 );
michael@0 718
michael@0 719 this.setHeader("Authorization", sig.getHeader());
michael@0 720
michael@0 721 return RESTRequest.prototype.dispatch.call(
michael@0 722 this, method, data, onComplete, onProgress
michael@0 723 );
michael@0 724 },
michael@0 725 };
michael@0 726

mercurial