|
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 file, |
|
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 #ifndef MERGED_COMPARTMENT |
|
6 |
|
7 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; |
|
8 |
|
9 this.EXPORTED_SYMBOLS = [ |
|
10 "RESTRequest", |
|
11 "RESTResponse", |
|
12 "TokenAuthenticatedRESTRequest", |
|
13 ]; |
|
14 |
|
15 #endif |
|
16 |
|
17 Cu.import("resource://gre/modules/Preferences.jsm"); |
|
18 Cu.import("resource://gre/modules/Services.jsm"); |
|
19 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
20 Cu.import("resource://gre/modules/Log.jsm"); |
|
21 Cu.import("resource://services-common/utils.js"); |
|
22 |
|
23 XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils", |
|
24 "resource://services-crypto/utils.js"); |
|
25 |
|
26 const Prefs = new Preferences("services.common.rest."); |
|
27 |
|
28 /** |
|
29 * Single use HTTP requests to RESTish resources. |
|
30 * |
|
31 * @param uri |
|
32 * URI for the request. This can be an nsIURI object or a string |
|
33 * that can be used to create one. An exception will be thrown if |
|
34 * the string is not a valid URI. |
|
35 * |
|
36 * Examples: |
|
37 * |
|
38 * (1) Quick GET request: |
|
39 * |
|
40 * new RESTRequest("http://server/rest/resource").get(function (error) { |
|
41 * if (error) { |
|
42 * // Deal with a network error. |
|
43 * processNetworkErrorCode(error.result); |
|
44 * return; |
|
45 * } |
|
46 * if (!this.response.success) { |
|
47 * // Bail out if we're not getting an HTTP 2xx code. |
|
48 * processHTTPError(this.response.status); |
|
49 * return; |
|
50 * } |
|
51 * processData(this.response.body); |
|
52 * }); |
|
53 * |
|
54 * (2) Quick PUT request (non-string data is automatically JSONified) |
|
55 * |
|
56 * new RESTRequest("http://server/rest/resource").put(data, function (error) { |
|
57 * ... |
|
58 * }); |
|
59 * |
|
60 * (3) Streaming GET |
|
61 * |
|
62 * let request = new RESTRequest("http://server/rest/resource"); |
|
63 * request.setHeader("Accept", "application/newlines"); |
|
64 * request.onComplete = function (error) { |
|
65 * if (error) { |
|
66 * // Deal with a network error. |
|
67 * processNetworkErrorCode(error.result); |
|
68 * return; |
|
69 * } |
|
70 * callbackAfterRequestHasCompleted() |
|
71 * }); |
|
72 * request.onProgress = function () { |
|
73 * if (!this.response.success) { |
|
74 * // Bail out if we're not getting an HTTP 2xx code. |
|
75 * return; |
|
76 * } |
|
77 * // Process body data and reset it so we don't process the same data twice. |
|
78 * processIncrementalData(this.response.body); |
|
79 * this.response.body = ""; |
|
80 * }); |
|
81 * request.get(); |
|
82 */ |
|
83 this.RESTRequest = function RESTRequest(uri) { |
|
84 this.status = this.NOT_SENT; |
|
85 |
|
86 // If we don't have an nsIURI object yet, make one. This will throw if |
|
87 // 'uri' isn't a valid URI string. |
|
88 if (!(uri instanceof Ci.nsIURI)) { |
|
89 uri = Services.io.newURI(uri, null, null); |
|
90 } |
|
91 this.uri = uri; |
|
92 |
|
93 this._headers = {}; |
|
94 this._log = Log.repository.getLogger(this._logName); |
|
95 this._log.level = |
|
96 Log.Level[Prefs.get("log.logger.rest.request")]; |
|
97 } |
|
98 RESTRequest.prototype = { |
|
99 |
|
100 _logName: "Services.Common.RESTRequest", |
|
101 |
|
102 QueryInterface: XPCOMUtils.generateQI([ |
|
103 Ci.nsIBadCertListener2, |
|
104 Ci.nsIInterfaceRequestor, |
|
105 Ci.nsIChannelEventSink |
|
106 ]), |
|
107 |
|
108 /*** Public API: ***/ |
|
109 |
|
110 /** |
|
111 * URI for the request (an nsIURI object). |
|
112 */ |
|
113 uri: null, |
|
114 |
|
115 /** |
|
116 * HTTP method (e.g. "GET") |
|
117 */ |
|
118 method: null, |
|
119 |
|
120 /** |
|
121 * RESTResponse object |
|
122 */ |
|
123 response: null, |
|
124 |
|
125 /** |
|
126 * nsIRequest load flags. Don't do any caching by default. Don't send user |
|
127 * cookies and such over the wire (Bug 644734). |
|
128 */ |
|
129 loadFlags: Ci.nsIRequest.LOAD_BYPASS_CACHE | Ci.nsIRequest.INHIBIT_CACHING | Ci.nsIRequest.LOAD_ANONYMOUS, |
|
130 |
|
131 /** |
|
132 * nsIHttpChannel |
|
133 */ |
|
134 channel: null, |
|
135 |
|
136 /** |
|
137 * Flag to indicate the status of the request. |
|
138 * |
|
139 * One of NOT_SENT, SENT, IN_PROGRESS, COMPLETED, ABORTED. |
|
140 */ |
|
141 status: null, |
|
142 |
|
143 NOT_SENT: 0, |
|
144 SENT: 1, |
|
145 IN_PROGRESS: 2, |
|
146 COMPLETED: 4, |
|
147 ABORTED: 8, |
|
148 |
|
149 /** |
|
150 * HTTP status text of response |
|
151 */ |
|
152 statusText: null, |
|
153 |
|
154 /** |
|
155 * Request timeout (in seconds, though decimal values can be used for |
|
156 * up to millisecond granularity.) |
|
157 * |
|
158 * 0 for no timeout. |
|
159 */ |
|
160 timeout: null, |
|
161 |
|
162 /** |
|
163 * The encoding with which the response to this request must be treated. |
|
164 * If a charset parameter is available in the HTTP Content-Type header for |
|
165 * this response, that will always be used, and this value is ignored. We |
|
166 * default to UTF-8 because that is a reasonable default. |
|
167 */ |
|
168 charset: "utf-8", |
|
169 |
|
170 /** |
|
171 * Called when the request has been completed, including failures and |
|
172 * timeouts. |
|
173 * |
|
174 * @param error |
|
175 * Error that occurred while making the request, null if there |
|
176 * was no error. |
|
177 */ |
|
178 onComplete: function onComplete(error) { |
|
179 }, |
|
180 |
|
181 /** |
|
182 * Called whenever data is being received on the channel. If this throws an |
|
183 * exception, the request is aborted and the exception is passed as the |
|
184 * error to onComplete(). |
|
185 */ |
|
186 onProgress: function onProgress() { |
|
187 }, |
|
188 |
|
189 /** |
|
190 * Set a request header. |
|
191 */ |
|
192 setHeader: function setHeader(name, value) { |
|
193 this._headers[name.toLowerCase()] = value; |
|
194 }, |
|
195 |
|
196 /** |
|
197 * Perform an HTTP GET. |
|
198 * |
|
199 * @param onComplete |
|
200 * Short-circuit way to set the 'onComplete' method. Optional. |
|
201 * @param onProgress |
|
202 * Short-circuit way to set the 'onProgress' method. Optional. |
|
203 * |
|
204 * @return the request object. |
|
205 */ |
|
206 get: function get(onComplete, onProgress) { |
|
207 return this.dispatch("GET", null, onComplete, onProgress); |
|
208 }, |
|
209 |
|
210 /** |
|
211 * Perform an HTTP PUT. |
|
212 * |
|
213 * @param data |
|
214 * Data to be used as the request body. If this isn't a string |
|
215 * it will be JSONified automatically. |
|
216 * @param onComplete |
|
217 * Short-circuit way to set the 'onComplete' method. Optional. |
|
218 * @param onProgress |
|
219 * Short-circuit way to set the 'onProgress' method. Optional. |
|
220 * |
|
221 * @return the request object. |
|
222 */ |
|
223 put: function put(data, onComplete, onProgress) { |
|
224 return this.dispatch("PUT", data, onComplete, onProgress); |
|
225 }, |
|
226 |
|
227 /** |
|
228 * Perform an HTTP POST. |
|
229 * |
|
230 * @param data |
|
231 * Data to be used as the request body. If this isn't a string |
|
232 * it will be JSONified automatically. |
|
233 * @param onComplete |
|
234 * Short-circuit way to set the 'onComplete' method. Optional. |
|
235 * @param onProgress |
|
236 * Short-circuit way to set the 'onProgress' method. Optional. |
|
237 * |
|
238 * @return the request object. |
|
239 */ |
|
240 post: function post(data, onComplete, onProgress) { |
|
241 return this.dispatch("POST", data, onComplete, onProgress); |
|
242 }, |
|
243 |
|
244 /** |
|
245 * Perform an HTTP DELETE. |
|
246 * |
|
247 * @param onComplete |
|
248 * Short-circuit way to set the 'onComplete' method. Optional. |
|
249 * @param onProgress |
|
250 * Short-circuit way to set the 'onProgress' method. Optional. |
|
251 * |
|
252 * @return the request object. |
|
253 */ |
|
254 delete: function delete_(onComplete, onProgress) { |
|
255 return this.dispatch("DELETE", null, onComplete, onProgress); |
|
256 }, |
|
257 |
|
258 /** |
|
259 * Abort an active request. |
|
260 */ |
|
261 abort: function abort() { |
|
262 if (this.status != this.SENT && this.status != this.IN_PROGRESS) { |
|
263 throw "Can only abort a request that has been sent."; |
|
264 } |
|
265 |
|
266 this.status = this.ABORTED; |
|
267 this.channel.cancel(Cr.NS_BINDING_ABORTED); |
|
268 |
|
269 if (this.timeoutTimer) { |
|
270 // Clear the abort timer now that the channel is done. |
|
271 this.timeoutTimer.clear(); |
|
272 } |
|
273 }, |
|
274 |
|
275 /*** Implementation stuff ***/ |
|
276 |
|
277 dispatch: function dispatch(method, data, onComplete, onProgress) { |
|
278 if (this.status != this.NOT_SENT) { |
|
279 throw "Request has already been sent!"; |
|
280 } |
|
281 |
|
282 this.method = method; |
|
283 if (onComplete) { |
|
284 this.onComplete = onComplete; |
|
285 } |
|
286 if (onProgress) { |
|
287 this.onProgress = onProgress; |
|
288 } |
|
289 |
|
290 // Create and initialize HTTP channel. |
|
291 let channel = Services.io.newChannelFromURI(this.uri, null, null) |
|
292 .QueryInterface(Ci.nsIRequest) |
|
293 .QueryInterface(Ci.nsIHttpChannel); |
|
294 this.channel = channel; |
|
295 channel.loadFlags |= this.loadFlags; |
|
296 channel.notificationCallbacks = this; |
|
297 |
|
298 // Set request headers. |
|
299 let headers = this._headers; |
|
300 for (let key in headers) { |
|
301 if (key == 'authorization') { |
|
302 this._log.trace("HTTP Header " + key + ": ***** (suppressed)"); |
|
303 } else { |
|
304 this._log.trace("HTTP Header " + key + ": " + headers[key]); |
|
305 } |
|
306 channel.setRequestHeader(key, headers[key], false); |
|
307 } |
|
308 |
|
309 // Set HTTP request body. |
|
310 if (method == "PUT" || method == "POST") { |
|
311 // Convert non-string bodies into JSON. |
|
312 if (typeof data != "string") { |
|
313 data = JSON.stringify(data); |
|
314 } |
|
315 |
|
316 this._log.debug(method + " Length: " + data.length); |
|
317 if (this._log.level <= Log.Level.Trace) { |
|
318 this._log.trace(method + " Body: " + data); |
|
319 } |
|
320 |
|
321 let stream = Cc["@mozilla.org/io/string-input-stream;1"] |
|
322 .createInstance(Ci.nsIStringInputStream); |
|
323 stream.setData(data, data.length); |
|
324 |
|
325 let type = headers["content-type"] || "text/plain"; |
|
326 channel.QueryInterface(Ci.nsIUploadChannel); |
|
327 channel.setUploadStream(stream, type, data.length); |
|
328 } |
|
329 // We must set this after setting the upload stream, otherwise it |
|
330 // will always be 'PUT'. Yeah, I know. |
|
331 channel.requestMethod = method; |
|
332 |
|
333 // Before opening the channel, set the charset that serves as a hint |
|
334 // as to what the response might be encoded as. |
|
335 channel.contentCharset = this.charset; |
|
336 |
|
337 // Blast off! |
|
338 try { |
|
339 channel.asyncOpen(this, null); |
|
340 } catch (ex) { |
|
341 // asyncOpen can throw in a bunch of cases -- e.g., a forbidden port. |
|
342 this._log.warn("Caught an error in asyncOpen: " + CommonUtils.exceptionStr(ex)); |
|
343 CommonUtils.nextTick(onComplete.bind(this, ex)); |
|
344 } |
|
345 this.status = this.SENT; |
|
346 this.delayTimeout(); |
|
347 return this; |
|
348 }, |
|
349 |
|
350 /** |
|
351 * Create or push back the abort timer that kills this request. |
|
352 */ |
|
353 delayTimeout: function delayTimeout() { |
|
354 if (this.timeout) { |
|
355 CommonUtils.namedTimer(this.abortTimeout, this.timeout * 1000, this, |
|
356 "timeoutTimer"); |
|
357 } |
|
358 }, |
|
359 |
|
360 /** |
|
361 * Abort the request based on a timeout. |
|
362 */ |
|
363 abortTimeout: function abortTimeout() { |
|
364 this.abort(); |
|
365 let error = Components.Exception("Aborting due to channel inactivity.", |
|
366 Cr.NS_ERROR_NET_TIMEOUT); |
|
367 if (!this.onComplete) { |
|
368 this._log.error("Unexpected error: onComplete not defined in " + |
|
369 "abortTimeout.") |
|
370 return; |
|
371 } |
|
372 this.onComplete(error); |
|
373 }, |
|
374 |
|
375 /*** nsIStreamListener ***/ |
|
376 |
|
377 onStartRequest: function onStartRequest(channel) { |
|
378 if (this.status == this.ABORTED) { |
|
379 this._log.trace("Not proceeding with onStartRequest, request was aborted."); |
|
380 return; |
|
381 } |
|
382 |
|
383 try { |
|
384 channel.QueryInterface(Ci.nsIHttpChannel); |
|
385 } catch (ex) { |
|
386 this._log.error("Unexpected error: channel is not a nsIHttpChannel!"); |
|
387 this.status = this.ABORTED; |
|
388 channel.cancel(Cr.NS_BINDING_ABORTED); |
|
389 return; |
|
390 } |
|
391 |
|
392 this.status = this.IN_PROGRESS; |
|
393 |
|
394 this._log.trace("onStartRequest: " + channel.requestMethod + " " + |
|
395 channel.URI.spec); |
|
396 |
|
397 // Create a response object and fill it with some data. |
|
398 let response = this.response = new RESTResponse(); |
|
399 response.request = this; |
|
400 response.body = ""; |
|
401 |
|
402 this.delayTimeout(); |
|
403 }, |
|
404 |
|
405 onStopRequest: function onStopRequest(channel, context, statusCode) { |
|
406 if (this.timeoutTimer) { |
|
407 // Clear the abort timer now that the channel is done. |
|
408 this.timeoutTimer.clear(); |
|
409 } |
|
410 |
|
411 // We don't want to do anything for a request that's already been aborted. |
|
412 if (this.status == this.ABORTED) { |
|
413 this._log.trace("Not proceeding with onStopRequest, request was aborted."); |
|
414 return; |
|
415 } |
|
416 |
|
417 try { |
|
418 channel.QueryInterface(Ci.nsIHttpChannel); |
|
419 } catch (ex) { |
|
420 this._log.error("Unexpected error: channel not nsIHttpChannel!"); |
|
421 this.status = this.ABORTED; |
|
422 return; |
|
423 } |
|
424 this.status = this.COMPLETED; |
|
425 |
|
426 let statusSuccess = Components.isSuccessCode(statusCode); |
|
427 let uri = channel && channel.URI && channel.URI.spec || "<unknown>"; |
|
428 this._log.trace("Channel for " + channel.requestMethod + " " + uri + |
|
429 " returned status code " + statusCode); |
|
430 |
|
431 if (!this.onComplete) { |
|
432 this._log.error("Unexpected error: onComplete not defined in " + |
|
433 "abortRequest."); |
|
434 this.onProgress = null; |
|
435 return; |
|
436 } |
|
437 |
|
438 // Throw the failure code and stop execution. Use Components.Exception() |
|
439 // instead of Error() so the exception is QI-able and can be passed across |
|
440 // XPCOM borders while preserving the status code. |
|
441 if (!statusSuccess) { |
|
442 let message = Components.Exception("", statusCode).name; |
|
443 let error = Components.Exception(message, statusCode); |
|
444 this.onComplete(error); |
|
445 this.onComplete = this.onProgress = null; |
|
446 return; |
|
447 } |
|
448 |
|
449 this._log.debug(this.method + " " + uri + " " + this.response.status); |
|
450 |
|
451 // Additionally give the full response body when Trace logging. |
|
452 if (this._log.level <= Log.Level.Trace) { |
|
453 this._log.trace(this.method + " body: " + this.response.body); |
|
454 } |
|
455 |
|
456 delete this._inputStream; |
|
457 |
|
458 this.onComplete(null); |
|
459 this.onComplete = this.onProgress = null; |
|
460 }, |
|
461 |
|
462 onDataAvailable: function onDataAvailable(channel, cb, stream, off, count) { |
|
463 // We get an nsIRequest, which doesn't have contentCharset. |
|
464 try { |
|
465 channel.QueryInterface(Ci.nsIHttpChannel); |
|
466 } catch (ex) { |
|
467 this._log.error("Unexpected error: channel not nsIHttpChannel!"); |
|
468 this.abort(); |
|
469 |
|
470 if (this.onComplete) { |
|
471 this.onComplete(ex); |
|
472 } |
|
473 |
|
474 this.onComplete = this.onProgress = null; |
|
475 return; |
|
476 } |
|
477 |
|
478 if (channel.contentCharset) { |
|
479 this.response.charset = channel.contentCharset; |
|
480 |
|
481 if (!this._converterStream) { |
|
482 this._converterStream = Cc["@mozilla.org/intl/converter-input-stream;1"] |
|
483 .createInstance(Ci.nsIConverterInputStream); |
|
484 } |
|
485 |
|
486 this._converterStream.init(stream, channel.contentCharset, 0, |
|
487 this._converterStream.DEFAULT_REPLACEMENT_CHARACTER); |
|
488 |
|
489 try { |
|
490 let str = {}; |
|
491 let num = this._converterStream.readString(count, str); |
|
492 if (num != 0) { |
|
493 this.response.body += str.value; |
|
494 } |
|
495 } catch (ex) { |
|
496 this._log.warn("Exception thrown reading " + count + " bytes from " + |
|
497 "the channel."); |
|
498 this._log.warn(CommonUtils.exceptionStr(ex)); |
|
499 throw ex; |
|
500 } |
|
501 } else { |
|
502 this.response.charset = null; |
|
503 |
|
504 if (!this._inputStream) { |
|
505 this._inputStream = Cc["@mozilla.org/scriptableinputstream;1"] |
|
506 .createInstance(Ci.nsIScriptableInputStream); |
|
507 } |
|
508 |
|
509 this._inputStream.init(stream); |
|
510 |
|
511 this.response.body += this._inputStream.read(count); |
|
512 } |
|
513 |
|
514 try { |
|
515 this.onProgress(); |
|
516 } catch (ex) { |
|
517 this._log.warn("Got exception calling onProgress handler, aborting " + |
|
518 this.method + " " + channel.URI.spec); |
|
519 this._log.debug("Exception: " + CommonUtils.exceptionStr(ex)); |
|
520 this.abort(); |
|
521 |
|
522 if (!this.onComplete) { |
|
523 this._log.error("Unexpected error: onComplete not defined in " + |
|
524 "onDataAvailable."); |
|
525 this.onProgress = null; |
|
526 return; |
|
527 } |
|
528 |
|
529 this.onComplete(ex); |
|
530 this.onComplete = this.onProgress = null; |
|
531 return; |
|
532 } |
|
533 |
|
534 this.delayTimeout(); |
|
535 }, |
|
536 |
|
537 /*** nsIInterfaceRequestor ***/ |
|
538 |
|
539 getInterface: function(aIID) { |
|
540 return this.QueryInterface(aIID); |
|
541 }, |
|
542 |
|
543 /*** nsIBadCertListener2 ***/ |
|
544 |
|
545 notifyCertProblem: function notifyCertProblem(socketInfo, sslStatus, targetHost) { |
|
546 this._log.warn("Invalid HTTPS certificate encountered!"); |
|
547 // Suppress invalid HTTPS certificate warnings in the UI. |
|
548 // (The request will still fail.) |
|
549 return true; |
|
550 }, |
|
551 |
|
552 /** |
|
553 * Returns true if headers from the old channel should be |
|
554 * copied to the new channel. Invoked when a channel redirect |
|
555 * is in progress. |
|
556 */ |
|
557 shouldCopyOnRedirect: function shouldCopyOnRedirect(oldChannel, newChannel, flags) { |
|
558 let isInternal = !!(flags & Ci.nsIChannelEventSink.REDIRECT_INTERNAL); |
|
559 let isSameURI = newChannel.URI.equals(oldChannel.URI); |
|
560 this._log.debug("Channel redirect: " + oldChannel.URI.spec + ", " + |
|
561 newChannel.URI.spec + ", internal = " + isInternal); |
|
562 return isInternal && isSameURI; |
|
563 }, |
|
564 |
|
565 /*** nsIChannelEventSink ***/ |
|
566 asyncOnChannelRedirect: |
|
567 function asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) { |
|
568 |
|
569 try { |
|
570 newChannel.QueryInterface(Ci.nsIHttpChannel); |
|
571 } catch (ex) { |
|
572 this._log.error("Unexpected error: channel not nsIHttpChannel!"); |
|
573 callback.onRedirectVerifyCallback(Cr.NS_ERROR_NO_INTERFACE); |
|
574 return; |
|
575 } |
|
576 |
|
577 // For internal redirects, copy the headers that our caller set. |
|
578 try { |
|
579 if (this.shouldCopyOnRedirect(oldChannel, newChannel, flags)) { |
|
580 this._log.trace("Copying headers for safe internal redirect."); |
|
581 for (let key in this._headers) { |
|
582 newChannel.setRequestHeader(key, this._headers[key], false); |
|
583 } |
|
584 } |
|
585 } catch (ex) { |
|
586 this._log.error("Error copying headers: " + CommonUtils.exceptionStr(ex)); |
|
587 } |
|
588 |
|
589 this.channel = newChannel; |
|
590 |
|
591 // We let all redirects proceed. |
|
592 callback.onRedirectVerifyCallback(Cr.NS_OK); |
|
593 } |
|
594 }; |
|
595 |
|
596 /** |
|
597 * Response object for a RESTRequest. This will be created automatically by |
|
598 * the RESTRequest. |
|
599 */ |
|
600 this.RESTResponse = function RESTResponse() { |
|
601 this._log = Log.repository.getLogger(this._logName); |
|
602 this._log.level = |
|
603 Log.Level[Prefs.get("log.logger.rest.response")]; |
|
604 } |
|
605 RESTResponse.prototype = { |
|
606 |
|
607 _logName: "Sync.RESTResponse", |
|
608 |
|
609 /** |
|
610 * Corresponding REST request |
|
611 */ |
|
612 request: null, |
|
613 |
|
614 /** |
|
615 * HTTP status code |
|
616 */ |
|
617 get status() { |
|
618 let status; |
|
619 try { |
|
620 status = this.request.channel.responseStatus; |
|
621 } catch (ex) { |
|
622 this._log.debug("Caught exception fetching HTTP status code:" + |
|
623 CommonUtils.exceptionStr(ex)); |
|
624 return null; |
|
625 } |
|
626 delete this.status; |
|
627 return this.status = status; |
|
628 }, |
|
629 |
|
630 /** |
|
631 * HTTP status text |
|
632 */ |
|
633 get statusText() { |
|
634 let statusText; |
|
635 try { |
|
636 statusText = this.request.channel.responseStatusText; |
|
637 } catch (ex) { |
|
638 this._log.debug("Caught exception fetching HTTP status text:" + |
|
639 CommonUtils.exceptionStr(ex)); |
|
640 return null; |
|
641 } |
|
642 delete this.statusText; |
|
643 return this.statusText = statusText; |
|
644 }, |
|
645 |
|
646 /** |
|
647 * Boolean flag that indicates whether the HTTP status code is 2xx or not. |
|
648 */ |
|
649 get success() { |
|
650 let success; |
|
651 try { |
|
652 success = this.request.channel.requestSucceeded; |
|
653 } catch (ex) { |
|
654 this._log.debug("Caught exception fetching HTTP success flag:" + |
|
655 CommonUtils.exceptionStr(ex)); |
|
656 return null; |
|
657 } |
|
658 delete this.success; |
|
659 return this.success = success; |
|
660 }, |
|
661 |
|
662 /** |
|
663 * Object containing HTTP headers (keyed as lower case) |
|
664 */ |
|
665 get headers() { |
|
666 let headers = {}; |
|
667 try { |
|
668 this._log.trace("Processing response headers."); |
|
669 let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel); |
|
670 channel.visitResponseHeaders(function (header, value) { |
|
671 headers[header.toLowerCase()] = value; |
|
672 }); |
|
673 } catch (ex) { |
|
674 this._log.debug("Caught exception processing response headers:" + |
|
675 CommonUtils.exceptionStr(ex)); |
|
676 return null; |
|
677 } |
|
678 |
|
679 delete this.headers; |
|
680 return this.headers = headers; |
|
681 }, |
|
682 |
|
683 /** |
|
684 * HTTP body (string) |
|
685 */ |
|
686 body: null |
|
687 |
|
688 }; |
|
689 |
|
690 /** |
|
691 * Single use MAC authenticated HTTP requests to RESTish resources. |
|
692 * |
|
693 * @param uri |
|
694 * URI going to the RESTRequest constructor. |
|
695 * @param authToken |
|
696 * (Object) An auth token of the form {id: (string), key: (string)} |
|
697 * from which the MAC Authentication header for this request will be |
|
698 * derived. A token as obtained from |
|
699 * TokenServerClient.getTokenFromBrowserIDAssertion is accepted. |
|
700 * @param extra |
|
701 * (Object) Optional extra parameters. Valid keys are: nonce_bytes, ts, |
|
702 * nonce, and ext. See CrytoUtils.computeHTTPMACSHA1 for information on |
|
703 * the purpose of these values. |
|
704 */ |
|
705 this.TokenAuthenticatedRESTRequest = |
|
706 function TokenAuthenticatedRESTRequest(uri, authToken, extra) { |
|
707 RESTRequest.call(this, uri); |
|
708 this.authToken = authToken; |
|
709 this.extra = extra || {}; |
|
710 } |
|
711 TokenAuthenticatedRESTRequest.prototype = { |
|
712 __proto__: RESTRequest.prototype, |
|
713 |
|
714 dispatch: function dispatch(method, data, onComplete, onProgress) { |
|
715 let sig = CryptoUtils.computeHTTPMACSHA1( |
|
716 this.authToken.id, this.authToken.key, method, this.uri, this.extra |
|
717 ); |
|
718 |
|
719 this.setHeader("Authorization", sig.getHeader()); |
|
720 |
|
721 return RESTRequest.prototype.dispatch.call( |
|
722 this, method, data, onComplete, onProgress |
|
723 ); |
|
724 }, |
|
725 }; |
|
726 |