Wednesday, April 28, 2010

GWT and Cross-Site Requests

Browsers have Same Origin Policy (SOP) which disallows Ajax (XMLHttpRequest) calls made to servers other than the ones where the page and/or javascript is loaded from. This is done for security reasons. Allowing calls to different servers than the ones where the page originated from could cause some web pages from sites to make calls onto other hosts which trust the client. SOP however causes a huge issue for GWT applications. GWT modules load just a single page and given SOP all the application needs have to be performed by the same server. This is not ideal if an GWT application is to make use of services provided by multiple servers. For example Medrium has Document Management, Practice Management, Mail, Clinical servers providing various services. If the Clinical Application needs to access a Document it will not be able to make a call onto Document Management server to retrieve the Document. The page containing this request has to be loaded from Document Management server or a proxy needs to be set up on Clincal server to retrieve the document and pass back to the Clinical GWT Client.

Given the need for Cross-Site requests W3C has proposed a Cross-Origin Resource Sharing specification which allows secure Cross-Site calls to be made -http://www.w3.org/TR/cors/. The specification allows the browser client and server to negotiate if access is allowed and make cross site calls. Most modern browsers – Firefox, Safari, Opera support this specification. This specification allows the same XMLHttpRequest to be used for both Cross-Site and same site calls. Microsoft however does not support XMLHttpRequest instead supports another API . IE8 supports XDomainRequest which can do Cross-Site requests but has a different API and also does not allow passing any Header information. On the server side, one can write a Servlet Filter which can respond to Client Cross Origin negotation requests. Jetty 7.x provides such a filter so no new filter needs to be written but Jetty's CrossOrigin filter can be configured via web.xml http://wiki.eclipse.org/Jetty/Feature/Cross_Origin_Filter. Cross Site requests are straight forward for the browsers that support W3C spec and would not require any support from GWT. However, since IE does not support W3C spec and has a completely different interface. So it is not possible to do Cross-Site requests from IE using GWT.

In order to support Cross Site requests for IE8 we first create a Javascript Overlay Type for XDomainRequest.
public class XDomainRequest extends JavaScriptObject {
public static native XDomainRequest create() /*-{
if ($wnd.XDomainRequest) {
return new XDomainRequest();
}
else {
$wnd.alert("Cross site requests not supported in this browser");
return null;
}
}-*/;
protected XDomainRequest() {}
public final native void abort() /*-{
this.abort();
}-*/;
public final native void clear() /*-{
var self = this;
$wnd.setTimeout(function() {
self.onload= new Function();
self.onprogress= new Function();
self.onerror= new Function();
self.ontimeout= new Function();
}, 0);
}-*/;
public final native String getContentType() /*-{
return this.contentType;
}-*/;
public final native void setTimeout(int value) /*-{
this.tmeout=value;
}-*/
public final native int getTimeout() /*-{
return this.timeout;
}-*/;
public final native String getResponseText() /*-{
return this.responseText;
}-*/;
public final native void open(String httpMethod, String url) /*-{
this.open(httpMethod, url);
}-*/;
public final native void send(String requestData) /*-{
this.send(requestData);
}-*/;
public final native void setHandler(XDomainRequestHandler handler) /*-{
var _this = this;
this.onload = $entry(function() {
handler.@com.medrium.gwt.client.rpc.XDomainRequestHandler::onLoad(Lcom/medrium/gwt/client/rpc/XDomainRequest;)(_this);
});
this.onerror = $entry(function() {
handler.@com.medrium.gwt.client.rpc.XDomainRequestHandler::onLoad(Lcom/medrium/gwt/client/rpc/XDomainRequest;)(_this);
});
this.ontimeout = $entry(function() {
handler.@com.medrium.gwt.client.rpc.XDomainRequestHandler::onTimeout(Lcom/medrium/gwt/client/rpc/XDomainRequest;)(_this);
});
this.onprogress = $entry(function() {
handler.@com.medrium.gwt.client.rpc.XDomainRequestHandler::onProgress(Lcom/medrium/gwt/client/rpc/XDomainRequest;)(_this);
});
}-*/;
}

A Domain request handler interface to handle callbacks from XDomainRequest
public interface XDomainRequestHandler {
public void onLoad(XDomainRequest req);
public void onProgress(XDomainRequest req);
public void onError(XDomainRequest req);
public void onTimeout(XDomainRequest req);
}

We then provide different implementations for GWT's RpcRequestBuilder, RequestBuilder, Request and Response classes
public class IECrossSiteRpcRequestBuilder extends RpcRequestBuilder {

protected RequestBuilder doCreate(java.lang.String serviceEntryPoint) {
return new IECrossSiteRequestBuilder(RequestBuilder.POST,
serviceEntryPoint);
}
/**
* Nothing to do. Cannot set custom headers in XDomainRequest
*/
protected void doFinish(RequestBuilder rb) {
}
}

public class IECrossSiteRequestBuilder extends RequestBuilder {
public IECrossSiteRequestBuilder(RequestBuilder.Method httpMethod,
String url) {
super(httpMethod, url);
}
public Request send() throws RequestException {
return doSend(getRequestData(), getCallback());
}
public Request sendRequest(String data, RequestCallback callback)
throws RequestException {
return doSend(data, callback);
}
private Request doSend(String data, final RequestCallback callback)
throws RequestException {
XDomainRequest xhr = XDomainRequest.create();
try {
xhr.open(getHTTPMethod(), getUrl());
}
catch (JavaScriptException e) {
RequestPermissionException requestPermissionException =
new RequestPermissionException(getUrl());
requestPermissionException.initCause(new RequestException(
e.getMessage()));
throw requestPermissionException;
}
// Cannot set content type on IE
final IECrossSiteRequest req= new IECrossSiteRequest(xhr);
req.setStatus(IECrossSiteRequest.OPEN);
final int timeout;
if ( (timeout = getTimeoutMillis()) > 0 ) {
xhr.setTimeout(getTimeoutMillis());
}
// set handlers
xhr.setHandler(new XDomainRequestHandler() {
public void onLoad(XDomainRequest r) {
req.setStatus(IECrossSiteRequest.DONE);
callback.onResponseReceived(req,
new IECrossSiteResponse(r));
}
public void onTimeout(XDomainRequest r) {
req.setStatus(IECrossSiteRequest.DONE);
callback.onError(req,
new RequestTimeoutException(req, timeout));
}
public void onProgress(XDomainRequest r) {
}
public void onError(XDomainRequest r) {
// Assume permission exception since XDomainRequest does not
// return an error reason
req.setStatus(IECrossSiteRequest.DONE);
callback.onError(req,
new RequestPermissionException(getUrl()));
}
});
try {
xhr.send(data);
req.setStatus(IECrossSiteRequest.SENT);
}
catch (JavaScriptException e) {
throw new RequestException(e.getMessage());
}
return req;
}
}

public class IECrossSiteRequest extends Request {
static final int UNSENT = 0;
static final int OPEN = 1;
static final int SENT = 2;
static final int DONE = 3;
private int _status = UNSENT;
private XDomainRequest _xhr;
void setStatus(int status) { _status = status; }
public IECrossSiteRequest(XDomainRequest xhr) {
if (xhr == null ) {
throw new NullPointerException();
}
_xhr = xhr;
}
public void cancel() {
if ( isPending() ) {
_xhr.abort();
}
}
public boolean isPending() {
return (_status == OPEN || _status == SENT);
}
}

public class IECrossSiteResponse extends Response {
private XDomainRequest _xhr;
public static class IEHeader extends Header {
private String _name;
private String _value;
public IEHeader(String name, String val) {
_name=name;
_value = val;
}
public String getName() { return _name; }
public String getValue() { return _value; }
}
public IECrossSiteResponse(XDomainRequest xhr) {
_xhr = xhr;
}

public String getHeader(String header) {
return header.equals("Content-Type") ? _xhr.getContentType() : null;
}
public Header[] getHeaders() {
if ( _xhr.getContentType() != null) {
Header ret[] = new Header[1];
ret[0] = new IEHeader("Content-Type",_xhr.getContentType());
return ret;
}
else {
return null;
}
}
public String getHeadersAsString() {
return ( _xhr.getContentType() == null ) ? ""
: ("Content-Type : " + _xhr.getContentType());
}
public int getStatusCode() {
return (_xhr != null) ? Response.SC_OK : Response.SC_BAD_REQUEST;
}
public String getStatusText() {
return "OK";
}
public String getText() {
return _xhr.getResponseText();
}
}

We can use deferred binding to replace RpcRequestBuilder, with IECrossSiteRpcRequestBuilder in the module definition for IE.
<module>
<inherits name='com.google.gwt.user.User'/>
...
<replace-with class="com.medrium.gwt.client.rpc.IECrossSiteRpcRequesrBuilder">
<when-type-is class="com.google.gwt.user.client.rpc.RpcRequestBuilder"/>
<when-property-is name="user.agent" value="ie8" />
</replace-with>
...
</module>

Cross site RPC calls can now be made by changing the URL on the async interface.
PatientConditionAsync pc = GWT.create(PatientCondition.class);
ServiceDefTarget target = (ServiceDefTarget)pc;
target.setServiceEntryPoint(crossSiteURL);
..

We still need to fix a few things on the server implementation, since the standard RemoteServiceServlet expects header information - Content-Type, X-GWT-Module-Base,X-GWT-Permutation. IE's XDomainRequest does not allow any headers so the standard service call in RemoteServiceServlet would reject the request. So we defined a new class that implementations which expect remote calls can derive from that get around this issue.

public class SecureRemoteServiceServletNoHeader extends
SecureRemoteServiceServlet {
public SecureRemoteServiceServletNoHeader() {}
protected void service(HttpServletRequest req,
HttpServletResponse res)
throws ServletException, IOException {
if ( req.getMethod().equals("POST") ) {
doPostNoHeader(req, res);
}
}
protected void doPostNoHeader(HttpServletRequest req,
HttpServletResponse res)
throws ServletException, IOException {
try {
synchronized(this) {
if ( perThreadRequest == null) {
perThreadRequest = new ThreadLocal();
}
if ( perThreadResponse == null) {
perThreadResponse = new ThreadLocal();
}
}
perThreadRequest.set(req);
perThreadResponse.set(res);
processPostNoHeader(req, res);
}
catch (Throwable e) {
RPCServletUtils.writeResponseForUnexpectedFailure(
getServletContext(), res, e);
}
finally {
perThreadRequest.set(null);
perThreadResponse.set(null);
}
}
public void processPostNoHeader(HttpServletRequest req,
HttpServletResponse res) throws ServletException, IOException,
SerializationException {
String payload = RPCServletUtils.readContentAsUtf8(req, false);
String respPayload = processCall(payload);
boolean gzipEncode = RPCServletUtils.acceptsGzipEncoding(req)
&& shouldCompressResponse(req, res, respPayload);
RPCServletUtils.writeResponse(getServletContext(),
res, respPayload, gzipEncode);
}
}

The model described here works for us on Chrome, Firefox, Safari, IE8 etc. On the server side we use Jetty and the Cross Origin Filter supplied by Jetty. On other web servers you can easily write a filter to accept the requests. On older browsers that do not support Cross Origin requests a redirect to the cross site URL may work (although we have not tried that). The redirect can be done easily using Servlet Filters.

7 comments:

  1. Hi. This is a good starting point for general CORS-Support for IE8 in GWT.

    I cannot find any licensing Information on your posts? Are there in a creative common licenses?

    Can i get your code post it on bitbucket.org with credits to you? I want to make some improvements on your implementation. What should the license be?

    Every not to restrictive license (like GPL,APGL) is fine for me.

    Kind regards
    Sebastian

    ReplyDelete
  2. Yes. They are under creative common license. Just added that to end of the page. So go ahead and use the code.

    ReplyDelete
  3. Prasad,

    Was JSONP considered as a viable option for addressing this problem?

    Also, can you comment on advantages/disadvantages of using proxy objects? Would performance degrade considerably due to the hand-off in both directions?

    ReplyDelete
  4. Regarding using JSONP - it works using script tags .. which does not fit very well with the GWT RPC model. Essentially we want very few changes to GWT client code and have the ability to use the same GWT client code and just change the Service Entry point to a different server.

    As far as using Proxy objects .. it sorts of defeats the purpose (at least in our case) of separting functionality into different servers. In addition, all calls have to go thru the same physical server causing potential bottlenecks/performance issues.
    I hope that in future even IE supports CORS - XMLHttpRequest instead of XDomainRequest for cross site requests, so that there is no need to write a special class for IE.

    ReplyDelete
  5. May This question is a little fool but:

    This source code is making use of XDomainRequest so it's not going to work on firefox or another XMLHttpRequest supported navegators? If thats the case, may I need to change a lot of code?

    Thanks for all, really great work!

    ReplyDelete
  6. Hi Francisco,
    XDomainRequest is used for IE and XMLHttpRequest is used for other browsers - firefox, chrome, safari .. which do support XMLHttpRequest. Deferred binding is used to replace the default RPCRequestBuilder with IECrossSiteRPCRequesBuilder for IE.

    ReplyDelete
  7. I'd like to try this to embed a GWT app into an existing page...and host the GWT app remotely. Is the code above available for download? The above code is missing the source for SecureRemoteServiceServlet. Also do you have a complete example, showing both client and server? From the code above it looks like you are providing CORS at the RPC junction. How would this be deployed? Somehow you have to get the client part of the GWT app into the browser, right?

    ReplyDelete