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.