I’ve just been learning about Cross-Origin Resource Sharing, to permit javascript downloaded from one domain to make Ajax requests out to another domain. I started learning this because I was writing a Google Maps client to test some back-end code and it wasn’t working for some reason. Thanks to the help of someone on the Prototype mailing list, and a packet trace, the problem was quickly found.
When I loaded my static page off of the disk, the browser assigns it an origin of null. I was then accessing a service running on my desktop, so its origin was localhost. As the origins differ, when I tried to make an Ajax request to it my browser automagickally makes an OPTIONS request to the server, requesting permission.
Let me show an example, captured via tcpdump:
sudo tcpdump -i lo -nn -s0 -w out.pcap tcp port 8000
When I load up this pcap file in wireshark and follow TCP stream, I see:
OPTIONS /route/?start=sta-9998&end=sta-9999&starttime=1274469161 HTTP/1.1
Host: localhost:8000
User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1.4) Gecko/20091206 Gentoo Firefox/3.5.4
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Origin: null
Access-Control-Request-Method: GET
Access-Control-Request-Headers: x-prototype-version,x-requested-with
This is the OPTIONS request to the server, asking if it is permitted for this client to make a cross-origin request to that server. Specifically, it is asking permission to make a GET request from an Origin of “null”. If the server doesn’t respond with the right access-control headers, the browser will not permit the GET request to take place.
I had to modify my server, written in Twisted Python, to respond with:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET
Access-Control-Allow-Headers: x-prototype-version,x-requested-with
Content-Length: 0
Access-Control-Max-Age: 2520
So here I’m saying, yes, it is permitted from any origin (hence the *) to make a GET request, and the client can cache this permission for 2520 seconds (42 minutes). This won’t be my response when I deploy, I will tightly control the domains that this service permits, and lower the max-age to more like 10 minutes.
Now, this initial response is not enough, be aware. These headers must be supplied in every response, not just the response to the OPTIONS request. So when the GET finally takes place it looks like:
GET /route/?start=sta-9998&end=sta-9999&starttime=1274469161 HTTP/1.1
Host: localhost:8000
User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.1.4) Gecko/20091206 Gentoo Firefox/3.5.4
Accept: application/json
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
X-Requested-With: XMLHttpRequest
X-Prototype-Version: 1.6.1
Origin: null
And the server now responds with:
HTTP/1.1 200 OK
Content-Length: 76
Access-Control-Allow-Headers: x-prototype-version,x-requested-with
Access-Control-Max-Age: 2520
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET
Content-Type: application/json
{
"reason": "No workers ready, try again soon",
"status": "defer"
}
This is just an example while the server is loading a rather large data set, and cannot respond yet. Note the Access-Control headers in the response, just like the initial OPTIONS response.
Doing this in Twisted is simple enough. Inside of a http.Request handler, you can set response headers with self.setHeader(header_name, header_value), like so:
self.setHeader('Access-Control-Allow-Origin', '*')
self.setHeader('Access-Control-Allow-Methods', 'GET')
self.setHeader('Access-Control-Allow-Headers',
'x-prototype-version,x-requested-with')
self.setHeader('Access-Control-Max-Age', 2520)
self.setHeader('Content-type', 'application/json')
My next steps are to tighten this granting of access, probably via configuration file, but I’m sure you get the idea.