400 lines, 187 LOC, 172 covered (91%)
16 | 1 | /* ***** BEGIN LICENSE BLOCK ***** |
2 | * Version: MPL 1.1/GPL 2.0/LGPL 2.1 |
|
3 | * |
|
4 | * The contents of this file are subject to the Mozilla Public License Version |
|
5 | * 1.1 (the "License"); you may not use this file except in compliance with |
|
6 | * the License. You may obtain a copy of the License at |
|
7 | * http://www.mozilla.org/MPL/ |
|
8 | * |
|
9 | * Software distributed under the License is distributed on an "AS IS" basis, |
|
10 | * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License |
|
11 | * for the specific language governing rights and limitations under the |
|
12 | * License. |
|
13 | * |
|
14 | * The Original Code is Bookmarks Sync. |
|
15 | * |
|
16 | * The Initial Developer of the Original Code is Mozilla. |
|
17 | * Portions created by the Initial Developer are Copyright (C) 2007 |
|
18 | * the Initial Developer. All Rights Reserved. |
|
19 | * |
|
20 | * Contributor(s): |
|
21 | * Dan Mills <thunder@mozilla.com> |
|
22 | * Anant Narayanan <anant@kix.in> |
|
23 | * |
|
24 | * Alternatively, the contents of this file may be used under the terms of |
|
25 | * either the GNU General Public License Version 2 or later (the "GPL"), or |
|
26 | * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), |
|
27 | * in which case the provisions of the GPL or the LGPL are applicable instead |
|
28 | * of those above. If you wish to allow use of your version of this file only |
|
29 | * under the terms of either the GPL or the LGPL, and not to allow others to |
|
30 | * use your version of this file under the terms of the MPL, indicate your |
|
31 | * decision by deleting the provisions above and replace them with the notice |
|
32 | * and other provisions required by the GPL or the LGPL. If you do not delete |
|
33 | * the provisions above, a recipient may use your version of this file under |
|
34 | * the terms of any one of the MPL, the GPL or the LGPL. |
|
35 | * |
|
36 | * ***** END LICENSE BLOCK ***** */ |
|
37 | ||
112 | 38 | const EXPORTED_SYMBOLS = ["Resource"]; |
39 | ||
64 | 40 | const Cc = Components.classes; |
64 | 41 | const Ci = Components.interfaces; |
64 | 42 | const Cr = Components.results; |
64 | 43 | const Cu = Components.utils; |
44 | ||
80 | 45 | Cu.import("resource://weave/ext/Observers.js"); |
80 | 46 | Cu.import("resource://weave/ext/Preferences.js"); |
80 | 47 | Cu.import("resource://weave/ext/Sync.js"); |
80 | 48 | Cu.import("resource://weave/log4moz.js"); |
80 | 49 | Cu.import("resource://weave/constants.js"); |
80 | 50 | Cu.import("resource://weave/util.js"); |
80 | 51 | Cu.import("resource://weave/auth.js"); |
52 | ||
53 | // = Resource = |
|
54 | // |
|
55 | // Represents a remote network resource, identified by a URI. |
|
149 | 56 | function Resource(uri) { |
819 | 57 | this._log = Log4Moz.repository.getLogger(this._logName); |
117 | 58 | this._log.level = |
1053 | 59 | Log4Moz.Level[Utils.prefs.getCharPref("log.logger.network.resources")]; |
351 | 60 | this.uri = uri; |
585 | 61 | this._headers = {}; |
62 | } |
|
32 | 63 | Resource.prototype = { |
32 | 64 | _logName: "Net.Resource", |
65 | ||
66 | // ** {{{ Resource.serverTime }}} ** |
|
67 | // |
|
68 | // Caches the latest server timestamp (X-Weave-Timestamp header). |
|
32 | 69 | serverTime: null, |
70 | ||
71 | // ** {{{ Resource.authenticator }}} ** |
|
72 | // |
|
73 | // Getter and setter for the authenticator module |
|
74 | // responsible for this particular resource. The authenticator |
|
75 | // module may modify the headers to perform authentication |
|
76 | // while performing a request for the resource, for example. |
|
194 | 77 | get authenticator() { |
324 | 78 | if (this._authenticator) |
4 | 79 | return this._authenticator; |
80 | else |
|
800 | 81 | return Auth.lookupAuthenticator(this.spec); |
82 | }, |
|
33 | 83 | set authenticator(value) { |
4 | 84 | this._authenticator = value; |
85 | }, |
|
86 | ||
87 | // ** {{{ Resource.headers }}} ** |
|
88 | // |
|
89 | // Headers to be included when making a request for the resource. |
|
90 | // Note: Header names should be all lower case, there's no explicit |
|
91 | // check for duplicates due to case! |
|
192 | 92 | get headers() { |
960 | 93 | return this.authenticator.onRequest(this._headers); |
94 | }, |
|
33 | 95 | set headers(value) { |
4 | 96 | this._headers = value; |
97 | }, |
|
71 | 98 | setHeader: function Res_setHeader() { |
156 | 99 | if (arguments.length % 2) |
100 | throw "setHeader only accepts arguments in multiples of 2"; |
|
632 | 101 | for (let i = 0; i < arguments.length; i += 2) { |
480 | 102 | this._headers[arguments[i].toLowerCase()] = arguments[i + 1]; |
39 | 103 | } |
104 | }, |
|
105 | ||
106 | // ** {{{ Resource.uri }}} ** |
|
107 | // |
|
108 | // URI representing this resource. |
|
1714 | 109 | get uri() { |
3364 | 110 | return this._uri; |
111 | }, |
|
149 | 112 | set uri(value) { |
468 | 113 | if (typeof value == 'string') |
616 | 114 | this._uri = Utils.makeURI(value); |
115 | else |
|
204 | 116 | this._uri = value; |
117 | }, |
|
118 | ||
119 | // ** {{{ Resource.spec }}} ** |
|
120 | // |
|
121 | // Get the string representation of the URI. |
|
348 | 122 | get spec() { |
632 | 123 | if (this._uri) |
1264 | 124 | return this._uri.spec; |
125 | return null; |
|
126 | }, |
|
127 | ||
128 | // ** {{{ Resource.data }}} ** |
|
129 | // |
|
130 | // Get and set the data encapulated in the resource. |
|
32 | 131 | _data: null, |
65 | 132 | get data() this._data, |
36 | 133 | set data(value) { |
16 | 134 | this._data = value; |
135 | }, |
|
136 | ||
137 | // ** {{{ Resource._createRequest }}} ** |
|
138 | // |
|
139 | // This method returns a new IO Channel for requests to be made |
|
140 | // through. It is never called directly, only {{{_request}}} uses it |
|
141 | // to obtain a request channel. |
|
142 | // |
|
187 | 143 | _createRequest: function Res__createRequest() { |
1240 | 144 | let channel = Svc.IO.newChannel(this.spec, null, null). |
1240 | 145 | QueryInterface(Ci.nsIRequest).QueryInterface(Ci.nsIHttpChannel); |
146 | ||
147 | // Always validate the cache: |
|
1240 | 148 | channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; |
1240 | 149 | channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; |
150 | ||
151 | // Setup a callback to handle bad HTTPS certificates. |
|
620 | 152 | channel.notificationCallbacks = new BadCertListener(); |
153 | ||
154 | // Avoid calling the authorizer more than once |
|
310 | 155 | let headers = this.headers; |
944 | 156 | for (let key in headers) { |
486 | 157 | if (key == 'Authorization') |
1270 | 158 | this._log.trace("HTTP Header " + key + ": ***** (suppressed)"); |
159 | else |
|
420 | 160 | this._log.trace("HTTP Header " + key + ": " + headers[key]); |
2402 | 161 | channel.setRequestHeader(key, headers[key], false); |
162 | } |
|
310 | 163 | return channel; |
164 | }, |
|
165 | ||
340 | 166 | _onProgress: function Res__onProgress(channel) {}, |
167 | ||
168 | // ** {{{ Resource._request }}} ** |
|
169 | // |
|
170 | // Perform a particular HTTP request on the resource. This method |
|
171 | // is never called directly, but is used by the high-level |
|
172 | // {{{get}}}, {{{put}}}, {{{post}}} and {{delete}} methods. |
|
187 | 173 | _request: function Res__request(action, data) { |
310 | 174 | let iter = 0; |
620 | 175 | let channel = this._createRequest(); |
176 | ||
620 | 177 | if ("undefined" != typeof(data)) |
102 | 178 | this._data = data; |
179 | ||
180 | // PUT and POST are trreated differently because |
|
181 | // they have payload data. |
|
1084 | 182 | if ("PUT" == action || "POST" == action) { |
183 | // Convert non-string bodies into JSON |
|
441 | 184 | if (this._data.constructor.toString() != String) |
330 | 185 | this._data = JSON.stringify(this._data); |
186 | ||
756 | 187 | this._log.debug(action + " Length: " + this._data.length); |
630 | 188 | this._log.trace(action + " Body: " + this._data); |
189 | ||
189 | 190 | let type = ('content-type' in this._headers)? |
132 | 191 | this._headers['content-type'] : 'text/plain'; |
192 | ||
189 | 193 | let stream = Cc["@mozilla.org/io/string-input-stream;1"]. |
252 | 194 | createInstance(Ci.nsIStringInputStream); |
504 | 195 | stream.setData(this._data, this._data.length); |
196 | ||
378 | 197 | channel.QueryInterface(Ci.nsIUploadChannel); |
630 | 198 | channel.setUploadStream(stream, type, this._data.length); |
199 | } |
|
200 | ||
201 | // Setup a channel listener so that the actual network operation |
|
202 | // is performed asynchronously. |
|
2170 | 203 | let [chanOpen, chanCb] = Sync.withCb(channel.asyncOpen, channel); |
930 | 204 | let listener = new ChannelListener(chanCb, this._onProgress, this._log); |
465 | 205 | channel.requestMethod = action; |
206 | ||
207 | // The channel listener might get a failure code |
|
155 | 208 | try { |
1083 | 209 | this._data = chanOpen(listener, null); |
210 | } |
|
3 | 211 | catch(ex) { |
212 | // Combine the channel stack with this request stack |
|
4 | 213 | let error = Error(ex.message); |
2 | 214 | let chanStack = []; |
2 | 215 | if (ex.stack) |
11 | 216 | chanStack = ex.stack.trim().split(/\n/).slice(1); |
9 | 217 | let requestStack = error.stack.split(/\n/).slice(1); |
218 | ||
219 | // Strip out the args for the last 2 frames because they're usually HUGE! |
|
18 | 220 | for (let i = 0; i <= 1; i++) |
20 | 221 | requestStack[i] = requestStack[i].replace(/\(".*"\)@/, "(...)@"); |
222 | ||
9 | 223 | error.stack = chanStack.concat(requestStack).join("\n"); |
2 | 224 | throw error; |
225 | } |
|
226 | ||
227 | // Set some default values in-case there's no response header |
|
462 | 228 | let headers = {}; |
308 | 229 | let status = 0; |
308 | 230 | let success = true; |
308 | 231 | try { |
232 | // Read out the response headers if available |
|
462 | 233 | channel.visitResponseHeaders({ |
1520 | 234 | visitHeader: function visitHeader(header, value) { |
5250 | 235 | headers[header.toLowerCase()] = value; |
236 | } |
|
237 | }); |
|
308 | 238 | status = channel.responseStatus; |
308 | 239 | success = channel.requestSucceeded; |
240 | ||
241 | // Log the status of the request |
|
901 | 242 | let mesg = [action, success ? "success" : "fail", status, |
1232 | 243 | channel.URI.spec].join(" "); |
616 | 244 | if (mesg.length > 200) |
368 | 245 | mesg = mesg.substr(0, 200) + "…"; |
924 | 246 | this._log.debug(mesg); |
247 | // Additionally give the full response body when Trace logging |
|
1078 | 248 | if (this._log.level <= Log4Moz.Level.Trace) |
1020 | 249 | this._log.trace(action + " body: " + this._data); |
250 | ||
251 | // this is a server-side safety valve to allow slowing down clients without hurting performance |
|
308 | 252 | if (headers["x-weave-backoff"]) |
317 | 253 | Observers.notify("weave:service:backoff:interval", parseInt(headers["x-weave-backoff"], 10)) |
254 | } |
|
255 | // Got a response but no header; must be cached (use default values) |
|
256 | catch(ex) { |
|
257 | this._log.debug(action + " cached: " + status); |
|
258 | } |
|
259 | ||
616 | 260 | let ret = new String(this._data); |
462 | 261 | ret.headers = headers; |
462 | 262 | ret.status = status; |
462 | 263 | ret.success = success; |
264 | ||
265 | // Make a lazy getter to convert the json response into an object |
|
1095 | 266 | Utils.lazy2(ret, "obj", function() JSON.parse(ret)); |
267 | ||
308 | 268 | return ret; |
269 | }, |
|
270 | ||
271 | // ** {{{ Resource.get }}} ** |
|
272 | // |
|
273 | // Perform an asynchronous HTTP GET for this resource. |
|
274 | // onComplete will be called on completion of the request. |
|
97 | 275 | get: function Res_get() { |
324 | 276 | return this._request("GET"); |
277 | }, |
|
278 | ||
279 | // ** {{{ Resource.get }}} ** |
|
280 | // |
|
281 | // Perform a HTTP PUT for this resource. |
|
64 | 282 | put: function Res_put(data) { |
192 | 283 | return this._request("PUT", data); |
284 | }, |
|
285 | ||
286 | // ** {{{ Resource.post }}} ** |
|
287 | // |
|
288 | // Perform a HTTP POST for this resource. |
|
63 | 289 | post: function Res_post(data) { |
186 | 290 | return this._request("POST", data); |
291 | }, |
|
292 | ||
293 | // ** {{{ Resource.delete }}} ** |
|
294 | // |
|
295 | // Perform a HTTP DELETE for this resource. |
|
107 | 296 | delete: function Res_delete() { |
135 | 297 | return this._request("DELETE"); |
298 | } |
|
299 | }; |
|
300 | ||
301 | // = ChannelListener = |
|
302 | // |
|
303 | // This object implements the {{{nsIStreamListener}}} interface |
|
304 | // and is called as the network operation proceeds. |
|
187 | 305 | function ChannelListener(onComplete, onProgress, logger) { |
465 | 306 | this._onComplete = onComplete; |
465 | 307 | this._onProgress = onProgress; |
465 | 308 | this._log = logger; |
775 | 309 | this.delayAbort(); |
310 | } |
|
32 | 311 | ChannelListener.prototype = { |
312 | // Wait 5 minutes before killing a request |
|
32 | 313 | ABORT_TIMEOUT: 300000, |
314 | ||
187 | 315 | onStartRequest: function Channel_onStartRequest(channel) { |
930 | 316 | channel.QueryInterface(Ci.nsIHttpChannel); |
317 | ||
318 | // Save the latest server timestamp when possible |
|
155 | 319 | try { |
1191 | 320 | Resource.serverTime = channel.getResponseHeader("X-Weave-Timestamp") - 0; |
321 | } |
|
255 | 322 | catch(ex) {} |
323 | ||
1860 | 324 | this._log.trace(channel.requestMethod + " " + channel.URI.spec); |
465 | 325 | this._data = ''; |
775 | 326 | this.delayAbort(); |
327 | }, |
|
328 | ||
187 | 329 | onStopRequest: function Channel_onStopRequest(channel, context, status) { |
330 | // Clear the abort timer now that the channel is done |
|
775 | 331 | this.abortTimer.clear(); |
332 | ||
465 | 333 | if (this._data == '') |
90 | 334 | this._data = null; |
335 | ||
336 | // Throw the failure code name (and stop execution) |
|
930 | 337 | if (!Components.isSuccessCode(status)) |
12 | 338 | this._onComplete.throw(Error(Components.Exception("", status).name)); |
339 | ||
924 | 340 | this._onComplete(this._data); |
341 | }, |
|
342 | ||
269 | 343 | onDataAvailable: function Channel_onDataAvail(req, cb, stream, off, count) { |
711 | 344 | let siStream = Cc["@mozilla.org/scriptableinputstream;1"]. |
948 | 345 | createInstance(Ci.nsIScriptableInputStream); |
1185 | 346 | siStream.init(stream); |
347 | ||
2133 | 348 | this._data += siStream.read(count); |
948 | 349 | this._onProgress(); |
1185 | 350 | this.delayAbort(); |
351 | }, |
|
352 | ||
353 | /** |
|
354 | * Create or push back the abort timer that kills this request |
|
355 | */ |
|
579 | 356 | delayAbort: function delayAbort() { |
4923 | 357 | Utils.delay(this.abortRequest, this.ABORT_TIMEOUT, this, "abortTimer"); |
358 | }, |
|
359 | ||
80 | 360 | abortRequest: function abortRequest() { |
361 | // Ignore any callbacks if we happen to get any now |
|
362 | this.onStopRequest = function() {}; |
|
363 | this.onDataAvailable = function() {}; |
|
364 | this._onComplete.throw(Error("Aborting due to channel inactivity.")); |
|
365 | } |
|
366 | }; |
|
367 | ||
368 | // = BadCertListener = |
|
369 | // |
|
370 | // We use this listener to ignore bad HTTPS |
|
371 | // certificates and continue a request on a network |
|
372 | // channel. Probably not a very smart thing to do, |
|
373 | // but greatly simplifies debugging and is just very |
|
374 | // convenient. |
|
342 | 375 | function BadCertListener() { |
376 | } |
|
32 | 377 | BadCertListener.prototype = { |
398 | 378 | getInterface: function(aIID) { |
1464 | 379 | return this.QueryInterface(aIID); |
380 | }, |
|
381 | ||
553 | 382 | QueryInterface: function(aIID) { |
3647 | 383 | if (aIID.equals(Components.interfaces.nsIBadCertListener2) || |
3647 | 384 | aIID.equals(Components.interfaces.nsIInterfaceRequestor) || |
3647 | 385 | aIID.equals(Components.interfaces.nsISupports)) |
310 | 386 | return this; |
387 | ||
1464 | 388 | throw Components.results.NS_ERROR_NO_INTERFACE; |
389 | }, |
|
390 | ||
80 | 391 | notifyCertProblem: function certProblem(socketInfo, sslStatus, targetHost) { |
392 | // Silently ignore? |
|
393 | let log = Log4Moz.repository.getLogger("Service.CertListener"); |
|
394 | log.level = |
|
395 | Log4Moz.Level[Utils.prefs.getCharPref("log.logger.network.resources")]; |
|
396 | log.debug("Invalid HTTPS certificate encountered, ignoring!"); |
|
397 | ||
398 | return true; |
|
399 | } |
|
16 | 400 | }; |