resource.js

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
};