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