services/sync/modules/resource.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
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 this.EXPORTED_SYMBOLS = [
michael@0 6 "AsyncResource",
michael@0 7 "Resource"
michael@0 8 ];
michael@0 9
michael@0 10 const Cc = Components.classes;
michael@0 11 const Ci = Components.interfaces;
michael@0 12 const Cr = Components.results;
michael@0 13 const Cu = Components.utils;
michael@0 14
michael@0 15 Cu.import("resource://gre/modules/Preferences.jsm");
michael@0 16 Cu.import("resource://services-common/async.js");
michael@0 17 Cu.import("resource://gre/modules/Log.jsm");
michael@0 18 Cu.import("resource://services-common/observers.js");
michael@0 19 Cu.import("resource://services-common/utils.js");
michael@0 20 Cu.import("resource://services-sync/constants.js");
michael@0 21 Cu.import("resource://services-sync/util.js");
michael@0 22
michael@0 23 const DEFAULT_LOAD_FLAGS =
michael@0 24 // Always validate the cache:
michael@0 25 Ci.nsIRequest.LOAD_BYPASS_CACHE |
michael@0 26 Ci.nsIRequest.INHIBIT_CACHING |
michael@0 27 // Don't send user cookies over the wire (Bug 644734).
michael@0 28 Ci.nsIRequest.LOAD_ANONYMOUS;
michael@0 29
michael@0 30 /*
michael@0 31 * AsyncResource represents a remote network resource, identified by a URI.
michael@0 32 * Create an instance like so:
michael@0 33 *
michael@0 34 * let resource = new AsyncResource("http://foobar.com/path/to/resource");
michael@0 35 *
michael@0 36 * The 'resource' object has the following methods to issue HTTP requests
michael@0 37 * of the corresponding HTTP methods:
michael@0 38 *
michael@0 39 * get(callback)
michael@0 40 * put(data, callback)
michael@0 41 * post(data, callback)
michael@0 42 * delete(callback)
michael@0 43 *
michael@0 44 * 'callback' is a function with the following signature:
michael@0 45 *
michael@0 46 * function callback(error, result) {...}
michael@0 47 *
michael@0 48 * 'error' will be null on successful requests. Likewise, result will not be
michael@0 49 * passed (=undefined) when an error occurs. Note that this is independent of
michael@0 50 * the status of the HTTP response.
michael@0 51 */
michael@0 52 this.AsyncResource = function AsyncResource(uri) {
michael@0 53 this._log = Log.repository.getLogger(this._logName);
michael@0 54 this._log.level =
michael@0 55 Log.Level[Svc.Prefs.get("log.logger.network.resources")];
michael@0 56 this.uri = uri;
michael@0 57 this._headers = {};
michael@0 58 this._onComplete = Utils.bind2(this, this._onComplete);
michael@0 59 }
michael@0 60 AsyncResource.prototype = {
michael@0 61 _logName: "Sync.AsyncResource",
michael@0 62
michael@0 63 // ** {{{ AsyncResource.serverTime }}} **
michael@0 64 //
michael@0 65 // Caches the latest server timestamp (X-Weave-Timestamp header).
michael@0 66 serverTime: null,
michael@0 67
michael@0 68 /**
michael@0 69 * Callback to be invoked at request time to add authentication details.
michael@0 70 *
michael@0 71 * By default, a global authenticator is provided. If this is set, it will
michael@0 72 * be used instead of the global one.
michael@0 73 */
michael@0 74 authenticator: null,
michael@0 75
michael@0 76 // The string to use as the base User-Agent in Sync requests.
michael@0 77 // These strings will look something like
michael@0 78 //
michael@0 79 // Firefox/4.0 FxSync/1.8.0.20100101.mobile
michael@0 80 //
michael@0 81 // or
michael@0 82 //
michael@0 83 // Firefox Aurora/5.0a1 FxSync/1.9.0.20110409.desktop
michael@0 84 //
michael@0 85 _userAgent:
michael@0 86 Services.appinfo.name + "/" + Services.appinfo.version + // Product.
michael@0 87 " FxSync/" + WEAVE_VERSION + "." + // Sync.
michael@0 88 Services.appinfo.appBuildID + ".", // Build.
michael@0 89
michael@0 90 // Wait 5 minutes before killing a request.
michael@0 91 ABORT_TIMEOUT: 300000,
michael@0 92
michael@0 93 // ** {{{ AsyncResource.headers }}} **
michael@0 94 //
michael@0 95 // Headers to be included when making a request for the resource.
michael@0 96 // Note: Header names should be all lower case, there's no explicit
michael@0 97 // check for duplicates due to case!
michael@0 98 get headers() {
michael@0 99 return this._headers;
michael@0 100 },
michael@0 101 set headers(value) {
michael@0 102 this._headers = value;
michael@0 103 },
michael@0 104 setHeader: function Res_setHeader(header, value) {
michael@0 105 this._headers[header.toLowerCase()] = value;
michael@0 106 },
michael@0 107 get headerNames() {
michael@0 108 return Object.keys(this.headers);
michael@0 109 },
michael@0 110
michael@0 111 // ** {{{ AsyncResource.uri }}} **
michael@0 112 //
michael@0 113 // URI representing this resource.
michael@0 114 get uri() {
michael@0 115 return this._uri;
michael@0 116 },
michael@0 117 set uri(value) {
michael@0 118 if (typeof value == 'string')
michael@0 119 this._uri = CommonUtils.makeURI(value);
michael@0 120 else
michael@0 121 this._uri = value;
michael@0 122 },
michael@0 123
michael@0 124 // ** {{{ AsyncResource.spec }}} **
michael@0 125 //
michael@0 126 // Get the string representation of the URI.
michael@0 127 get spec() {
michael@0 128 if (this._uri)
michael@0 129 return this._uri.spec;
michael@0 130 return null;
michael@0 131 },
michael@0 132
michael@0 133 // ** {{{ AsyncResource.data }}} **
michael@0 134 //
michael@0 135 // Get and set the data encapulated in the resource.
michael@0 136 _data: null,
michael@0 137 get data() this._data,
michael@0 138 set data(value) {
michael@0 139 this._data = value;
michael@0 140 },
michael@0 141
michael@0 142 // ** {{{ AsyncResource._createRequest }}} **
michael@0 143 //
michael@0 144 // This method returns a new IO Channel for requests to be made
michael@0 145 // through. It is never called directly, only {{{_doRequest}}} uses it
michael@0 146 // to obtain a request channel.
michael@0 147 //
michael@0 148 _createRequest: function Res__createRequest(method) {
michael@0 149 let channel = Services.io.newChannel(this.spec, null, null)
michael@0 150 .QueryInterface(Ci.nsIRequest)
michael@0 151 .QueryInterface(Ci.nsIHttpChannel);
michael@0 152
michael@0 153 channel.loadFlags |= DEFAULT_LOAD_FLAGS;
michael@0 154
michael@0 155 // Setup a callback to handle channel notifications.
michael@0 156 let listener = new ChannelNotificationListener(this.headerNames);
michael@0 157 channel.notificationCallbacks = listener;
michael@0 158
michael@0 159 // Compose a UA string fragment from the various available identifiers.
michael@0 160 if (Svc.Prefs.get("sendVersionInfo", true)) {
michael@0 161 let ua = this._userAgent + Svc.Prefs.get("client.type", "desktop");
michael@0 162 channel.setRequestHeader("user-agent", ua, false);
michael@0 163 }
michael@0 164
michael@0 165 let headers = this.headers;
michael@0 166
michael@0 167 if (this.authenticator) {
michael@0 168 let result = this.authenticator(this, method);
michael@0 169 if (result && result.headers) {
michael@0 170 for (let [k, v] in Iterator(result.headers)) {
michael@0 171 headers[k.toLowerCase()] = v;
michael@0 172 }
michael@0 173 }
michael@0 174 } else {
michael@0 175 this._log.debug("No authenticator found.");
michael@0 176 }
michael@0 177
michael@0 178 for (let [key, value] in Iterator(headers)) {
michael@0 179 if (key == 'authorization')
michael@0 180 this._log.trace("HTTP Header " + key + ": ***** (suppressed)");
michael@0 181 else
michael@0 182 this._log.trace("HTTP Header " + key + ": " + headers[key]);
michael@0 183 channel.setRequestHeader(key, headers[key], false);
michael@0 184 }
michael@0 185 return channel;
michael@0 186 },
michael@0 187
michael@0 188 _onProgress: function Res__onProgress(channel) {},
michael@0 189
michael@0 190 _doRequest: function _doRequest(action, data, callback) {
michael@0 191 this._log.trace("In _doRequest.");
michael@0 192 this._callback = callback;
michael@0 193 let channel = this._createRequest(action);
michael@0 194
michael@0 195 if ("undefined" != typeof(data))
michael@0 196 this._data = data;
michael@0 197
michael@0 198 // PUT and POST are treated differently because they have payload data.
michael@0 199 if ("PUT" == action || "POST" == action) {
michael@0 200 // Convert non-string bodies into JSON
michael@0 201 if (this._data.constructor.toString() != String)
michael@0 202 this._data = JSON.stringify(this._data);
michael@0 203
michael@0 204 this._log.debug(action + " Length: " + this._data.length);
michael@0 205 this._log.trace(action + " Body: " + this._data);
michael@0 206
michael@0 207 let type = ('content-type' in this._headers) ?
michael@0 208 this._headers['content-type'] : 'text/plain';
michael@0 209
michael@0 210 let stream = Cc["@mozilla.org/io/string-input-stream;1"].
michael@0 211 createInstance(Ci.nsIStringInputStream);
michael@0 212 stream.setData(this._data, this._data.length);
michael@0 213
michael@0 214 channel.QueryInterface(Ci.nsIUploadChannel);
michael@0 215 channel.setUploadStream(stream, type, this._data.length);
michael@0 216 }
michael@0 217
michael@0 218 // Setup a channel listener so that the actual network operation
michael@0 219 // is performed asynchronously.
michael@0 220 let listener = new ChannelListener(this._onComplete, this._onProgress,
michael@0 221 this._log, this.ABORT_TIMEOUT);
michael@0 222 channel.requestMethod = action;
michael@0 223 try {
michael@0 224 channel.asyncOpen(listener, null);
michael@0 225 } catch (ex) {
michael@0 226 // asyncOpen can throw in a bunch of cases -- e.g., a forbidden port.
michael@0 227 this._log.warn("Caught an error in asyncOpen: " + CommonUtils.exceptionStr(ex));
michael@0 228 CommonUtils.nextTick(callback.bind(this, ex));
michael@0 229 }
michael@0 230 },
michael@0 231
michael@0 232 _onComplete: function _onComplete(error, data, channel) {
michael@0 233 this._log.trace("In _onComplete. Error is " + error + ".");
michael@0 234
michael@0 235 if (error) {
michael@0 236 this._callback(error);
michael@0 237 return;
michael@0 238 }
michael@0 239
michael@0 240 this._data = data;
michael@0 241 let action = channel.requestMethod;
michael@0 242
michael@0 243 this._log.trace("Channel: " + channel);
michael@0 244 this._log.trace("Action: " + action);
michael@0 245
michael@0 246 // Process status and success first. This way a problem with headers
michael@0 247 // doesn't fail to include accurate status information.
michael@0 248 let status = 0;
michael@0 249 let success = false;
michael@0 250
michael@0 251 try {
michael@0 252 status = channel.responseStatus;
michael@0 253 success = channel.requestSucceeded; // HTTP status.
michael@0 254
michael@0 255 this._log.trace("Status: " + status);
michael@0 256 this._log.trace("Success: " + success);
michael@0 257
michael@0 258 // Log the status of the request.
michael@0 259 let mesg = [action, success ? "success" : "fail", status,
michael@0 260 channel.URI.spec].join(" ");
michael@0 261 this._log.debug("mesg: " + mesg);
michael@0 262
michael@0 263 if (mesg.length > 200)
michael@0 264 mesg = mesg.substr(0, 200) + "…";
michael@0 265 this._log.debug(mesg);
michael@0 266
michael@0 267 // Additionally give the full response body when Trace logging.
michael@0 268 if (this._log.level <= Log.Level.Trace)
michael@0 269 this._log.trace(action + " body: " + data);
michael@0 270
michael@0 271 } catch(ex) {
michael@0 272 // Got a response, but an exception occurred during processing.
michael@0 273 // This shouldn't occur.
michael@0 274 this._log.warn("Caught unexpected exception " + CommonUtils.exceptionStr(ex) +
michael@0 275 " in _onComplete.");
michael@0 276 this._log.debug(CommonUtils.stackTrace(ex));
michael@0 277 }
michael@0 278
michael@0 279 // Process headers. They can be empty, or the call can otherwise fail, so
michael@0 280 // put this in its own try block.
michael@0 281 let headers = {};
michael@0 282 try {
michael@0 283 this._log.trace("Processing response headers.");
michael@0 284
michael@0 285 // Read out the response headers if available.
michael@0 286 channel.visitResponseHeaders({
michael@0 287 visitHeader: function visitHeader(header, value) {
michael@0 288 headers[header.toLowerCase()] = value;
michael@0 289 }
michael@0 290 });
michael@0 291
michael@0 292 // This is a server-side safety valve to allow slowing down
michael@0 293 // clients without hurting performance.
michael@0 294 if (headers["x-weave-backoff"]) {
michael@0 295 let backoff = headers["x-weave-backoff"];
michael@0 296 this._log.debug("Got X-Weave-Backoff: " + backoff);
michael@0 297 Observers.notify("weave:service:backoff:interval",
michael@0 298 parseInt(backoff, 10));
michael@0 299 }
michael@0 300
michael@0 301 if (success && headers["x-weave-quota-remaining"]) {
michael@0 302 Observers.notify("weave:service:quota:remaining",
michael@0 303 parseInt(headers["x-weave-quota-remaining"], 10));
michael@0 304 }
michael@0 305 } catch (ex) {
michael@0 306 this._log.debug("Caught exception " + CommonUtils.exceptionStr(ex) +
michael@0 307 " visiting headers in _onComplete.");
michael@0 308 this._log.debug(CommonUtils.stackTrace(ex));
michael@0 309 }
michael@0 310
michael@0 311 let ret = new String(data);
michael@0 312 ret.status = status;
michael@0 313 ret.success = success;
michael@0 314 ret.headers = headers;
michael@0 315
michael@0 316 // Make a lazy getter to convert the json response into an object.
michael@0 317 // Note that this can cause a parse error to be thrown far away from the
michael@0 318 // actual fetch, so be warned!
michael@0 319 XPCOMUtils.defineLazyGetter(ret, "obj", function() {
michael@0 320 try {
michael@0 321 return JSON.parse(ret);
michael@0 322 } catch (ex) {
michael@0 323 this._log.warn("Got exception parsing response body: \"" + CommonUtils.exceptionStr(ex));
michael@0 324 // Stringify to avoid possibly printing non-printable characters.
michael@0 325 this._log.debug("Parse fail: Response body starts: \"" +
michael@0 326 JSON.stringify((ret + "").slice(0, 100)) +
michael@0 327 "\".");
michael@0 328 throw ex;
michael@0 329 }
michael@0 330 }.bind(this));
michael@0 331
michael@0 332 this._callback(null, ret);
michael@0 333 },
michael@0 334
michael@0 335 get: function get(callback) {
michael@0 336 this._doRequest("GET", undefined, callback);
michael@0 337 },
michael@0 338
michael@0 339 put: function put(data, callback) {
michael@0 340 if (typeof data == "function")
michael@0 341 [data, callback] = [undefined, data];
michael@0 342 this._doRequest("PUT", data, callback);
michael@0 343 },
michael@0 344
michael@0 345 post: function post(data, callback) {
michael@0 346 if (typeof data == "function")
michael@0 347 [data, callback] = [undefined, data];
michael@0 348 this._doRequest("POST", data, callback);
michael@0 349 },
michael@0 350
michael@0 351 delete: function delete_(callback) {
michael@0 352 this._doRequest("DELETE", undefined, callback);
michael@0 353 }
michael@0 354 };
michael@0 355
michael@0 356
michael@0 357 /*
michael@0 358 * Represent a remote network resource, identified by a URI, with a
michael@0 359 * synchronous API.
michael@0 360 *
michael@0 361 * 'Resource' is not recommended for new code. Use the asynchronous API of
michael@0 362 * 'AsyncResource' instead.
michael@0 363 */
michael@0 364 this.Resource = function Resource(uri) {
michael@0 365 AsyncResource.call(this, uri);
michael@0 366 }
michael@0 367 Resource.prototype = {
michael@0 368
michael@0 369 __proto__: AsyncResource.prototype,
michael@0 370
michael@0 371 _logName: "Sync.Resource",
michael@0 372
michael@0 373 // ** {{{ Resource._request }}} **
michael@0 374 //
michael@0 375 // Perform a particular HTTP request on the resource. This method
michael@0 376 // is never called directly, but is used by the high-level
michael@0 377 // {{{get}}}, {{{put}}}, {{{post}}} and {{delete}} methods.
michael@0 378 _request: function Res__request(action, data) {
michael@0 379 let cb = Async.makeSyncCallback();
michael@0 380 function callback(error, ret) {
michael@0 381 if (error)
michael@0 382 cb.throw(error);
michael@0 383 cb(ret);
michael@0 384 }
michael@0 385
michael@0 386 // The channel listener might get a failure code
michael@0 387 try {
michael@0 388 this._doRequest(action, data, callback);
michael@0 389 return Async.waitForSyncCallback(cb);
michael@0 390 } catch(ex) {
michael@0 391 // Combine the channel stack with this request stack. Need to create
michael@0 392 // a new error object for that.
michael@0 393 let error = Error(ex.message);
michael@0 394 error.result = ex.result;
michael@0 395 let chanStack = [];
michael@0 396 if (ex.stack)
michael@0 397 chanStack = ex.stack.trim().split(/\n/).slice(1);
michael@0 398 let requestStack = error.stack.split(/\n/).slice(1);
michael@0 399
michael@0 400 // Strip out the args for the last 2 frames because they're usually HUGE!
michael@0 401 for (let i = 0; i <= 1; i++)
michael@0 402 requestStack[i] = requestStack[i].replace(/\(".*"\)@/, "(...)@");
michael@0 403
michael@0 404 error.stack = chanStack.concat(requestStack).join("\n");
michael@0 405 throw error;
michael@0 406 }
michael@0 407 },
michael@0 408
michael@0 409 // ** {{{ Resource.get }}} **
michael@0 410 //
michael@0 411 // Perform an asynchronous HTTP GET for this resource.
michael@0 412 get: function Res_get() {
michael@0 413 return this._request("GET");
michael@0 414 },
michael@0 415
michael@0 416 // ** {{{ Resource.put }}} **
michael@0 417 //
michael@0 418 // Perform a HTTP PUT for this resource.
michael@0 419 put: function Res_put(data) {
michael@0 420 return this._request("PUT", data);
michael@0 421 },
michael@0 422
michael@0 423 // ** {{{ Resource.post }}} **
michael@0 424 //
michael@0 425 // Perform a HTTP POST for this resource.
michael@0 426 post: function Res_post(data) {
michael@0 427 return this._request("POST", data);
michael@0 428 },
michael@0 429
michael@0 430 // ** {{{ Resource.delete }}} **
michael@0 431 //
michael@0 432 // Perform a HTTP DELETE for this resource.
michael@0 433 delete: function Res_delete() {
michael@0 434 return this._request("DELETE");
michael@0 435 }
michael@0 436 };
michael@0 437
michael@0 438 // = ChannelListener =
michael@0 439 //
michael@0 440 // This object implements the {{{nsIStreamListener}}} interface
michael@0 441 // and is called as the network operation proceeds.
michael@0 442 function ChannelListener(onComplete, onProgress, logger, timeout) {
michael@0 443 this._onComplete = onComplete;
michael@0 444 this._onProgress = onProgress;
michael@0 445 this._log = logger;
michael@0 446 this._timeout = timeout;
michael@0 447 this.delayAbort();
michael@0 448 }
michael@0 449 ChannelListener.prototype = {
michael@0 450
michael@0 451 onStartRequest: function Channel_onStartRequest(channel) {
michael@0 452 this._log.trace("onStartRequest called for channel " + channel + ".");
michael@0 453
michael@0 454 try {
michael@0 455 channel.QueryInterface(Ci.nsIHttpChannel);
michael@0 456 } catch (ex) {
michael@0 457 this._log.error("Unexpected error: channel is not a nsIHttpChannel!");
michael@0 458 channel.cancel(Cr.NS_BINDING_ABORTED);
michael@0 459 return;
michael@0 460 }
michael@0 461
michael@0 462 // Save the latest server timestamp when possible.
michael@0 463 try {
michael@0 464 AsyncResource.serverTime = channel.getResponseHeader("X-Weave-Timestamp") - 0;
michael@0 465 }
michael@0 466 catch(ex) {}
michael@0 467
michael@0 468 this._log.trace("onStartRequest: " + channel.requestMethod + " " +
michael@0 469 channel.URI.spec);
michael@0 470 this._data = '';
michael@0 471 this.delayAbort();
michael@0 472 },
michael@0 473
michael@0 474 onStopRequest: function Channel_onStopRequest(channel, context, status) {
michael@0 475 // Clear the abort timer now that the channel is done.
michael@0 476 this.abortTimer.clear();
michael@0 477
michael@0 478 if (!this._onComplete) {
michael@0 479 this._log.error("Unexpected error: _onComplete not defined in onStopRequest.");
michael@0 480 this._onProgress = null;
michael@0 481 return;
michael@0 482 }
michael@0 483
michael@0 484 try {
michael@0 485 channel.QueryInterface(Ci.nsIHttpChannel);
michael@0 486 } catch (ex) {
michael@0 487 this._log.error("Unexpected error: channel is not a nsIHttpChannel!");
michael@0 488
michael@0 489 this._onComplete(ex, this._data, channel);
michael@0 490 this._onComplete = this._onProgress = null;
michael@0 491 return;
michael@0 492 }
michael@0 493
michael@0 494 let statusSuccess = Components.isSuccessCode(status);
michael@0 495 let uri = channel && channel.URI && channel.URI.spec || "<unknown>";
michael@0 496 this._log.trace("Channel for " + channel.requestMethod + " " + uri + ": " +
michael@0 497 "isSuccessCode(" + status + ")? " + statusSuccess);
michael@0 498
michael@0 499 if (this._data == '') {
michael@0 500 this._data = null;
michael@0 501 }
michael@0 502
michael@0 503 // Pass back the failure code and stop execution. Use Components.Exception()
michael@0 504 // instead of Error() so the exception is QI-able and can be passed across
michael@0 505 // XPCOM borders while preserving the status code.
michael@0 506 if (!statusSuccess) {
michael@0 507 let message = Components.Exception("", status).name;
michael@0 508 let error = Components.Exception(message, status);
michael@0 509
michael@0 510 this._onComplete(error, undefined, channel);
michael@0 511 this._onComplete = this._onProgress = null;
michael@0 512 return;
michael@0 513 }
michael@0 514
michael@0 515 this._log.trace("Channel: flags = " + channel.loadFlags +
michael@0 516 ", URI = " + uri +
michael@0 517 ", HTTP success? " + channel.requestSucceeded);
michael@0 518 this._onComplete(null, this._data, channel);
michael@0 519 this._onComplete = this._onProgress = null;
michael@0 520 },
michael@0 521
michael@0 522 onDataAvailable: function Channel_onDataAvail(req, cb, stream, off, count) {
michael@0 523 let siStream;
michael@0 524 try {
michael@0 525 siStream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(Ci.nsIScriptableInputStream);
michael@0 526 siStream.init(stream);
michael@0 527 } catch (ex) {
michael@0 528 this._log.warn("Exception creating nsIScriptableInputStream." + CommonUtils.exceptionStr(ex));
michael@0 529 this._log.debug("Parameters: " + req.URI.spec + ", " + stream + ", " + off + ", " + count);
michael@0 530 // Cannot proceed, so rethrow and allow the channel to cancel itself.
michael@0 531 throw ex;
michael@0 532 }
michael@0 533
michael@0 534 try {
michael@0 535 this._data += siStream.read(count);
michael@0 536 } catch (ex) {
michael@0 537 this._log.warn("Exception thrown reading " + count + " bytes from " + siStream + ".");
michael@0 538 throw ex;
michael@0 539 }
michael@0 540
michael@0 541 try {
michael@0 542 this._onProgress();
michael@0 543 } catch (ex) {
michael@0 544 this._log.warn("Got exception calling onProgress handler during fetch of "
michael@0 545 + req.URI.spec);
michael@0 546 this._log.debug(CommonUtils.exceptionStr(ex));
michael@0 547 this._log.trace("Rethrowing; expect a failure code from the HTTP channel.");
michael@0 548 throw ex;
michael@0 549 }
michael@0 550
michael@0 551 this.delayAbort();
michael@0 552 },
michael@0 553
michael@0 554 /**
michael@0 555 * Create or push back the abort timer that kills this request.
michael@0 556 */
michael@0 557 delayAbort: function delayAbort() {
michael@0 558 try {
michael@0 559 CommonUtils.namedTimer(this.abortRequest, this._timeout, this, "abortTimer");
michael@0 560 } catch (ex) {
michael@0 561 this._log.warn("Got exception extending abort timer: " + CommonUtils.exceptionStr(ex));
michael@0 562 }
michael@0 563 },
michael@0 564
michael@0 565 abortRequest: function abortRequest() {
michael@0 566 // Ignore any callbacks if we happen to get any now
michael@0 567 this.onStopRequest = function() {};
michael@0 568 let error = Components.Exception("Aborting due to channel inactivity.",
michael@0 569 Cr.NS_ERROR_NET_TIMEOUT);
michael@0 570 if (!this._onComplete) {
michael@0 571 this._log.error("Unexpected error: _onComplete not defined in " +
michael@0 572 "abortRequest.");
michael@0 573 return;
michael@0 574 }
michael@0 575 this._onComplete(error);
michael@0 576 }
michael@0 577 };
michael@0 578
michael@0 579 /**
michael@0 580 * This class handles channel notification events.
michael@0 581 *
michael@0 582 * An instance of this class is bound to each created channel.
michael@0 583 *
michael@0 584 * Optionally pass an array of header names. Each header named
michael@0 585 * in this array will be copied between the channels in the
michael@0 586 * event of a redirect.
michael@0 587 */
michael@0 588 function ChannelNotificationListener(headersToCopy) {
michael@0 589 this._headersToCopy = headersToCopy;
michael@0 590
michael@0 591 this._log = Log.repository.getLogger(this._logName);
michael@0 592 this._log.level = Log.Level[Svc.Prefs.get("log.logger.network.resources")];
michael@0 593 }
michael@0 594 ChannelNotificationListener.prototype = {
michael@0 595 _logName: "Sync.Resource",
michael@0 596
michael@0 597 getInterface: function(aIID) {
michael@0 598 return this.QueryInterface(aIID);
michael@0 599 },
michael@0 600
michael@0 601 QueryInterface: function(aIID) {
michael@0 602 if (aIID.equals(Ci.nsIBadCertListener2) ||
michael@0 603 aIID.equals(Ci.nsIInterfaceRequestor) ||
michael@0 604 aIID.equals(Ci.nsISupports) ||
michael@0 605 aIID.equals(Ci.nsIChannelEventSink))
michael@0 606 return this;
michael@0 607
michael@0 608 throw Cr.NS_ERROR_NO_INTERFACE;
michael@0 609 },
michael@0 610
michael@0 611 notifyCertProblem: function certProblem(socketInfo, sslStatus, targetHost) {
michael@0 612 let log = Log.repository.getLogger("Sync.CertListener");
michael@0 613 log.warn("Invalid HTTPS certificate encountered!");
michael@0 614
michael@0 615 // This suppresses the UI warning only. The request is still cancelled.
michael@0 616 return true;
michael@0 617 },
michael@0 618
michael@0 619 asyncOnChannelRedirect:
michael@0 620 function asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
michael@0 621
michael@0 622 let oldSpec = (oldChannel && oldChannel.URI) ? oldChannel.URI.spec : "<undefined>";
michael@0 623 let newSpec = (newChannel && newChannel.URI) ? newChannel.URI.spec : "<undefined>";
michael@0 624 this._log.debug("Channel redirect: " + oldSpec + ", " + newSpec + ", " + flags);
michael@0 625
michael@0 626 this._log.debug("Ensuring load flags are set.");
michael@0 627 newChannel.loadFlags |= DEFAULT_LOAD_FLAGS;
michael@0 628
michael@0 629 // For internal redirects, copy the headers that our caller set.
michael@0 630 try {
michael@0 631 if ((flags & Ci.nsIChannelEventSink.REDIRECT_INTERNAL) &&
michael@0 632 newChannel.URI.equals(oldChannel.URI)) {
michael@0 633 this._log.debug("Copying headers for safe internal redirect.");
michael@0 634
michael@0 635 // QI the channel so we can set headers on it.
michael@0 636 try {
michael@0 637 newChannel.QueryInterface(Ci.nsIHttpChannel);
michael@0 638 } catch (ex) {
michael@0 639 this._log.error("Unexpected error: channel is not a nsIHttpChannel!");
michael@0 640 throw ex;
michael@0 641 }
michael@0 642
michael@0 643 for (let header of this._headersToCopy) {
michael@0 644 let value = oldChannel.getRequestHeader(header);
michael@0 645 if (value) {
michael@0 646 let printed = (header == "authorization") ? "****" : value;
michael@0 647 this._log.debug("Header: " + header + " = " + printed);
michael@0 648 newChannel.setRequestHeader(header, value, false);
michael@0 649 } else {
michael@0 650 this._log.warn("No value for header " + header);
michael@0 651 }
michael@0 652 }
michael@0 653 }
michael@0 654 } catch (ex) {
michael@0 655 this._log.error("Error copying headers: " + CommonUtils.exceptionStr(ex));
michael@0 656 }
michael@0 657
michael@0 658 // We let all redirects proceed.
michael@0 659 try {
michael@0 660 callback.onRedirectVerifyCallback(Cr.NS_OK);
michael@0 661 } catch (ex) {
michael@0 662 this._log.error("onRedirectVerifyCallback threw!" + CommonUtils.exceptionStr(ex));
michael@0 663 }
michael@0 664 }
michael@0 665 };

mercurial