20 jun 2011

Evitando SOP en GWT

Cuando desarrollamos aplicaciones web una de las tares mas básicas es la comunicación cliente servidor, y si bien GWT nos proporciona diferentes mecanismos (RPC, JSONP, etc) , uno de los mas habituales es la realización de peticiones HTTP al servidor.
Para esto se nos proporciona la clase HTTP cliente mediante la cual creamos y recibimos requests, siempre bajo las restricciones que impone la política de SOP (same origin policy) de los navegadores, que evita que el código cliente interactue con recursos de otros dominios previniendo potenciales problemas de seguridad.
Por otro lado, una de las formas mas prácticas de desarrollar en GWT es el llamado modo desarrollo, en el cual las aplicaciones corren sobre la máquina virtual de Java, pero quedamos restringuidos a realizar peticiones HTTP al dominio en el cual estamos ejecutando, del tipo "http://127.0.0.1:8888/MyApp.html?gwt.codesvr=127.0.0.1:9997".
Dicho esto nada mas cómodo que trabajar en modo desarrollo y realizar desde ahí las consultas al server, pero lamentablemente estos dos panoramas son totalmente incompatibles, quizás podríamos crear resultados de retorno simulados para las peticiones, también compilar todo cada tanto para verificar que todo funciona como esperamos, pero aun así cualquiera de las dos opciones es bastante impráctica, sobre todo compilar que ya de por si se lleva su tiempo, otra posible solución es cambiar todo esto por el uso de JSONP, pero no es lo que queremos, eso es solo otro hack al navegador, yo quiero Modo Desarrollo mas peticiones HTTP.

Leyendo precisamente en Wikipedia sobre SOP, encontré un enlace a una técnica un poco desconocida llamada CORS (Cross-Origin Resource Sharing) la cual habilita la posibilidad de realizar peticiones evitando el SOP de los navegadores, y claro, vi la luz al final del tunel.
Básicamente se trata de agregarle a la respuesta del servidor una serie de cabeceras que especifiquen el uso de esta técnica, soportada por varios navegadores modernos (ok, ok,  solo probé en Chrome y FF4, si alguien desarrolla con otro browser largo de aquí).

Para ver como funciona, realizaremos una aplicación de prueba con GWT y Python, recuerden que el uso de esta técnica es solo para desarrollo, nunca para codigo en produccion.
Este metodo fue probado con Python sobre WSGI y PHP, los navegadores son Firefox 4.0 y Chrome 6.0.472.63 sobre una caja Kubuntu 10.04.2 LTS.

Primero, abrimos Eclipse y realizamos un nuevo proyecto GWT, vacío, asi observamos bien el funcionamiento de esta técnica, luego creamos una petición HTTP (recordar de agregar al archivo XML del módulo la linea  <inherits name="com.google.gwt.http.HTTP" />) y la apuntamos a un dominio diferente al que corremos la aplicación, quedando de tal manera:


package cors.test.client;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.RequestException;
import com.google.gwt.http.client.Response;
import com.google.gwt.http.client.URL;
import com.google.gwt.user.client.Window;
/**
* Entry point classes define onModuleLoad().
*/
public class CORS_test implements EntryPoint {
 public void onModuleLoad() {
  
  String url = "http://localhost/CORS_test.wsgi";
  RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, URL.encode(url));
  
  try{
   Request request = builder.sendRequest(null, new RequestCallback() {
    
    @Override
    public void onResponseReceived(Request request, Response response) {
     
     if(200 == response.getStatusCode()){
      Window.alert(response.getText());
     }
     else{
      // handle the error
      Window.alert("failed petition");
     }
    }
    
    @Override
    public void onError(Request request, Throwable exception) {
     Window.alert("CORS... sure");
    }
   });
   
  }
  catch(RequestException e){
   Window.alert("Couldn't connect to server");
  }
  
 }
}

Luego, creamos el archivo de respondera a la petición, en este caso CORS_test.wsgi y lo colocamos en nuestro localhost:

def application(environ, start_response):
    status = '200 OK'
    response_headers = [('Content-type','text/plain')]
    start_response(status, response_headers)
    return ['Hello world from Python!']

apuntamos nuestro navegador a http://localhost/CORS_test.wsgi y debemos ver el mensaje "Hello world from Python!"
Ahora, corremos nuestra aplicación GWT en modo desarrollo, recibimos el siguiente mensaje: "failed petition", efectivamente SOP nos esta bloqueando las peticiones, por lo que agregamos las siguientes cabeceras en la respuesta:

# CORS_test.wsgi

def application(environ, start_response):
    status = '200 OK'
    response_headers = [
        ('Content-type','text/plain'),
        ('Access-Control-Allow-Origin','http://127.0.0.1:8888'),
        ('Access-Control-Allow-Methods','GET, POST')
    ]
    start_response(status, response_headers)
    return ['Hello world from Python!']

y probamos nuevamente, obteniendo el mensaje: "Hello world from Python!"
Fácil verdad, lo importante es que la cabecera "Access-Control-Allow-Origin" especifique nuestro dominio, en este caso "127.0.0.1:8888", aunque también podríamos haber colocado *  e igual funcionaría, ahora toca en nuestro querido PHP, recordemos cambiar el valor de la URL por: String url = "http://localhost/CORS_test.php".

# CORS_test.php

<?php

header("Content-type:text/plain");
header("Access-Control-Allow-Origin:http://127.0.0.1:8888");
header("Access-Control-Allow-Methods:GET, POST");
echo "Hello world! from PHP";

?>

cuidado con los espacios en blanco de la etiqueta header, pueden hacer que la aplicación falle, ahora al correrlo veremos el mensaje: "Hello world! from PHP".
Bueno, esta es la técnica básica, los invito a leer la  especificación  , y ahora si: podemos volver a desarrollar dignamente.

No hay comentarios: