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:
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):
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.