Introduction
Starting with Gecko 1.8.1 (Firefox 2), it is possible to create sandboxed HTTP connections which don't affect the user's cookies. This article will cover the basics of doing HTTP connections from XPCOM JavaScript, and should easily translate to C++ XPCOM.
Setting up an HTTP connection
The first step in setting up an HTTP connection from an URL (stored in a string) is to create an nsIURI
out of it. nsIURI
is an XPCOM representation of an URI, with useful methods to query and manipulate the URI. To create an nsIURI
from an string, we use the newURI
method of nsIIOService
:
// the IO service var ioService = Components.classes["@mozilla.org/network/io-service;1"] .getService(Components.interfaces.nsIIOService); // create an nsIURI var uri = ioService.newURI(myURLString, null, null);
Once the nsIURI
has been created, a nsIChannel
can be generated from it using nsIIOService
's newChannelFromURI
method:
// get a channel for that nsIURI var channel = ioService.newChannelFromURI(uri);
To initiate the connection, the asyncOpen
method is called. It takes two arguments: a listener and a context that is passed to the listener's methods.
channel.asyncOpen(listener, null);
HTTP notifications
The above mentioned listener is a nsIStreamListener
, which gets notified about events such as HTTP redirects and data availability.
onStartRequest
- gets called when a new request is initiated.onDataAvailable
- new data is available. Since this is a stream, it could be called multiple times (depending on the size of the returned data, networking conditions, etc).onStopRequest
- the request has finished.onChannelRedirect
- when a redirect happens, a newnsIChannel
is created, and both the old and new ones are passed in as arguments.
Since nsIStreamListener
does not cover cookies, the current channel being used will need to be stored as a global, since another listener will be used for cookie notifications (covered in the next section). It is usually best to use a JavaScript wrapper that implements all the required methods and calls the specified callback function when the connection has completed. Below is an example:
// global channel var gChannel; // init the channel // the IO service var ioService = Components.classes["@mozilla.org/network/io-service;1"] .getService(Components.interfaces.nsIIOService); // create an nsIURI var uri = ioService.newURI(myURLString, null, null); // get a channel for that nsIURI gChannel = ioService.newChannelFromURI(uri); // get an listener var listener = new StreamListener(callbackFunc); gChannel.notificationCallbacks = listener; gChannel.asyncOpen(listener, null); function StreamListener(aCallbackFunc) { this.mCallbackFunc = aCallbackFunc; } StreamListener.prototype = { mData: "", // nsIStreamListener onStartRequest: function (aRequest, aContext) { this.mData = ""; }, onDataAvailable: function (aRequest, aContext, aStream, aSourceOffset, aLength) { var scriptableInputStream = Components.classes["@mozilla.org/scriptableinputstream;1"] .createInstance(Components.interfaces.nsIScriptableInputStream); scriptableInputStream.init(aStream); this.mData += scriptableInputStream.read(aLength); }, onStopRequest: function (aRequest, aContext, aStatus) { if (Components.isSuccessCode(aStatus)) { // request was successfull this.mCallbackFunc(this.mData); } else { // request failed this.mCallbackFunc(null); } gChannel = null; }, // nsIChannelEventSink onChannelRedirect: function (aOldChannel, aNewChannel, aFlags) { // if redirecting, store the new channel gChannel = aNewChannel; }, // nsIInterfaceRequestor getInterface: function (aIID) { try { return this.QueryInterface(aIID); } catch (e) { throw Components.results.NS_NOINTERFACE; } }, // nsIProgressEventSink (not implementing will cause annoying exceptions) onProgress : function (aRequest, aContext, aProgress, aProgressMax) { }, onStatus : function (aRequest, aContext, aStatus, aStatusArg) { }, // nsIHttpEventSink (not implementing will cause annoying exceptions) onRedirect : function (aOldChannel, aNewChannel) { }, // we are faking an XPCOM interface, so we need to implement QI QueryInterface : function(aIID) { if (aIID.equals(Components.interfaces.nsISupports) || aIID.equals(Components.interfaces.nsIInterfaceRequestor) || aIID.equals(Components.interfaces.nsIChannelEventSink) || aIID.equals(Components.interfaces.nsIProgressEventSink) || aIID.equals(Components.interfaces.nsIHttpEventSink) || aIID.equals(Components.interfaces.nsIStreamListener)) return this; throw Components.results.NS_NOINTERFACE; } };
Quick note: storing the channel in a global (especially in an extension) isn't a good idea, but was done to make the code easier to read. It would be better to have the entire implementation inside a class and storing the channel as a member:
function myClass() { this.mChannel = null; // ... var listener = new this.StreamListener(callbackFunc); // ... } myClass.prototype.StreamListener = function (aCallbackFunc) { return ({ mData: "", // ... }) }
Handling cookies
When sending a request, cookies that apply to the URL are sent with the HTTP request. The HTTP response can also contain cookies, which the browser processes. As of Mozilla 1.8.1 (Firefox 2), it is now possible to intercept those two cases.
For example, this means that if the user was logged into an webmail account, another account on the same domain could be checked without changing the user's cookies.
The observer service (nsIObserverService
) is used to send general notifications, including the two cookie ones. The addObserver
method is used to add an observer for a certain topic and takes in three agruments:
- an object than implements
nsIObserver
- the topic to listen for. For cookies, the two topics are:
http-on-modify-request
- happens after the cookie data has been loaded into the request, but before the request is sent.http-on-examine-response
- happens after the response is received, but before the cookies are processed
- whether to hold a weak reference to the observer argument. Use
false
.
In order to avoid memory leaks, the observer needs to be removed at one point. The removeObserver
method takes in the listener object and the topic and removes it from the notification list.
As with the above stream listener, an nsIObserver
implementing object is needed, which only needs to implement one method, observe
. The observe
method gets passed in three arguments, which for the two cookie topics are:
aSubject
: the channel (nsIChannel
) that caused this notification to happen.aTopic
: the notification topic.aData
:null
for the two topics.
Since the observers get notified for the registered topic for any connection, the listener needs to make sure that the notification is for the HTTP connection our code created. Since the channel that causes the notification is passed in as the first argument, comparing it to the globally stored channel (gChannel
) in the previous section (which also gets updated each time a redirect happens).
// create an nsIObserver implementor var listener = { observe : function(aSubject, aTopic, aData) { // Make sure it is our connection first. if (aSubject == gChannel) { var httpChannel = aSubject.QueryInterface(Components.interfaces.nsIHttpChannel); if (aTopic == "http-on-modify-request") { // ... } else if (aTopic == "http-on-examine-response") { // ... } } }, QueryInterface : function(aIID) { if (aIID.equals(Components.interfaces.nsISupports) || aIID.equals(Components.interfaces.nsIObserver)) return this; throw Components.results.NS_NOINTERFACE; } }; // get the observer service and register for the two coookie topics. var observerService = Components.classes["@mozilla.org/observer-service;1"] .getService(Components.interfaces.nsIObserverService); observerService.addObserver(listener, "http-on-modify-request", false); observerService.addObserver(listener, "http-on-examine-response", false);
The final piece is to manipulate the cookies. In order to manipulate cookies, the nsIChannel
needs to be converted into a nsIHttpChannel
by using QueryInterface
(QI):
var httpChannel = aSubject.QueryInterface(Components.interfaces.nsIHttpChannel);
Cookies are actually part of the HTTP header, nsIHttpChannel
provides four methods for working with headers: two for getting and setting request headers, and two for getting and setting response headers. The cookie header for requests is called "Cookie", while for responses it is "Set-Cookie".
getRequestHeader(aHeader)
- returns the request header value for the requested header.setRequestHeader(aHeader, aValue, aMerge)
- sets the request header's value. IfaMerge
istrue
, the new value is appened, otherwise the old value is overwritten.getResponseHeader(aHeader)
- returns the response header value for the requested header.setResponseHeader(aHeader, aValue, aMerge)
- sets the response header's value. IfaMerge
istrue
, the new value is appened, otherwise the old value is overwritten.
These methods provide all the required functionality needed to modify cookies before they are processed/sent, allowing for sandboxed cookie connections that don't affect the user's cookies.
HTTP referrer
If the HTTP request needs to have a referrer set, two additional steps are needed after the nsIChannel
is created, but before it is opened. First, a nsIURI
needs to be generated for the referrer URL. Like before, the nsIIOService
is used:
var referrerURI = ioService.newURI(referrerURL, null, null);
Next, the nsIChannel
is QIed to nsIHttpChannel
and the referrer
property is set to the generated nsIURI
:
var httpChannel = channel.QueryInterface(Components.interfaces.nsIHttpChannel); httpChannel.referrer = referrerURI;
Creating HTTP POSTs
To create an HTTP POST, a few additional steps are required after the nsIChannel
is created.
First, a nsIInputStream
instance is created, after which the setData
method is called. The first argument is the POST data as a string, while the second argument is the length of that data. In this case, the data is URL encoded, meaning that the string should look like this: foo=bar&baz=eek
.
var inputStream = Components.classes["@mozilla.org/io/string-input-stream;1"] .createInstance(Components.interfaces.nsIStringInputStream); inputStream.setData(postData, postData.length);
Next, the nsIChannel
is QIed to an nsIUploadChannel
. Its setUploadStream
method is called, passing in the nsIInputStream
and the type (in this case, "application/x-www-form-urlencoded"):
var uploadChannel = gChannel.QueryInterface(Components.interfaces.nsIUploadChannel); uploadChannel.setUploadStream(inputStream, "application/x-www-form-urlencoded", -1);
Due to a bug, calling setUploadStream
will reset the nsIHttpChannel
to be a PUT request, so now the request type is set to POST:
// order important - setUploadStream resets to PUT httpChannel.requestMethod = "POST";