Monday, August 6, 2012

Getting HTML5 Ready - CORS

Getting HTML5 Ready - CORS:
This is the second post in this series where, in an attempt to improve my knowledge and skills with HTML and CSS3, I go from left to right across the rainbow of features on HTML5 Readiness. Last time I covered HTML5 parsing, which, while extremely important, is somewhat of a "behind the scenes" type feature. This time around we look at a little more exciting feature, CORS.

CORS - Cross-Origin Resource Sharing

In order to explain CORS, please forgive me a bad analogy. Prior to CORS, the web browser often served as some sort of chastity enforcer between my studly web app and your foxy API. While my app indicated that it wanted to consume your data and your API made it clear that it's data was open to being consumed, the browser would step in and stop this connection due to security concerns.

You could get around these security by creating some sort of server-side component to shuttle API requests, which was often unduly complicated and unecessary and, in my bad analogy, would be like bringing along a chaperone. You could also use JSONP in API's that supported it but this was limited to just GET requests making it like your API can date my app, but you'll only ever get to second base.

Ok. Enough with the bad analogy already, sorry.

With CORS, my web app can freely communicate with your API, even using POST, PUT and DELETE, provided your API's security restrictions say this is allowed. This means that you can also eliminate the server-side component and do all the API communication client-side using JavaScript.

In this post I will focus on the client-side communication rather than how to enable CORS server-side for your API. However, if you are interested in how to enable CORS for your API, you can take a look at this good tutorial by Monsur Hossain on HTML5 Rocks, which also looks at the client-side communication. There is also enable-cors-org that explains how to set up CORS on a variety of web servers from Apache to IIS to ExpressJS.

Examining the Spec

As usual, I'll start this section by stating that I am no expert at reading W3C or WHATWG specifications. I'll just try to do my best to pick out the important parts.

Unlike the HTML5 Parsing spec, this document is written both for browser vendors as well as web application authors. The introduction within the W3C specification for CORS actually does a good job of providing some simple examples of both how to enable CORS on your server, by setting the "Access-Control-Allow-Origin" header,  as well as how to communicate with that API using the XMLHttpRequest. It also makes mention of the "preflight request" that is required for requests that are not "simple methods" (simple methods are later defined specifcally as GET, HEAD and POST), such as PUT or DELETE, without going into too much detail.

After briefly discussing some suggestions for security on cross-origin request, the spec then goes into defining the various types of headers you can specify on your server for allowing different types of CORS requests. The interesting items to note here include the "Access-Control-Allow-Credentials" header which, if enabled for request can share things like cookies and HTTP authentication information. Another is the "Access-Control-Max-Age" header which indicates how long a preflight request is cached for non-simple method requests.

The next lengthy sections go into detail on how either the browser or server should handle various types of CORS requests. Honestly, it didn't seem that useful or easy to understand for a coder, so it may not be all that relevant to my readers. However, the "Use Cases" appendix was very informative, explaining some current and future uses of CORS, some of which I, personally, would not have thought of. For example, when not using CORS, loading images from one URL into a canvas element at another URL will cause the canvas to become "tainted" and calls to the toDataURL() method will error.

Browser Support

In order to examine browser support, I built a simple example that just does a basic GET from an cross-origin API. A lot of API's out don't seem to currently support CORS yet but a growing number do. One such API that has supported CORS for some time is the GitHub API. In my example below, I make a call to the GitHub API to get a list of repositories based upon the keyword of "javascript" and then populate the page with the results.

<html>
<head>
   <title>CORS Example</title>
<script>
   function onloadHandler() {
      var xhr = new XMLHttpRequest();
      xhr.open('GET', 'https://api.github.com/legacy/repos/search/javascript', true);
      // Response handlers.
      xhr.onload = function () {
         var repos = JSON.parse(xhr.response), i, reposHTML = "";
         for (i = 0; i < repos.repositories.length; i++) {
            reposHTML += "<p><a href='" + repos.repositories[i].url + "'>" + repos.repositories[i].name + "</a><br>" + repos.repositories[i].description + "</p>";
         }
         document.getElementById("allRepos").innerHTML = reposHTML;
      };
  
      xhr.onerror = function () {
         alert('error making the request.');
      };
  
      xhr.send();
   }
</script>
</head>
<body onload="onloadHandler()">
   <div id="allRepos"></div>
</body>
</html>


This is a pretty trivial example and, as you can see, it doesn't make use of any frameworks. As you can see, in the onLoadHandler() function, I simply create an XMLHttpRequest and open it for a GET call to the GitHub API URL (the third parameter, which is set to true, indicates that this request is asynchronous).

Next I create the event handlers for my request. I am only handling the onload and onerror events but there are a number of other events available to me including onloadstart, onprogress, onabort, ontimeout and onloadend. In my onload method, I simply am parsing the JSON response and populating some very simple HTML inside a div. Once my event handlers are created, I simply send the request and since the GitHub API is enabled for CORS requests, I do not get a security error.

Chrome, FireFox, Opera, Safari

Apparently Chrome supported CORS via the XMLHttpRequest level 2 as of version 3 (that seems like it must be ages ago). The above example worked fine in Chrome. Firefox version 3.5 and up support CORS and the example ran fine in the current version. Opera support was added somewhat late in the game, coming only as of version 12 (the current release is 12.1), however this example ran fine in the current release. I didn't test on Safari (figured what's the point now that it is deprecated for Windows, which I am on) but seeing as support was added in version 4 I suspect this simple example would run fine there as well. Which brings us to...

Internet Explorer

The only browser that merits its own section here. Technically, Internet Explorer 9 (the current version) supports CORS, but not via the XMLHttpRequest object. Rather, IE uses the XDomainRequest object which, for purposes of our simple example, works mostly the same other than the call to open() doesn't accept the asynchronous argument. Here is the code using XDomainRequest for IE.

<html>
<head>
   <title>CORS Example</title>
<script>
   function onloadHandler() {
      var xhr = new XDomainRequest();
      xhr.open('GET', 'https://api.github.com/legacy/repos/search/javascript');
      // Response handlers.
      xhr.onload = function () {
         var repos = JSON.parse(xhr.response), i, reposHTML = "";
         for (i = 0; i < repos.repositories.length; i++) {
            reposHTML += "<p><a href='" + repos.repositories[i].url + "'>" + repos.repositories[i].name + "</a><br>" + repos.repositories[i].description + "</p>";
         }
         document.getElementById("allRepos").innerHTML = reposHTML;
      };
  
      xhr.onerror = function () {
         alert('error making the request.');
      };
  
      xhr.send();
   }
</script>
</head>
<body onload="onloadHandler()">
   <div id="allRepos"></div>
</body>
</html>


Sadly, this still doesn't work. Why? Well it turns out that XDomainRequest has a number of additional security restrictions one of which uses an "overly broad" restriction on mixing HTTP and HTTPS requests. While sending an unsecure HTTP call from a secure page would be undesirable and one could understandably sympathize with blocking this, IE also blocks calls to secure request from unsecure pages. Thus, since GitHub's API calls all happen over HTTPS I cannot call them from an unsecure page. There is a convoluted workaround but it seems that in the end these issues will be resolved in IE10 which supports CORS via XMLHttpRequest.

Using jQuery

You can further simplify using CORS by simply relying on jQuery as it does support CORS requests via the ajax() method. Obviously, the same restrictions apply, as in it suddenly won't start magically working in IE, but it can simplify your code dramatically. I am by no means a jQuery expert but, for example, the following code creates the same example above but using jQuery.

<html>
<head>
   <title>CORS Example</title>
<script src="jquery-1.7.2.min.js"></script>
<script>
   $(function() {
      $.ajax("https://api.github.com/legacy/repos/search/javascript").done(function(data) {
         var i, repo;
         $.each(data.repositories, function (i, repo) {
            $("#allRepos").append("<p><a href='" + repo.url + "'>" + repo.name + "</a><br>" + repo.description + "</p>");
         });
      });
   });
</script>
</head>
<body>
   <div id="allRepos"></div>
</body>
</html>


As you can see, both the CORS request to the GitHub API and the handling of the result are both significantly simpler.

What's Next

My example here only dug into doing a simple GET request via CORS. Obviously, this is no different than what you can achieve today via JSONP except that we don't need to rely on the JSONP format. However, the real power of CORS lies in the ability to do POST, PUT and other types of requests. For example, Google's YouTube API announced CORS support back in May which included an example that allows you to upload a video to YouTube. All of the authentication and posting of the video is done client-side via JavaScript (you can view the source on the example).

If you'd like more information on how to create and handle these types of requests, I highly recommend the CORS tutorial on HTML5Rocks.com which was a great resource when writing this article.

Up next I will be taking a look at making document regions editable via the contenteditable attribute.

DIGITAL JUICE

No comments:

Post a Comment

Thank's!