2012-04-06 10:26:06 +04:00
|
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
|
2015-10-07 15:03:21 +03:00
|
|
|
var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
2012-04-06 10:26:06 +04:00
|
|
|
|
2012-10-31 20:13:28 +04:00
|
|
|
this.EXPORTED_SYMBOLS = [
|
2012-04-18 02:32:33 +04:00
|
|
|
"RESTRequest",
|
|
|
|
"RESTResponse",
|
2014-01-24 06:04:38 +04:00
|
|
|
"TokenAuthenticatedRESTRequest",
|
2012-04-18 02:32:33 +04:00
|
|
|
];
|
2012-04-06 10:26:06 +04:00
|
|
|
|
2013-04-15 23:45:37 +04:00
|
|
|
Cu.import("resource://gre/modules/Preferences.jsm");
|
2012-04-06 10:26:06 +04:00
|
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
2016-01-15 22:39:12 +03:00
|
|
|
Cu.import("resource://gre/modules/NetUtil.jsm");
|
2012-04-06 10:26:06 +04:00
|
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
2013-08-26 22:55:58 +04:00
|
|
|
Cu.import("resource://gre/modules/Log.jsm");
|
2012-04-06 10:26:06 +04:00
|
|
|
Cu.import("resource://services-common/utils.js");
|
|
|
|
|
2013-01-30 19:07:22 +04:00
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "CryptoUtils",
|
|
|
|
"resource://services-crypto/utils.js");
|
|
|
|
|
2016-03-10 09:20:29 +03:00
|
|
|
const Prefs = new Preferences("services.common.");
|
2012-04-06 10:26:06 +04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Single use HTTP requests to RESTish resources.
|
|
|
|
*
|
|
|
|
* @param uri
|
|
|
|
* URI for the request. This can be an nsIURI object or a string
|
|
|
|
* that can be used to create one. An exception will be thrown if
|
|
|
|
* the string is not a valid URI.
|
|
|
|
*
|
|
|
|
* Examples:
|
|
|
|
*
|
|
|
|
* (1) Quick GET request:
|
|
|
|
*
|
|
|
|
* new RESTRequest("http://server/rest/resource").get(function (error) {
|
|
|
|
* if (error) {
|
|
|
|
* // Deal with a network error.
|
|
|
|
* processNetworkErrorCode(error.result);
|
|
|
|
* return;
|
|
|
|
* }
|
|
|
|
* if (!this.response.success) {
|
|
|
|
* // Bail out if we're not getting an HTTP 2xx code.
|
|
|
|
* processHTTPError(this.response.status);
|
|
|
|
* return;
|
|
|
|
* }
|
|
|
|
* processData(this.response.body);
|
|
|
|
* });
|
|
|
|
*
|
|
|
|
* (2) Quick PUT request (non-string data is automatically JSONified)
|
|
|
|
*
|
|
|
|
* new RESTRequest("http://server/rest/resource").put(data, function (error) {
|
|
|
|
* ...
|
|
|
|
* });
|
|
|
|
*
|
|
|
|
* (3) Streaming GET
|
|
|
|
*
|
|
|
|
* let request = new RESTRequest("http://server/rest/resource");
|
|
|
|
* request.setHeader("Accept", "application/newlines");
|
|
|
|
* request.onComplete = function (error) {
|
|
|
|
* if (error) {
|
|
|
|
* // Deal with a network error.
|
|
|
|
* processNetworkErrorCode(error.result);
|
|
|
|
* return;
|
|
|
|
* }
|
|
|
|
* callbackAfterRequestHasCompleted()
|
|
|
|
* });
|
|
|
|
* request.onProgress = function () {
|
|
|
|
* if (!this.response.success) {
|
|
|
|
* // Bail out if we're not getting an HTTP 2xx code.
|
|
|
|
* return;
|
|
|
|
* }
|
|
|
|
* // Process body data and reset it so we don't process the same data twice.
|
|
|
|
* processIncrementalData(this.response.body);
|
|
|
|
* this.response.body = "";
|
|
|
|
* });
|
|
|
|
* request.get();
|
|
|
|
*/
|
2012-10-31 20:13:28 +04:00
|
|
|
this.RESTRequest = function RESTRequest(uri) {
|
2012-04-06 10:26:06 +04:00
|
|
|
this.status = this.NOT_SENT;
|
|
|
|
|
|
|
|
// If we don't have an nsIURI object yet, make one. This will throw if
|
|
|
|
// 'uri' isn't a valid URI string.
|
|
|
|
if (!(uri instanceof Ci.nsIURI)) {
|
2017-01-09 22:27:26 +03:00
|
|
|
uri = Services.io.newURI(uri);
|
2012-04-06 10:26:06 +04:00
|
|
|
}
|
|
|
|
this.uri = uri;
|
|
|
|
|
|
|
|
this._headers = {};
|
2013-08-26 22:55:58 +04:00
|
|
|
this._log = Log.repository.getLogger(this._logName);
|
2012-04-06 10:26:06 +04:00
|
|
|
this._log.level =
|
2013-08-26 22:55:58 +04:00
|
|
|
Log.Level[Prefs.get("log.logger.rest.request")];
|
2012-04-06 10:26:06 +04:00
|
|
|
}
|
|
|
|
RESTRequest.prototype = {
|
|
|
|
|
|
|
|
_logName: "Services.Common.RESTRequest",
|
|
|
|
|
|
|
|
QueryInterface: XPCOMUtils.generateQI([
|
|
|
|
Ci.nsIBadCertListener2,
|
|
|
|
Ci.nsIInterfaceRequestor,
|
|
|
|
Ci.nsIChannelEventSink
|
|
|
|
]),
|
|
|
|
|
2017-01-10 20:08:32 +03:00
|
|
|
/** Public API: **/
|
2012-04-06 10:26:06 +04:00
|
|
|
|
2016-02-26 07:46:30 +03:00
|
|
|
/**
|
|
|
|
* A constant boolean that indicates whether this object will automatically
|
|
|
|
* utf-8 encode request bodies passed as an object. Used for feature detection
|
|
|
|
* so, eg, loop can use the same source code for old and new Firefox versions.
|
|
|
|
*/
|
|
|
|
willUTF8EncodeObjectRequests: true,
|
|
|
|
|
2012-04-06 10:26:06 +04:00
|
|
|
/**
|
|
|
|
* URI for the request (an nsIURI object).
|
|
|
|
*/
|
|
|
|
uri: null,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* HTTP method (e.g. "GET")
|
|
|
|
*/
|
|
|
|
method: null,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* RESTResponse object
|
|
|
|
*/
|
|
|
|
response: null,
|
|
|
|
|
|
|
|
/**
|
2012-09-13 02:08:07 +04:00
|
|
|
* nsIRequest load flags. Don't do any caching by default. Don't send user
|
|
|
|
* cookies and such over the wire (Bug 644734).
|
2012-04-06 10:26:06 +04:00
|
|
|
*/
|
2012-09-13 02:08:07 +04:00
|
|
|
loadFlags: Ci.nsIRequest.LOAD_BYPASS_CACHE | Ci.nsIRequest.INHIBIT_CACHING | Ci.nsIRequest.LOAD_ANONYMOUS,
|
2012-04-06 10:26:06 +04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* nsIHttpChannel
|
|
|
|
*/
|
|
|
|
channel: null,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Flag to indicate the status of the request.
|
|
|
|
*
|
|
|
|
* One of NOT_SENT, SENT, IN_PROGRESS, COMPLETED, ABORTED.
|
|
|
|
*/
|
|
|
|
status: null,
|
|
|
|
|
|
|
|
NOT_SENT: 0,
|
|
|
|
SENT: 1,
|
|
|
|
IN_PROGRESS: 2,
|
|
|
|
COMPLETED: 4,
|
|
|
|
ABORTED: 8,
|
|
|
|
|
2014-01-24 06:04:38 +04:00
|
|
|
/**
|
|
|
|
* HTTP status text of response
|
|
|
|
*/
|
|
|
|
statusText: null,
|
|
|
|
|
2012-04-06 10:26:06 +04:00
|
|
|
/**
|
|
|
|
* Request timeout (in seconds, though decimal values can be used for
|
|
|
|
* up to millisecond granularity.)
|
|
|
|
*
|
|
|
|
* 0 for no timeout.
|
|
|
|
*/
|
|
|
|
timeout: null,
|
|
|
|
|
2012-07-14 03:52:31 +04:00
|
|
|
/**
|
|
|
|
* The encoding with which the response to this request must be treated.
|
|
|
|
* If a charset parameter is available in the HTTP Content-Type header for
|
|
|
|
* this response, that will always be used, and this value is ignored. We
|
|
|
|
* default to UTF-8 because that is a reasonable default.
|
|
|
|
*/
|
|
|
|
charset: "utf-8",
|
|
|
|
|
2012-04-06 10:26:06 +04:00
|
|
|
/**
|
|
|
|
* Called when the request has been completed, including failures and
|
|
|
|
* timeouts.
|
|
|
|
*
|
|
|
|
* @param error
|
|
|
|
* Error that occurred while making the request, null if there
|
|
|
|
* was no error.
|
|
|
|
*/
|
|
|
|
onComplete: function onComplete(error) {
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Called whenever data is being received on the channel. If this throws an
|
|
|
|
* exception, the request is aborted and the exception is passed as the
|
|
|
|
* error to onComplete().
|
|
|
|
*/
|
|
|
|
onProgress: function onProgress() {
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set a request header.
|
|
|
|
*/
|
|
|
|
setHeader: function setHeader(name, value) {
|
|
|
|
this._headers[name.toLowerCase()] = value;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Perform an HTTP GET.
|
|
|
|
*
|
|
|
|
* @param onComplete
|
|
|
|
* Short-circuit way to set the 'onComplete' method. Optional.
|
|
|
|
* @param onProgress
|
|
|
|
* Short-circuit way to set the 'onProgress' method. Optional.
|
|
|
|
*
|
|
|
|
* @return the request object.
|
|
|
|
*/
|
|
|
|
get: function get(onComplete, onProgress) {
|
|
|
|
return this.dispatch("GET", null, onComplete, onProgress);
|
|
|
|
},
|
|
|
|
|
2014-11-18 01:12:26 +03:00
|
|
|
/**
|
|
|
|
* Perform an HTTP PATCH.
|
|
|
|
*
|
|
|
|
* @param data
|
|
|
|
* Data to be used as the request body. If this isn't a string
|
|
|
|
* it will be JSONified automatically.
|
|
|
|
* @param onComplete
|
|
|
|
* Short-circuit way to set the 'onComplete' method. Optional.
|
|
|
|
* @param onProgress
|
|
|
|
* Short-circuit way to set the 'onProgress' method. Optional.
|
|
|
|
*
|
|
|
|
* @return the request object.
|
|
|
|
*/
|
|
|
|
patch: function patch(data, onComplete, onProgress) {
|
|
|
|
return this.dispatch("PATCH", data, onComplete, onProgress);
|
|
|
|
},
|
|
|
|
|
2012-04-06 10:26:06 +04:00
|
|
|
/**
|
|
|
|
* Perform an HTTP PUT.
|
|
|
|
*
|
|
|
|
* @param data
|
|
|
|
* Data to be used as the request body. If this isn't a string
|
|
|
|
* it will be JSONified automatically.
|
|
|
|
* @param onComplete
|
|
|
|
* Short-circuit way to set the 'onComplete' method. Optional.
|
|
|
|
* @param onProgress
|
|
|
|
* Short-circuit way to set the 'onProgress' method. Optional.
|
|
|
|
*
|
|
|
|
* @return the request object.
|
|
|
|
*/
|
|
|
|
put: function put(data, onComplete, onProgress) {
|
|
|
|
return this.dispatch("PUT", data, onComplete, onProgress);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Perform an HTTP POST.
|
|
|
|
*
|
|
|
|
* @param data
|
|
|
|
* Data to be used as the request body. If this isn't a string
|
|
|
|
* it will be JSONified automatically.
|
|
|
|
* @param onComplete
|
|
|
|
* Short-circuit way to set the 'onComplete' method. Optional.
|
|
|
|
* @param onProgress
|
|
|
|
* Short-circuit way to set the 'onProgress' method. Optional.
|
|
|
|
*
|
|
|
|
* @return the request object.
|
|
|
|
*/
|
|
|
|
post: function post(data, onComplete, onProgress) {
|
|
|
|
return this.dispatch("POST", data, onComplete, onProgress);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Perform an HTTP DELETE.
|
|
|
|
*
|
|
|
|
* @param onComplete
|
|
|
|
* Short-circuit way to set the 'onComplete' method. Optional.
|
|
|
|
* @param onProgress
|
|
|
|
* Short-circuit way to set the 'onProgress' method. Optional.
|
|
|
|
*
|
|
|
|
* @return the request object.
|
|
|
|
*/
|
|
|
|
delete: function delete_(onComplete, onProgress) {
|
|
|
|
return this.dispatch("DELETE", null, onComplete, onProgress);
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Abort an active request.
|
|
|
|
*/
|
|
|
|
abort: function abort() {
|
|
|
|
if (this.status != this.SENT && this.status != this.IN_PROGRESS) {
|
|
|
|
throw "Can only abort a request that has been sent.";
|
|
|
|
}
|
|
|
|
|
|
|
|
this.status = this.ABORTED;
|
|
|
|
this.channel.cancel(Cr.NS_BINDING_ABORTED);
|
|
|
|
|
|
|
|
if (this.timeoutTimer) {
|
|
|
|
// Clear the abort timer now that the channel is done.
|
|
|
|
this.timeoutTimer.clear();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2017-01-10 20:08:32 +03:00
|
|
|
/** Implementation stuff **/
|
2012-04-06 10:26:06 +04:00
|
|
|
|
|
|
|
dispatch: function dispatch(method, data, onComplete, onProgress) {
|
|
|
|
if (this.status != this.NOT_SENT) {
|
|
|
|
throw "Request has already been sent!";
|
|
|
|
}
|
|
|
|
|
|
|
|
this.method = method;
|
|
|
|
if (onComplete) {
|
|
|
|
this.onComplete = onComplete;
|
|
|
|
}
|
|
|
|
if (onProgress) {
|
|
|
|
this.onProgress = onProgress;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create and initialize HTTP channel.
|
2016-01-15 22:39:12 +03:00
|
|
|
let channel = NetUtil.newChannel({uri: this.uri, loadUsingSystemPrincipal: true})
|
|
|
|
.QueryInterface(Ci.nsIRequest)
|
|
|
|
.QueryInterface(Ci.nsIHttpChannel);
|
2012-04-06 10:26:06 +04:00
|
|
|
this.channel = channel;
|
|
|
|
channel.loadFlags |= this.loadFlags;
|
|
|
|
channel.notificationCallbacks = this;
|
|
|
|
|
2016-03-10 09:20:29 +03:00
|
|
|
this._log.debug(`${method} request to ${this.uri.spec}`);
|
2012-04-06 10:26:06 +04:00
|
|
|
// Set request headers.
|
|
|
|
let headers = this._headers;
|
|
|
|
for (let key in headers) {
|
2017-01-17 18:48:17 +03:00
|
|
|
if (key == "authorization") {
|
2012-04-06 10:26:06 +04:00
|
|
|
this._log.trace("HTTP Header " + key + ": ***** (suppressed)");
|
|
|
|
} else {
|
|
|
|
this._log.trace("HTTP Header " + key + ": " + headers[key]);
|
|
|
|
}
|
|
|
|
channel.setRequestHeader(key, headers[key], false);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set HTTP request body.
|
2014-11-18 01:12:26 +03:00
|
|
|
if (method == "PUT" || method == "POST" || method == "PATCH") {
|
2016-02-26 07:46:30 +03:00
|
|
|
// Convert non-string bodies into JSON with utf-8 encoding. If a string
|
|
|
|
// is passed we assume they've already encoded it.
|
|
|
|
let contentType = headers["content-type"];
|
2012-04-06 10:26:06 +04:00
|
|
|
if (typeof data != "string") {
|
|
|
|
data = JSON.stringify(data);
|
2016-02-26 07:46:30 +03:00
|
|
|
if (!contentType) {
|
|
|
|
contentType = "application/json";
|
|
|
|
}
|
|
|
|
if (!contentType.includes("charset")) {
|
|
|
|
data = CommonUtils.encodeUTF8(data);
|
|
|
|
contentType += "; charset=utf-8";
|
|
|
|
} else {
|
|
|
|
// If someone handed us an object but also a custom content-type
|
|
|
|
// it's probably confused. We could go to even further lengths to
|
|
|
|
// respect it, but this shouldn't happen in practice.
|
|
|
|
Cu.reportError("rest.js found an object to JSON.stringify but also a " +
|
|
|
|
"content-type header with a charset specification. " +
|
|
|
|
"This probably isn't going to do what you expect");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!contentType) {
|
|
|
|
contentType = "text/plain";
|
2012-04-06 10:26:06 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
this._log.debug(method + " Length: " + data.length);
|
2013-08-26 22:55:58 +04:00
|
|
|
if (this._log.level <= Log.Level.Trace) {
|
2012-04-06 10:26:06 +04:00
|
|
|
this._log.trace(method + " Body: " + data);
|
|
|
|
}
|
|
|
|
|
|
|
|
let stream = Cc["@mozilla.org/io/string-input-stream;1"]
|
|
|
|
.createInstance(Ci.nsIStringInputStream);
|
|
|
|
stream.setData(data, data.length);
|
|
|
|
|
|
|
|
channel.QueryInterface(Ci.nsIUploadChannel);
|
2016-02-26 07:46:30 +03:00
|
|
|
channel.setUploadStream(stream, contentType, data.length);
|
2012-04-06 10:26:06 +04:00
|
|
|
}
|
|
|
|
// We must set this after setting the upload stream, otherwise it
|
|
|
|
// will always be 'PUT'. Yeah, I know.
|
|
|
|
channel.requestMethod = method;
|
|
|
|
|
2012-07-14 03:52:31 +04:00
|
|
|
// Before opening the channel, set the charset that serves as a hint
|
|
|
|
// as to what the response might be encoded as.
|
|
|
|
channel.contentCharset = this.charset;
|
|
|
|
|
2012-04-06 10:26:06 +04:00
|
|
|
// Blast off!
|
2013-01-03 03:40:03 +04:00
|
|
|
try {
|
2016-01-15 22:39:12 +03:00
|
|
|
channel.asyncOpen2(this);
|
2013-01-03 03:40:03 +04:00
|
|
|
} catch (ex) {
|
|
|
|
// asyncOpen can throw in a bunch of cases -- e.g., a forbidden port.
|
2015-12-16 11:43:00 +03:00
|
|
|
this._log.warn("Caught an error in asyncOpen", ex);
|
2013-01-03 03:40:03 +04:00
|
|
|
CommonUtils.nextTick(onComplete.bind(this, ex));
|
|
|
|
}
|
2012-04-06 10:26:06 +04:00
|
|
|
this.status = this.SENT;
|
|
|
|
this.delayTimeout();
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create or push back the abort timer that kills this request.
|
|
|
|
*/
|
|
|
|
delayTimeout: function delayTimeout() {
|
|
|
|
if (this.timeout) {
|
|
|
|
CommonUtils.namedTimer(this.abortTimeout, this.timeout * 1000, this,
|
|
|
|
"timeoutTimer");
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Abort the request based on a timeout.
|
|
|
|
*/
|
|
|
|
abortTimeout: function abortTimeout() {
|
|
|
|
this.abort();
|
|
|
|
let error = Components.Exception("Aborting due to channel inactivity.",
|
|
|
|
Cr.NS_ERROR_NET_TIMEOUT);
|
|
|
|
if (!this.onComplete) {
|
|
|
|
this._log.error("Unexpected error: onComplete not defined in " +
|
2014-11-18 01:12:26 +03:00
|
|
|
"abortTimeout.");
|
2012-04-06 10:26:06 +04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.onComplete(error);
|
|
|
|
},
|
|
|
|
|
2017-01-10 20:08:32 +03:00
|
|
|
/** nsIStreamListener **/
|
2012-04-06 10:26:06 +04:00
|
|
|
|
|
|
|
onStartRequest: function onStartRequest(channel) {
|
|
|
|
if (this.status == this.ABORTED) {
|
|
|
|
this._log.trace("Not proceeding with onStartRequest, request was aborted.");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
channel.QueryInterface(Ci.nsIHttpChannel);
|
|
|
|
} catch (ex) {
|
|
|
|
this._log.error("Unexpected error: channel is not a nsIHttpChannel!");
|
|
|
|
this.status = this.ABORTED;
|
|
|
|
channel.cancel(Cr.NS_BINDING_ABORTED);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.status = this.IN_PROGRESS;
|
|
|
|
|
|
|
|
this._log.trace("onStartRequest: " + channel.requestMethod + " " +
|
|
|
|
channel.URI.spec);
|
|
|
|
|
|
|
|
// Create a response object and fill it with some data.
|
|
|
|
let response = this.response = new RESTResponse();
|
|
|
|
response.request = this;
|
|
|
|
response.body = "";
|
|
|
|
|
|
|
|
this.delayTimeout();
|
|
|
|
},
|
|
|
|
|
|
|
|
onStopRequest: function onStopRequest(channel, context, statusCode) {
|
|
|
|
if (this.timeoutTimer) {
|
|
|
|
// Clear the abort timer now that the channel is done.
|
|
|
|
this.timeoutTimer.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
// We don't want to do anything for a request that's already been aborted.
|
|
|
|
if (this.status == this.ABORTED) {
|
|
|
|
this._log.trace("Not proceeding with onStopRequest, request was aborted.");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
channel.QueryInterface(Ci.nsIHttpChannel);
|
|
|
|
} catch (ex) {
|
|
|
|
this._log.error("Unexpected error: channel not nsIHttpChannel!");
|
|
|
|
this.status = this.ABORTED;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.status = this.COMPLETED;
|
|
|
|
|
|
|
|
let statusSuccess = Components.isSuccessCode(statusCode);
|
|
|
|
let uri = channel && channel.URI && channel.URI.spec || "<unknown>";
|
|
|
|
this._log.trace("Channel for " + channel.requestMethod + " " + uri +
|
|
|
|
" returned status code " + statusCode);
|
|
|
|
|
|
|
|
if (!this.onComplete) {
|
|
|
|
this._log.error("Unexpected error: onComplete not defined in " +
|
|
|
|
"abortRequest.");
|
|
|
|
this.onProgress = null;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Throw the failure code and stop execution. Use Components.Exception()
|
|
|
|
// instead of Error() so the exception is QI-able and can be passed across
|
|
|
|
// XPCOM borders while preserving the status code.
|
|
|
|
if (!statusSuccess) {
|
|
|
|
let message = Components.Exception("", statusCode).name;
|
|
|
|
let error = Components.Exception(message, statusCode);
|
2015-01-28 02:11:08 +03:00
|
|
|
this._log.debug(this.method + " " + uri + " failed: " + statusCode + " - " + message);
|
2012-04-06 10:26:06 +04:00
|
|
|
this.onComplete(error);
|
|
|
|
this.onComplete = this.onProgress = null;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this._log.debug(this.method + " " + uri + " " + this.response.status);
|
|
|
|
|
|
|
|
// Additionally give the full response body when Trace logging.
|
2013-08-26 22:55:58 +04:00
|
|
|
if (this._log.level <= Log.Level.Trace) {
|
2012-04-06 10:26:06 +04:00
|
|
|
this._log.trace(this.method + " body: " + this.response.body);
|
|
|
|
}
|
|
|
|
|
|
|
|
delete this._inputStream;
|
|
|
|
|
|
|
|
this.onComplete(null);
|
|
|
|
this.onComplete = this.onProgress = null;
|
|
|
|
},
|
|
|
|
|
2012-07-14 03:52:31 +04:00
|
|
|
onDataAvailable: function onDataAvailable(channel, cb, stream, off, count) {
|
|
|
|
// We get an nsIRequest, which doesn't have contentCharset.
|
2012-04-06 10:26:06 +04:00
|
|
|
try {
|
2012-07-14 03:52:31 +04:00
|
|
|
channel.QueryInterface(Ci.nsIHttpChannel);
|
|
|
|
} catch (ex) {
|
|
|
|
this._log.error("Unexpected error: channel not nsIHttpChannel!");
|
|
|
|
this.abort();
|
|
|
|
|
|
|
|
if (this.onComplete) {
|
|
|
|
this.onComplete(ex);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.onComplete = this.onProgress = null;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (channel.contentCharset) {
|
|
|
|
this.response.charset = channel.contentCharset;
|
|
|
|
|
|
|
|
if (!this._converterStream) {
|
|
|
|
this._converterStream = Cc["@mozilla.org/intl/converter-input-stream;1"]
|
|
|
|
.createInstance(Ci.nsIConverterInputStream);
|
|
|
|
}
|
|
|
|
|
|
|
|
this._converterStream.init(stream, channel.contentCharset, 0,
|
|
|
|
this._converterStream.DEFAULT_REPLACEMENT_CHARACTER);
|
|
|
|
|
|
|
|
try {
|
2012-06-16 01:49:11 +04:00
|
|
|
let str = {};
|
|
|
|
let num = this._converterStream.readString(count, str);
|
|
|
|
if (num != 0) {
|
|
|
|
this.response.body += str.value;
|
|
|
|
}
|
2012-07-14 03:52:31 +04:00
|
|
|
} catch (ex) {
|
|
|
|
this._log.warn("Exception thrown reading " + count + " bytes from " +
|
2015-12-16 11:43:00 +03:00
|
|
|
"the channel", ex);
|
2012-07-14 03:52:31 +04:00
|
|
|
throw ex;
|
2012-06-16 01:49:11 +04:00
|
|
|
}
|
2012-07-14 03:52:31 +04:00
|
|
|
} else {
|
|
|
|
this.response.charset = null;
|
|
|
|
|
|
|
|
if (!this._inputStream) {
|
|
|
|
this._inputStream = Cc["@mozilla.org/scriptableinputstream;1"]
|
|
|
|
.createInstance(Ci.nsIScriptableInputStream);
|
|
|
|
}
|
|
|
|
|
|
|
|
this._inputStream.init(stream);
|
|
|
|
|
|
|
|
this.response.body += this._inputStream.read(count);
|
2012-04-06 10:26:06 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
this.onProgress();
|
|
|
|
} catch (ex) {
|
|
|
|
this._log.warn("Got exception calling onProgress handler, aborting " +
|
2015-12-16 11:43:00 +03:00
|
|
|
this.method + " " + channel.URI.spec, ex);
|
2012-04-06 10:26:06 +04:00
|
|
|
this.abort();
|
|
|
|
|
|
|
|
if (!this.onComplete) {
|
|
|
|
this._log.error("Unexpected error: onComplete not defined in " +
|
|
|
|
"onDataAvailable.");
|
|
|
|
this.onProgress = null;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.onComplete(ex);
|
|
|
|
this.onComplete = this.onProgress = null;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.delayTimeout();
|
|
|
|
},
|
|
|
|
|
2017-01-10 20:08:32 +03:00
|
|
|
/** nsIInterfaceRequestor **/
|
2012-04-06 10:26:06 +04:00
|
|
|
|
2017-01-10 20:09:02 +03:00
|
|
|
getInterface(aIID) {
|
2012-04-06 10:26:06 +04:00
|
|
|
return this.QueryInterface(aIID);
|
|
|
|
},
|
|
|
|
|
2017-01-10 20:08:32 +03:00
|
|
|
/** nsIBadCertListener2 **/
|
2012-04-06 10:26:06 +04:00
|
|
|
|
|
|
|
notifyCertProblem: function notifyCertProblem(socketInfo, sslStatus, targetHost) {
|
|
|
|
this._log.warn("Invalid HTTPS certificate encountered!");
|
|
|
|
// Suppress invalid HTTPS certificate warnings in the UI.
|
|
|
|
// (The request will still fail.)
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
|
2012-10-10 22:49:21 +04:00
|
|
|
/**
|
|
|
|
* Returns true if headers from the old channel should be
|
|
|
|
* copied to the new channel. Invoked when a channel redirect
|
|
|
|
* is in progress.
|
|
|
|
*/
|
|
|
|
shouldCopyOnRedirect: function shouldCopyOnRedirect(oldChannel, newChannel, flags) {
|
|
|
|
let isInternal = !!(flags & Ci.nsIChannelEventSink.REDIRECT_INTERNAL);
|
|
|
|
let isSameURI = newChannel.URI.equals(oldChannel.URI);
|
|
|
|
this._log.debug("Channel redirect: " + oldChannel.URI.spec + ", " +
|
|
|
|
newChannel.URI.spec + ", internal = " + isInternal);
|
|
|
|
return isInternal && isSameURI;
|
|
|
|
},
|
|
|
|
|
2017-01-10 20:08:32 +03:00
|
|
|
/** nsIChannelEventSink **/
|
2012-04-06 10:26:06 +04:00
|
|
|
asyncOnChannelRedirect:
|
|
|
|
function asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
|
|
|
|
|
2016-06-30 10:15:35 +03:00
|
|
|
let oldSpec = (oldChannel && oldChannel.URI) ? oldChannel.URI.spec : "<undefined>";
|
|
|
|
let newSpec = (newChannel && newChannel.URI) ? newChannel.URI.spec : "<undefined>";
|
|
|
|
this._log.debug("Channel redirect: " + oldSpec + ", " + newSpec + ", " + flags);
|
|
|
|
|
2012-04-06 10:26:06 +04:00
|
|
|
try {
|
|
|
|
newChannel.QueryInterface(Ci.nsIHttpChannel);
|
|
|
|
} catch (ex) {
|
|
|
|
this._log.error("Unexpected error: channel not nsIHttpChannel!");
|
|
|
|
callback.onRedirectVerifyCallback(Cr.NS_ERROR_NO_INTERFACE);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2012-10-10 22:49:21 +04:00
|
|
|
// For internal redirects, copy the headers that our caller set.
|
|
|
|
try {
|
|
|
|
if (this.shouldCopyOnRedirect(oldChannel, newChannel, flags)) {
|
|
|
|
this._log.trace("Copying headers for safe internal redirect.");
|
|
|
|
for (let key in this._headers) {
|
|
|
|
newChannel.setRequestHeader(key, this._headers[key], false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (ex) {
|
2015-12-16 11:43:00 +03:00
|
|
|
this._log.error("Error copying headers", ex);
|
2012-10-10 22:49:21 +04:00
|
|
|
}
|
|
|
|
|
2012-04-06 10:26:06 +04:00
|
|
|
this.channel = newChannel;
|
|
|
|
|
|
|
|
// We let all redirects proceed.
|
|
|
|
callback.onRedirectVerifyCallback(Cr.NS_OK);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Response object for a RESTRequest. This will be created automatically by
|
|
|
|
* the RESTRequest.
|
|
|
|
*/
|
2012-10-31 20:13:28 +04:00
|
|
|
this.RESTResponse = function RESTResponse() {
|
2013-08-26 22:55:58 +04:00
|
|
|
this._log = Log.repository.getLogger(this._logName);
|
2012-04-06 10:26:06 +04:00
|
|
|
this._log.level =
|
2013-08-26 22:55:58 +04:00
|
|
|
Log.Level[Prefs.get("log.logger.rest.response")];
|
2012-04-06 10:26:06 +04:00
|
|
|
}
|
|
|
|
RESTResponse.prototype = {
|
|
|
|
|
2016-03-10 09:20:29 +03:00
|
|
|
_logName: "Services.Common.RESTResponse",
|
2012-04-06 10:26:06 +04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Corresponding REST request
|
|
|
|
*/
|
|
|
|
request: null,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* HTTP status code
|
|
|
|
*/
|
|
|
|
get status() {
|
|
|
|
let status;
|
|
|
|
try {
|
2014-01-24 06:04:38 +04:00
|
|
|
status = this.request.channel.responseStatus;
|
2012-04-06 10:26:06 +04:00
|
|
|
} catch (ex) {
|
2015-12-16 11:43:00 +03:00
|
|
|
this._log.debug("Caught exception fetching HTTP status code", ex);
|
2012-04-06 10:26:06 +04:00
|
|
|
return null;
|
|
|
|
}
|
2014-08-27 18:52:21 +04:00
|
|
|
Object.defineProperty(this, "status", {value: status});
|
|
|
|
return status;
|
2012-04-06 10:26:06 +04:00
|
|
|
},
|
|
|
|
|
2014-01-24 06:04:38 +04:00
|
|
|
/**
|
|
|
|
* HTTP status text
|
|
|
|
*/
|
|
|
|
get statusText() {
|
|
|
|
let statusText;
|
|
|
|
try {
|
|
|
|
statusText = this.request.channel.responseStatusText;
|
|
|
|
} catch (ex) {
|
2015-12-16 11:43:00 +03:00
|
|
|
this._log.debug("Caught exception fetching HTTP status text", ex);
|
2014-01-24 06:04:38 +04:00
|
|
|
return null;
|
|
|
|
}
|
2014-08-27 18:52:21 +04:00
|
|
|
Object.defineProperty(this, "statusText", {value: statusText});
|
|
|
|
return statusText;
|
2014-01-24 06:04:38 +04:00
|
|
|
},
|
|
|
|
|
2012-04-06 10:26:06 +04:00
|
|
|
/**
|
|
|
|
* Boolean flag that indicates whether the HTTP status code is 2xx or not.
|
|
|
|
*/
|
|
|
|
get success() {
|
|
|
|
let success;
|
|
|
|
try {
|
2014-01-24 06:04:38 +04:00
|
|
|
success = this.request.channel.requestSucceeded;
|
2012-04-06 10:26:06 +04:00
|
|
|
} catch (ex) {
|
2015-12-16 11:43:00 +03:00
|
|
|
this._log.debug("Caught exception fetching HTTP success flag", ex);
|
2012-04-06 10:26:06 +04:00
|
|
|
return null;
|
|
|
|
}
|
2014-08-27 18:52:21 +04:00
|
|
|
Object.defineProperty(this, "success", {value: success});
|
|
|
|
return success;
|
2012-04-06 10:26:06 +04:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Object containing HTTP headers (keyed as lower case)
|
|
|
|
*/
|
|
|
|
get headers() {
|
|
|
|
let headers = {};
|
|
|
|
try {
|
|
|
|
this._log.trace("Processing response headers.");
|
|
|
|
let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel);
|
2017-01-10 20:09:02 +03:00
|
|
|
channel.visitResponseHeaders(function(header, value) {
|
2012-04-06 10:26:06 +04:00
|
|
|
headers[header.toLowerCase()] = value;
|
|
|
|
});
|
|
|
|
} catch (ex) {
|
2015-12-16 11:43:00 +03:00
|
|
|
this._log.debug("Caught exception processing response headers", ex);
|
2012-04-06 10:26:06 +04:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2014-08-27 18:52:21 +04:00
|
|
|
Object.defineProperty(this, "headers", {value: headers});
|
|
|
|
return headers;
|
2012-04-06 10:26:06 +04:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* HTTP body (string)
|
|
|
|
*/
|
|
|
|
body: null
|
|
|
|
|
|
|
|
};
|
2012-04-18 02:32:33 +04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Single use MAC authenticated HTTP requests to RESTish resources.
|
|
|
|
*
|
|
|
|
* @param uri
|
|
|
|
* URI going to the RESTRequest constructor.
|
|
|
|
* @param authToken
|
|
|
|
* (Object) An auth token of the form {id: (string), key: (string)}
|
|
|
|
* from which the MAC Authentication header for this request will be
|
|
|
|
* derived. A token as obtained from
|
|
|
|
* TokenServerClient.getTokenFromBrowserIDAssertion is accepted.
|
|
|
|
* @param extra
|
|
|
|
* (Object) Optional extra parameters. Valid keys are: nonce_bytes, ts,
|
|
|
|
* nonce, and ext. See CrytoUtils.computeHTTPMACSHA1 for information on
|
|
|
|
* the purpose of these values.
|
|
|
|
*/
|
2012-10-31 20:13:28 +04:00
|
|
|
this.TokenAuthenticatedRESTRequest =
|
|
|
|
function TokenAuthenticatedRESTRequest(uri, authToken, extra) {
|
2012-04-18 02:32:33 +04:00
|
|
|
RESTRequest.call(this, uri);
|
|
|
|
this.authToken = authToken;
|
|
|
|
this.extra = extra || {};
|
|
|
|
}
|
|
|
|
TokenAuthenticatedRESTRequest.prototype = {
|
|
|
|
__proto__: RESTRequest.prototype,
|
|
|
|
|
|
|
|
dispatch: function dispatch(method, data, onComplete, onProgress) {
|
|
|
|
let sig = CryptoUtils.computeHTTPMACSHA1(
|
|
|
|
this.authToken.id, this.authToken.key, method, this.uri, this.extra
|
|
|
|
);
|
|
|
|
|
|
|
|
this.setHeader("Authorization", sig.getHeader());
|
|
|
|
|
|
|
|
return RESTRequest.prototype.dispatch.call(
|
|
|
|
this, method, data, onComplete, onProgress
|
|
|
|
);
|
|
|
|
},
|
2012-07-14 03:52:31 +04:00
|
|
|
};
|