services/common/rest.js

changeset 0
6474c204b198
     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 +

mercurial