Se noi inviamo una richiesta fetch al di fuori del nostro sito, è probabile che la richiesta fallirà.
Per esempio, potremmo provare con http://example.com:
try {
await fetch('http://example.com');
} catch(err) {
alert(err); // fetch fallita!
}
La richiesta fetch è fallita, come supponevamo.
Il concetto chiave è l’origin – la tripletta di valori dominio/porta/protocollo.
Cross-origin requests – sono le richieste inviate ad un altro dominio (o anche un sottodominio), protocollo o porta – queste richieste necessitano di speciali headers server-side.
Questa policy è chiamata “CORS”: Cross-Origin Resource Sharing.
Perché CORS è necessaria? Una breve storia
CORS è pensata per proteggere internet dagli hackers malvagi.
Davvero! Facciamo una breve digressione storica.
Per molti anni uno script da un sito non poteva accedere al contenuto di un altro sito.
Questa semplice ma potente regola era una delle fondamenta della sicurezza di internet. Per esempio uno script malevolo dal sito hacker.com non avrebbe potuto accedere alla mailbox di un utente sul sito gmail.com. Le persone erano al sicuro.
JavaScript inoltre non aveva alcun metodo particolare per eseguire richieste remote a quel tempo. Era ancora un linguaggio utile solo a decorare le pagine web.
Ma gli sviluppatori web necessitavano di più potere! Una serie di trucchi furono inventati per arginare queste limitazioni ed eseguire richieste remote ad altri siti web.
Utilizzando i forms
Un modo per comunicare con un altro server era eseguire l’invio di un <form>. L’utente sottometteva il form in un <iframe>, così da restare nella pagina corrente, come ad esempio:
<!-- form target -->
<iframe name="iframe"></iframe>
<!-- un form può essere generato in modo dinamico ed inviato da JavaScript -->
<form target="iframe" method="POST" action="http://another.com/…">
...
</form>
Così era possibile eseguire richieste GET/POST ad un altro sito, anche senza metodi specifici per le richieste di rete, dato che i forms possono inviare i dati ovunque. Ma poiché è vietato accedere al contenuto di un <iframe>da un altro sito, non è possibile leggere la risposta.
Per essere precisi, era presente un trucco per arginare questo problema, richiedendo script speciali sia nell’iframe che nella pagina. Quindi la comunicazione con l’iframe era tecnicamente possibile, ma in questo momento non ha senso entrare nei dettagli e quindi lasciamo che questi dinosauri riposino in pace.
Utilizzando gli scripts
Un altro trucco era l’uso del tag script. Uno script può avere qualunque src e con qualunque dominio, come ad esempio <script src="http://another.com/…">. È quindi possibile eseguire uno script da qualunque sito.
Ad esempio un sito, come another.com, potrebbe esporre dati per questo tipo di accesso, utilizzando un protocollo chiamato “JSONP (JSON with padding)”.
Ecco come funziona.
Diciamo che, sul nostro sito, dobbiamo ottenere le informazioni sul tempo da http://another.com:
-
Per prima cosa, dichiariamo una funzione globale che accetti i dati, ad esempio
gotWeather.// 1. Dichiariamo la funzione per processare i dati meteo function gotWeather({ temperature, humidity }) { alert(`temperatura: ${temperature}, umidità: ${humidity}`); } -
Successivamente scriviamo un tag
<script>consrc="http://another.com/weather.json?callback=gotWeather", usando il nome della funzione come valore del parametrocallbackdell’url.let script = document.createElement('script'); script.src = `http://another.com/weather.json?callback=gotWeather`; document.body.append(script); -
Il server remoto
another.comgenererà dinamicamente uno script che chiameràgotWeather(...)con i dati che vogliamo ricevere.// La risposta attesa da server dovrebbe essere ad esempio: gotWeather({ temperature: 25, humidity: 78 }); -
Quando lo script remoto sarà caricato ed eseguito,
gotWeatherverrà eseguita e, poiché è la nostra funzione, otterremo i dati.
La soluzione funziona e non viola la sicurezza, dato che che entrambe le parti consento il passaggio dei dati in questa modalità. E quando entrambe le parti acconsento, non si tratta di un hack. Esistono ancora servizi che forniscono tale accesso, poiché funziona anche con browser molto vecchi.
Dopo un po’, arrivarono nei browser i metodi JavaScript per le richieste di networking.
Inizialmente le cross-origin requests erano proibite. Ma dopo lunghe discussioni, le cross-origin requests furono consentite, ma qualsiasi funzionalità richiede una esplicità autorizzazione dal serve, indicate per mezzo di speciali headers.
Safe requests
Ci sono due tipi di cross-origin requests:
- Simple requests.
- Tutte le altre.
Le Simple Requests sono le più semplici da realizzare e quindi inizieremo da queste.
Una simple request è una richiesta che soddisfa due condizioni:
- Simple method: GET, POST or HEAD
- Simple headers – gli unici custom headers consentiti sono:
Accept,Accept-Language,Content-Language,Content-Typecon il valoreapplication/x-www-form-urlencoded,multipart/form-dataotext/plain.
Ogni altra request è considerata “non-simple”. Per esempio, una request con il metodo PUTo con l’HTTP-header API-Key non rispettano queste limitazioni.
La sostanziale differenza è che una “simple request” può essere fatta con un <form> o un tag <script>, senza alcun metodo speciale.
Quindi, anche un server molto vecchio dovrebbe essere in grado di accettare una simple request.
Differentemente, requests con headers non-standard o ad esempio con metodo DELETE non possono essere create in questo modo. Per molto tempo JavaScript non è stato in grado di fare tali request. Quindi un server più vecchio può presumere che questo tipo di richieste provengono solo da fonti privilegiate, “perché una pagina web non sarebbe in grado di inviarle”.
Quando proviamo a fare una unsafe request, il browser invia una speciale request “preflight” che chiede al server – accetti questo tipo di richieste cross-origin, o no?
E, a meno che il server non confermi esplicitamente con degli headers, la unsafe request non sarà inviata.
Andiamo ad analizzare i dettagli.
CORS per le simple requests
Se una request è cross-origin, il browser aggiunge sempre un header Origin ad essa.
Per esempio, se richiediamo https://anywhere.com/request da https://javascript.info/page, gli headers dovrebbero essere:
GET /request
Host: anywhere.com
Origin: https://javascript.info
...
Come potrai notare, l’header Origin contiene esattamente l’origine (dominio/protocollo/porta), senza il path.
Il server può valutare quindi l’header Origin e, se il server è autorizzato ad accettare questo tipo di request, aggiunge uno speciale header Access-Control-Allow-Origin alla response. Questo header dovrebbe contenere le origin autorizzate (nel nostro caso https://javascript.info), o un asterisco *. Quindi la response ha esito positivo, altrimenti verrà comunicato un errore.
Il browser gioca un ruolo di mediatore che garantisce tra le parti:
- Assicura che il contenuto
Originè inviato con una cross-origin request. - Controlla se esiste il permesso
Access-Control-Allow-Originnel response, e quindi in questo caso JavaScript sarà autorizzato ad accedere alla response, altrimenti fallirà con un errore.
Ecco un esempio di una risposta autorizzata dal server:
200 OK
Content-Type:text/html; charset=UTF-8
Access-Control-Allow-Origin: https://javascript.info
Response headers
Per le cross-origin request, di default JavaScript può solo accedere alle così dette “safe” response headers:
Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma
L’accesso ad ogni altra response header causerà un errore.
Content-LengthNota che nella lista non è contemplata l’header Content-Length!
Questa header contiene la dimensione completa del response. Così, se dovessimo scaricare qualcosa e desidereremmo tracciare la percentuale di progresso, sarebbe necessario un permesso addizionale per accedere a questo header (come potrai vedere sotto).
Per garantire l’accesso a JavaScript ad ogni altro response header, il server deve inviare l’header Access-Control-Expose-Headers. Esso contiene una lista separata da virgole dei nomi degli unsafe headers ai quali è possibile accedere.
Per esempio:
200 OK
Content-Type:text/html; charset=UTF-8
Content-Length: 12345
API-Key: 2c9de507f2c54aa1
Access-Control-Allow-Origin: https://javascript.info
Access-Control-Expose-Headers: Content-Length,API-Key