×

Stay in touch

×

Ajax cross domain on Jersey restfull webservices

Ajax cross domain on Jersey restfull webservices

  • May 27, 2011

At work, at LesTanukis, I'm working on a REST web service served by the open source application server Glassfish and the REST lib Jersey. I asked myself how to query these webservices directly from a web client, ie, how to bypass the same origin policy security restrictions. Here are some notes that recounts my route.

PHP Ajax bridge

The first solution is obvious, building a bridge between the client and the web server, and that's this one that will query the webservice.

For example, in our case, we use the Drupal CMS.

To open the door to an Ajax request, the easiest way is to implement a hook_menu, with the MENU_CALLBACK constant as argument:

 function your-module-name_menu(){
return array( "ajax_bridge" => array(
"title" => t("Ajax bridge"),
"page callback" => "_ajax_dispatcher",
'access arguments' => array('access home page'),
"type" => MENU_CALLBACK
) );
}

Here, I create a url, ( "/ajax_bridge" ), which will call the _ajax_dispatcher function if it's called.

Here's what the documentation says about the drupal constant MENU_CALLBACK:

MENU_CALLBACK: Callbacks simply register a path so that the correct function is fired when the URL is accessed.

That is exactly what we need. We can then call the webservice with cURL:

  1. function requestGlassfish(){
  2.  
  3. header('Cache-Control: no-cache, must-revalidate');
  4. header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
  5. header('Content-type: application/json');
  6.  
  7. $sServeurUrl = filter_input( INPUT_POST, "glassfish_url", FILTER_SANITIZE_URL );
  8.  
  9. if( FALSE === $sServeurUrl ){
  10. echo json_encode(array("error" => "Glassfish URL not set"));
  11. exit();
  12. }
  13.  
  14. if( extension_loaded("curl") ){
  15.  
  16. $ch = curl_init();
  17.  
  18. CURLOPT_URL => "http://localhost:8080/services/rest/" . $sServeurUrl,
  19. CURLOPT_RETURNTRANSFER => 1,
  20. CURLOPT_CONNECTTIMEOUT => 5,
  21. CURLOPT_TIMEOUT => 10,
  22. CURLOPT_HEADER => 0,
  23. CURLOPT_HTTPHEADER => array( 'Accept: application/json' )
  24. ));
  25.  
  26. $output = curl_exec($ch);
  27.  
  28. if ($output === FALSE) {
  29. echo json_encode(array("error" => "Curl error, empty response: " . curl_error($ch)));
  30. curl_close($ch);
  31. exit();
  32. }
  33.  
  34. echo json_encode(array("success" =>$output ));
  35. curl_close($ch);
  36. }
  37. else{
  38. echo json_encode(array("error" => "Curl extension is not loaded"));
  39. exit();
  40. }
  41. }

Here, we begin by sending some HTTP header with MIME type "application/JSON", and as we are experiencing, we disable the cache. We then create a HTTP request to the Glassfish server, with the Accept header set to "application/json", so that Jersey understand that we want some JSON. We then echo the returned string. Easy...

However, this solution does not meet the specifications, we can not say that the client application directly request the webservice ... But, as we host Apache, it is not possible to force Glassfish to listen on port 80. We are therefore forced to choose another port, and so, the same origin policy restriction applies and prevents a direct query to Glassfish.

However, we know that the src attribute of the script tag is not subject to this restriction. You can load a script from any URL hosted on the web. We could then query the webservice by injecting a script tag in our page. This is called JSONP, JSON With Padding.

JSONp

The idea is simple. Inject a script tag dynamically in head of our page, with a url get parameter defining a callback function defined in advance. Here's how we might proceed:

function handleJSONanswser( JSON_object ){
//do what we need with JSON_object
}

//inject the script tag
var script = document.createElement('script');
script.type="text/javascript";
script.src = "http://glassfish_url:8080/rest?callback=handleJSONanswser";
document.getElementsByTagName('head')[0].appendChild(script);

The server then return the wanted JSON, wrapped inside the defined callback:

handleJSONanswser( [{"object1"},{"object2"},{"object3"},...] ); 

Once the query ends, our function is executed with the expected JSON data as parameter. Magical...

This solution is simplified by jQuery, thanks to the function getJSON. We then need to configure Jersey so that it return the JSON array wrapped within the function passed as parameter.

Here are some tips found on the net:

Summarize:

Get the get paramater (the funcion name) with Jersey annotation:

@QueryParam("jsoncallback") @DefaultValue("fn") String callback

Return the JSON object wrapped in the function, thanks to JSONWithPadding

return new JSONWithPadding(objectOfIntereset,callback); 

However, we did not choose that solution. Indeed, each request leads to the injection of a new tag in our html code. Furthermore, browsers issues when dealing with errors, resulting in silently failures difficult to debug . Therefore we preferred to use the HTML5 solution entitled "cross origin resource sharing", or CORS.

Cross origin resource sharing

CORS is a specification that allows a server to send special HTTP headers that will allow a client request from another server to succeed.

There are two types of CORS requests:

Simple

A query of type POST or GET with the Content-Type  header set to application/x-www-form-urlencoded or multipart/form-data or text/plain. In this case, the server should return the special header Access-Control-Allow-Origin, which must hold the string send by the client within the Origin header, or, if the resource is public, an *. In this case, any client wil be allowed to query the server.

Preflighted requêtes

For all other requests (PUT, DELETE, with special headers like X-Requested-With, or with an exotic Content-Type such as application/json ), the client will make 2 requests. A first one of type OPTION, then the main request itself. Here is an excerpt from hacks.mozilla.org:

The specification mandates that browsers "preflight" the request, soliciting supported methods from the server with an HTTP OPTIONS request header, and then, upon "approval" from the server, sending the actual request with the actual HTTP request method.

Let see the server-side code.

We must first declare a Maven dependency to the Glassfish web API, and to Jettison, the lib that we'll use to serialize our entities to JSON:

 <dependency>
    <groupId>javax</groupId>
    <artifactId>javaee-web-api</artifactId>
    <version>6.0</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>org.codehaus.jettison</groupId>
    <artifactId>jettison</artifactId>
    <version>1.3</version>
</dependency>

I'll not talk about web.xml and glassfish-web.xml configuration files, that will allow us you to set ours base urls and load Jersey.

We have here a method called getAllJSON, within the class Fluxfacade, which holds webservices for our Stream (Flux in french) JPA entity:

  1. ...
  2.  
  3. import org.codehaus.jettison.json.JSONObject;
  4. import org.codehaus.jettison.json.JSONArray;
  5. import org.codehaus.jettison.json.JSONException;
  6.  
  7. import javax.ws.rs.core.Response;
  8. import javax.ws.rs.core.Response.ResponseBuilder;
  9.  
  10. ...
  11.  
  12. @Path("/flux")
  13. public class FluxFacade {
  14.  
  15. ...
  16.  
  17. @GET
  18. @Path("/all")
  19. @Produces({"application/json"})
  20. public Response getAllJSON( ) {
  21.  
  22. List<Flux> l = em.createNamedQuery("Flux.findAll", Flux.class ).getResultList();
  23.  
  24. JSONArray jsonarray = new JSONArray();
  25.  
  26. for( Flux f : l ) {
  27. jsonarray.put( fluxToJson(f) );
  28. }
  29.  
  30. ResponseBuilder builder = Response.ok(jsonarray);
  31. builder.header("Access-Control-Allow-Origin", "*");
  32. builder.header("Access-Control-Max-Age", "3600");
  33. builder.header("Access-Control-Allow-Methods", "GET");
  34. builder.header("Access-Control-Allow-Headers", "X-Requested-With,Host,User-Agent,Accept,Accept-Language,Accept-Encoding,Accept-Charset,Keep-Alive,Connection,Referer,Origin");
  35.  
  36. return builder.build();
  37. }
  38.  
  39. private JSONObject fluxToJson( Flux f ){
  40. try {
  41. return new JSONObject()
  42. .put("id", f.getId() )
  43. .put("iduser", f.getIdUser())
  44. .put("title", f.getTitle() )
  45. .put("is_active", f.getIsActive() )
  46. .put("isocountry", f.getCountry() )
  47. .put("longitude", f.getLongitude() )
  48. .put("latitude", f.getLatitude() );
  49. } catch (JSONException je){
  50. return null;
  51. }
  52. }
  53.  
  54. ...
  55. }

We can go very far with Jersey, configuring entity serialization (aka marshalling), through the JAXB API. The method I present here has the advantage of being simple and effective. What is important is the use of the ResponseBuilder header's method, which allows to send the CORS headers. Here is a screenshot of firebug showing the exchanged headers:

Firebug CORS request capture

Client side, here is the code used:

  1. //simplified
  2. requestService : function( o ){
  3. jQuery.ajax({
  4. beforeSend: function(req) {
  5. req.setRequestHeader("Accept", "application/json");
  6. },
  7. type: o.method,
  8. url: "http://glassfish_url:8080/services/rest/" + o.url,
  9. dataType: "json",
  10. success: function(data) {
  11. o.app.trigger('flux_loaded', data );
  12. },
  13. error:function(XMLHttpRequest, textStatus, errorThrown){
  14.  
  15. console.dir(errorThrown);
  16. console.dir(textStatus);
  17. console.debug(XMLHttpRequest);
  18. }
  19. });
  20. }

Easy...

Finally, note that Internet Explorer does not use exactly the same API (...). Microsoft created the XDomainRequest object, which performs the same function (and accepts same server headers):

//from msdn
var xdr = new XDomainRequest();
xdr.open("get", "http://www.contoso.com/xdr.aspx");
xdr.send();

For IE6, IE7, we'll have to go through a PHP bridge or a JSONP call, after having detected the lack of the withCredentials property of the XMLHttpRequest object (from hacks.mozilla.org):

var request = new XMLHttpRequest();
if( request.withCredentials !== undefined ){
// make cross-site requests
}
else if( window.XDomainRequest ) {
//ie8 with XDomainRequest
}
else {
//JSONp for ie7
}

Ok, well done...

But manually add the HTML5 HTTP headers will quickly become tedious...

Fortunately, Jersey is a servlet like any other. We can so add a filter that will do the job for us ...

  1. @WebFilter(filterName="HTML5CorsFilter",urlPatterns={"/rest/*"})
  2. public class HTML5CorsFilter implements javax.servlet.Filter{
  3.  
  4. private static final Logger logger = Logger.getLogger( HTML5CorsFilter.class.getName() );
  5.  
  6. @Override
  7. public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
  8.  
  9. logger.log( Level.INFO, "HTML5CorsFilter add HTML5 CORS Headers" );
  10.  
  11. HttpServletResponse res = (HttpServletResponse) response;
  12. res.addHeader("Access-Control-Allow-Origin", "*");
  13. res.addHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT");
  14. res.addHeader("Access-Control-Allow-Headers", "Content-Type");
  15. chain.doFilter(request, response);
  16. }
  17. }