CORS and preflighted requests with TMS XData

Bookmarks: 

Friday, April 10, 2015

From Wikipedia: Cross-origin resource sharing (CORS) is a mechanism that enables many resources (e.g. fonts, JavaScript, etc.) on a web page to be requested from another domain outside the domain from which the resource originated. In other words, if you are trying to access entities in a TMS XData server from a web page (for example, using AJAX requests), this mechanism (CORS) will eventually get in action.

Enabling CORS in TMS XData is very simple. Actually a single line of code:

    XDataServerModule.AccessControlAllowOrigin := '*';

And it will handle most of what's needed, including preflighted requests. This post could end here if you are looking about how to deal with CORS in TMS XData. But let me use the opportunity to explain and illustrate how CORS works.

Let's use the SQLiteConsolerServer demo that is included in TMS XData distribution. This very simple demo creates an SQLite database in memory, fill it with some predefined data (artists, albums and tracks), and starts a XData server in the address "http://localhost:2001/tms/music" to provide the objects from the database. This is what you will get after running the demo.

Now if we go to our browser (I'm using Chrome here) and enter the URL "http://localhost:2001/tms/music/Track(1)", this is what we get:

So far so good, our server is providing the JSON representation of our Track object with id equals to 1. But what happens if we try to do the same request using AJAX? Let's create a small HTML page with some JavaScript that performs the same GET request we're doing directly with the browser. Here is the full code of the HTML:


<!DOCTYPE html>
<html>
<body>

<script>
	function processResponse(xmlhttp) {
		switch(xmlhttp.status) {
			case 200:
				var track = JSON.parse(xmlhttp.responseText);
				document.getElementById("getButton").innerText=track.Name;
				break;
			case 404:
				document.getElementById("getButton").innerText="(not found)";
				break;
			default:
				document.getElementById("getButton").innerText="(invalid)";
		}
	}

	function getTrack1Name() { 
		var xmlhttp=new XMLHttpRequest(); 
		
		xmlhttp.onreadystatechange = function() {
			if (xmlhttp.readyState == 4) {
				processResponse(xmlhttp);
			}
		}
		xmlhttp.open("GET","http://localhost:2001/tms/music/Track(1)",true);
		xmlhttp.send(null); 
    }

	function deleteTrack1() { 
		var xmlhttp=new XMLHttpRequest(); 
		xmlhttp.open("DELETE","http://localhost:2001/tms/music/Track(1)",true) 
		xmlhttp.send(null); 
	}
</script>

<button onclick="getTrack1Name();" id="getButton">Get Track 1 Name</button> 
<br><br>
<button onclick="deleteTrack1();">Delete Track 1</button> 

</body>
</html>


Code is very simple, it just provides two buttons that perform GET and DELETE requests to get the name of Track 1 and delete Track 1, respectively.

Let's open that page in browser (I'm using a WAMP server here but you could just double-click the HTML file):

If we click the first button to retrieve the name of Track 1, we get this:

It doesn't work. Why is that? If we press F12 in Chrome to get more info about it, you can get a clue about what's going on:

That's CORS in action. The browser doesn't allow a request from domain "localhost:8080" (where our web page is located) to the domain "localhost:2001" (where our XData server is located) unless our server states that it allows it (using the mentioned response header).

We can then modify our SQLiteConsoleServer demo to add that small line of code mentioned in the beginning of this post:

   {...}
    Module.AccessControlAllowOrigin := '*'; // Add this line
    Server.AddModule(Module);
    Server.Start;
    {...}

Then if we restart our server, refresh our test page, and try pressing the button again, here is what we get:

Now it works! Here is the response returned by the XData server:

HTTP/1.1 200 OK
Content-Length: 228
Content-Type: application/json
Server: Microsoft-HTTPAPI/2.0
access-control-allow-origin: *
Date: Fri, 10 Apr 2015 14:08:03 GMT

{
    "$id": 1,
    "@xdata.type": "XData.Default.Track",
    "Id": 1,
    "Name": "Black Dog",
    "Composer": "Jimmy Page, Robert Plant, John Paul Jones",
    "Milliseconds": 296672,
    "Genre@xdata.ref": "Genre(1)"
}

Note the presence of header "access-control-allow-origin" which states that the server allows requests from any server. You could just restrict this to a specific server origin address by simply defining the name of the server instead of using "*" when setting the property.

Now what about preflighted requests? It will happen when we click our "Delete Track 1" button. From this nice Mozilla web page explaining CORS, it explains that a request must be preflighted if the HTTP method is different than GET, HEAD or POST, or even if request use custom headers or content-type different than some accepted ones. This covers a lot of very common REST requests: DELETE, PUT, or POSTing JSON data.

So what happens exactly when we click "Delete Track 1" button? This is the request Chrome will send to our XData server:

OPTIONS http://localhost:2001/tms/music/Track(1) HTTP/1.1
Host: localhost:2001
Connection: keep-alive
Access-Control-Request-Method: DELETE
Origin: http://localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36
Accept: */*
Referer: http://localhost:8080/tests/cors.html
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8,pt;q=0.6

Note that instead of sending a DELETE request, it sent an OPTIONS request, which is the preflighted one. This means the browser is "checking" the server if the request he's going to perform is valid. It indicates it's going to perform a DELETE method using the "Access-Control-Request-Method" header. If the request had different headers, it would also send header "Access-Control-Request-Headers" to check with the server if the headers will be allowed.

The XData server then responds informing the client that the DELETE request will be accepted:

HTTP/1.1 200 OK
Server: Microsoft-HTTPAPI/2.0
access-control-allow-methods: GET,PUT,PATCH,DELETE,OPTIONS
access-control-allow-origin: *
access-control-max-age: 1728000
Date: Fri, 10 Apr 2015 14:16:15 GMT
Connection: close
Content-Length: 0

And finally, Chrome performs the actual DELETE request:

DELETE http://localhost:2001/tms/music/Track(1) HTTP/1.1
Host: localhost:2001
Connection: keep-alive
Origin: http://localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36
Accept: */*
Referer: http://localhost:8080/tests/cors.html
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8,pt;q=0.6

If we press the "Get Track 1 Name" button again, we will be informed it doesn't exist:

So, although enabling CORS in XData is just a single line of code, my intention here was to explain CORS with little more details, including preflighted requests, and show how XData makes it work under the hood.



Wagner R. Landgraf


Bookmarks: 

This blog post has received 2 comments.


1. Monday, April 13, 2015 at 4:20:40 PM

Is there any security issue created by this new feature? I think about same-origin security policy. Is it possible to use both of them?

Thank you


ghazali


2. Monday, April 13, 2015 at 11:11:02 PM

CORS is a standardized way to relax same-origin security policy. It''s just a feature, it''s up to you to decide if you must ensure same-origin or you can relax it using CORS. The purpose of the blog is to show XData has both options (by default CORS is not enabled to browsers will enforce same-origin) and that it also support preflighted requests.

Wagner Landgraf




Add a new comment:
Author:
Email:
  You will receive a confirmation mail with a link to validate your comment, so please use a valid email address.
Comment:
 
Change Image
Fill in the characters from the image above:
 

All fields are required.
 




Previous  |  Next  |  Index