Un web proxy in Rack per cross-domain Ajax

In weLaika stiamo lavorando allo sviluppo di un social-network con un’architettura logica a due livelli: da una parte uno storage ultra-performante su Google App Engine, dall’altra una serie di differenti frontend per l’utente finale. Il primo frontend è quello web – al quale weLaika sta lavorando. Il secondo, e per ora ultimo, sarà realizzato con tecnologia Flash.

Tutti i frontend sono in grado di comunicare col backend tramite API. I metodi API sono stati suddivisi in “pubblici” e “privati”. Quelli pubblici sono in grado di rispondere con formato JSONP e possono dunque essere utilizzati da applicazioni di terze parti, le API private sono invece pensate solo ed esclusivamente per i frontend “ufficiali” ed è possibile accedervi solo mediante richieste Ajax pure, quindi da pagine all’interno dello stesso dominio del backend (per maggiori info, date un’occhio alla same-origin policy).

Tutto bellissimo e sensato, ma come lavorare sul frontend web in locale, con la possibilità effettuare richieste Ajax verso il backend? I browsers non ce lo permettono (se non mediante hack vari, e non sempre comunque)!

Cross-domain Ajax con Rack e Net::HTTP

Così come suggerito anche da Yahoo, la soluzione è semplice: il server locale deve poter intercettare tutte le richieste Ajax, capire quali sono quelle da inoltrare verso un server remoto, fingersi client con quest’ultimo passando i medesimi parametri ricevuti dal browser (cookie compresi), ricevere la risposta (header compresi) e inoltrare il tutto al browser. Phew.

Tutto ciò in realtà è moderatamente semplice da fare. Ecco qui un middleware Rack in grado di comportarsi esattamente in questo modo, sfruttando la libreria standard Net::HTTP:

rack_proxy.rb
1 require "net/http" 
2 
3 class Rack::Proxy 
4 def initialize(app, &block) 
5 self.class.send(:define_method, :uri_for, &block) 
6 @app = app 
7 end 
8 
9 def call(env) 
10 req = Rack::Request.new(env)
11 method = req.request_method.downcase 
12 method[0..0] = method[0..0].upcase 
13 
14 return @app.call(env) unless uri = uri_for(req) 
15 
16 sub_request = Net::HTTP.const_get(method).new("#{uri.path}#{"?" if uri.query}#{uri.query}") 
17 
18 if sub_request.request_body_permitted? and req.body 
19 sub_request.body_stream = req.body 
20 sub_request.content_length = req.content_length 
21 sub_request.content_type = req.content_type 
22 end 
23 
24 sub_request["Cookie"] = req.env["HTTP_COOKIE"] 
25 sub_request["Accept-Encoding"] = req.accept_encoding 
26 sub_request["Referer"] = req.referer 
27 sub_request.basic_auth *uri.userinfo.split(':') if (uri.userinfo && uri.userinfo.index(':'))
28 
29 http = Net::HTTP.new(uri.host, uri.port) 
30 
31 sub_response = http.start { |http| http.request(sub_request) } 
32 
33 headers = {} 
34 sub_response.each_header do |k,v| 
35 headers[k] = v unless k.to_s =~ /content-length|transfer-encoding/i 
36 end 
37 
38 [sub_response.code.to_i, headers, [sub_response.read_body]] 
39 end 
40 end

Il suo utilizzo è molto semplice, un esempio pratico (da lanciare per esempio con thin -R config.ru start):

config.ru
1 use Rack::Proxy do |req| 
2 if req.path =~ /api/ 
3 URI.parse("http://www.api-server.com#{req.path}#{"?" if req.query_string}#{req.query_string}") 
4 end
5 end 
6 
7 run Rack::Directory.new(".")

In questo caso, facciamo partire un server Rack che normalmente serve tutti i files contenuti nella directory corrente (mediante il fantastico Rack::Directory), ma il middleware Rack creato, prima di passare la palla a Rack::Directory, controlla se l’URL non contiene la stringa "api". In caso affermativo, si comporta da proxy, forwardando la richiesta HTTP ricevuta al server www.api-server.com, sul medesimo path.