30 agosto 2016

CORS, apache e jQuery, ovvero "il triangolo di ajax"

Cosa sono le chiamate CORS? 

Le chiamate CORS (cross-origin HTTP request) sono da sempre una bestia nera di cui il programmatore medio del web sa poco o niente.
Nella sostanza si classificano come chiamate CORS tutte le chiamate a servizi che risiedono su un dominio differente da quello della pagina che origina la chiamata.

Se dalla splendida homepage del mio sito:
http://miosito.mio/index.html
voglio chiamare un servizio sul server del mio amico all'indirizzo:
http://serverdelmioamico.suo/servizio.php

La chiamata tipica ad un servizio tramite ajax con jQuery è:
  $.ajax("http://serverdelmioamico.suo/servizio.php",
    {data:jsondidati,
     success:function(data){ //qualcosa col risultato}
  });

questa chiamata ricade nella categoria CORS.
Cosa significa? Che il browser la effettua, la chiamata arriva al server che la elabora e invia la risposta, il browser riceve la risposta e....la scarta.
Perché? Perché di default le chiamate CORS sono disabilitate dal browser e dal server, per motivi di sicurezza.


Come si abilitano le chiamate CORS?

Servono 2 interventi, uno sul client e uno sul server. Il client deve dichiarare di fare una CORS, il server deve acconsentire al client dicendo esplicitamente che acconsente alla richiesta.
Spezziamo quindi l'intervento in 2 parti.

Abilitazione sul client (jQuery)

Il client aggiunge un parametro che consente a jQuery la corretta gestione della chiamata, in termini di header inviati e gestione della risposta. La chiamata usata sopra a titolo d'esempio, diventa:

  $.ajax("http://serverdelmioamico.suo/servizio.php",
    {data:jsondidati,
     success:function(data){ //qualcosa col risultato},
     xhrFields: {withCredentials: true }
  });

Perché questo funzioni è opportuno usare almeno la versione 1.5.2 di jQuery, cosa che, di questi tempi, è assai probabile, ma in caso di vecchio codice da estendere potrebbe non essere scontato.

Abilitazione sul server (Apache)

Prima di tutto è indispensabile abilitare il modulo headers di apache (a2enmod headers su debian e ubuntu).
A questo punto la via semplice, se tutto funziona, è aggiungere un header sul server. Assumendo che si tratti di apache 2.4, l'operazione è piuttosto facile. Nel VirtualHost, o nel file ".htaccess" basta aggiungere la riga:

Header set Access-Control-Allow-Headers "*"

I dettagli sono qui (http://httpd.apache.org/docs/current/mod/mod_headers.html)*.
Nella sostanza questa riga imposta all'header della risposta una informazione che dice: non importa chi mi chiama, a me va bene comunque, browser caro, mostra pure la risposta.

Questa operazione apre il servizio a tutti i chiamanti, senza restrizioni. Per fare in modo che le chiamate siano limitate a quelle provenienti da pagine del server "http://miosito.mio/", la riga dovrà essere:
Header set Access-Control-Allow-Headers "http://miosito.mio/"

Giusto per stare sicuri si possono aggiungere queste 2 righe

Header add Access-Control-Allow-Headers "origin, x-requested-with, content-type"
Header add Access-Control-Allow-Methods "PUT, GET, POST, DELETE, OPTIONS"

Che dovrebbero essere autoesplicative, ma nel dubbio dicono che si permette l'accesso se la chiamata arriva con uno degli header e uno dei metodi elencati fra virgolette.

Purtroppo l'intreccio delle regole di chiamata fa si che questa base non sia sufficiente se vogliamo che il chiamante non sia limitato ad una sola origine (cioè se siamo nel caso in cui ci serve "*" invece di un nome server specifico).
La richiesta withCredentials infatti richiede che Access-Control-Allow-Headers abbia come valore il server d'origine e non un wildchar ("*"), ma togliendola il server non manda l'header per abilitare la CORS.
Quindi serve un piccolo sforzo in più lato server, una volta per tutte.
Il server deve leggere il,l'origine del chiamante (comprensivo di protocollo e porta) e usarlo per impostare l'header di risposta.
Questa operazione, apparentemente ardua, si risolve in realtà con uno sforzo piuttosto piccolo (a sapere come), basta tramutare la nostra riga in:

SetEnvIfNoCase Origin "https?://(www\.)?(.*)(:\d+)?$" ACAO=$0
Header set Access-Control-Allow-Origin %{ACAO}e env=ACAO

Questa diavoleria legge l'header Origin, controlla se ha l'aspetto indicato (che poi è qualunque cosa inizzi con http:// o https:// e mette il valore in una variabile (ACAO, visto l'amore degli informatici per gli acronimi).
L'header a questo punto viene impostato con il valore della variabile ACAO, che poi è quello dell'origine.

Giusto per stare sul sicuro si può aggiungere

Header set Vary Origin

agli header restituiti, che serve a gestire alcune situazioni piuttosto particolari, ma visto che male non ne fa, tanto vale averlo.

Il blocco complessivo diventa:

SetEnvIfNoCase Origin "http(s)?://(www\.)?(.*)(:\d+)?$" ACAO=$0
Header set Access-Control-Allow-Origin %{ACAO}e env=ACAO
Header add Access-Control-Allow-Headers "origin, x-requested-with, content-type"
Header add Access-Control-Allow-Methods "PUT, GET, POST, DELETE, OPTIONS"
Header set Vary Origin


Ultima nota: se si vuole aprire ad una serie di server e non a tutti, la riga del SetEnvIfNoCase deve essere modificata di conseguenza. Per fare un breve esempio, se vogliamo che a chiamare possa essere miosito.mio con tutti i suoi domini di terzo livello (es: test.miosito.mio, prova.miosito.mio, blog.miosito.mio, ...) e serverdimiocugino.suo con i suoi domini di terzo livello, la riga diventerà:

SetEnvIfNoCase Origin "http(s)?://(.*\.)?(miosito\.mio|serverdimiocugino\.suo)(:\d+)?$" ACAO=$0

Si tratta (come l'altra, del resto) di una espressione regolare, che si legge più o meno così:
http con o senza "s" seguito da "://", eventualmente qualcosa, ma se c'è deve essere seguita da un ".", (come potrebbe essere "www.", "test." o altro) seguito da miosito.mio oppure serverdimiocugino.suo, seguito eventualmente da un ":" e dei numeri (indicazione della porta).


* Una piccola nota a riguardo per chi giustamente ha scelto di saltare il manuale: "Header set" imposta un header, mentre "Header add" lo aggiunge. Ai nostri fini la differenza è probabilmente nulla, ma i problemi sorgono quando da codice viene già impostato l'header in questione. Nel caso di "add" l'header viene raddoppiato (e alcuni client un po' permalosi non la prendono bene, specie se non hanno lo stesso valore), mentre "set" sovrascrive il valore. Esistono anche "setifempty", che imposta il valore solo se non era già presente e "merge" che prova ad estendere una lista di valori per gli header a valore multiplo.
La realtà è che manipolare gli stessi header a livello di codice di servizio e di configurazione del web server può portare a una serie di situazioni impreviste e richiede un coordinamento che difficilmente si mantiene nel tempo. Sarebbe buona norma quindi decidere a priori quali header siano da impostare a livello di apache e quali a codice, evitando sovrapposizioni, dove non sia strettamente necessario.

Nessun commento:

Posta un commento