Cross-origin resource sharing (CORS) est une spécification W3C, qui autorise les requêtes Cross-Domain. Elle permet de gérer les accès à une ressource sur un serveur, lié à un domaine, par un script provenant d’un serveur lié à un autre domaine.

La spécification CORS n’est pas supportée par tous les navigateurs (Wikipedia)

Le standard CORS fonctionne à l’aide du champ « Access-Control-Allow-Origin » qui est rajouté dans le header de la requête HTTP. Ce champ permet de vérifier si les scripts provenant du domaine d’origine ont le droit d’accéder à des ressources depuis le serveur demandé.

La spécification permet au navigateur de faire des preflight , si nécessaire. Ces dernières sont des requêtes de vérification de droits d’accès, avant de faire la requête cross-origin. Elles utilisent pour cela une requête HTTP de type OPTION dans laquelle est insérée le domaine d’origine ainsi que des headers supplémentaires optionnels.

Le serveur valide l’accès en envoyant une réponse à la requête preflight, contenant dans son header toutes les règles nécessaires pour récupérer la ressource. Le serveur spécifie également dans sa réponse si une authentification est nécessaire ou pas.

Schémas des requêtes pour récupérer une ressource en Cross-Domain

 

Exemple du header de la requête OPTION Preflight

 

Réponse du serveur suite à la requête Preflight

Pour l’implémentation de CORS sur un serveur il suffit d’ajouter le champs l’Access-Control-Allow-Origin dans le HTTP Header.

Si ce header est « Access-Control-Allow-Origin : * » cela signifie que tous les scripts provenant de n’importe quel domaine ont accès au Body de la réponse HTTP. Ceci peut faciliter les attaques de type cross-site scripting (XSS) de la part de sites malveillants, c’est pour ça qu’il est conseillé de définir votre Access-Control-Allow-Origin ainsi :

« Access-Control-Allow-Origin: http://domain1.com:8080 http://www.domaine2.com »

Exemple d’utilisation de CORS pour l’upload de fichiers sur AWS S3

Dans cet exemple nous allons Uploader des fichiers depuis le navigateur  vers un Bucket S3, en utilisant JavaScript, en exploitant la nouvelle fonctionnalité CORS proposée par AWS pour son service S3 (Simple Storage Service).

Pour cela nous allons, en plus, exploiter deux autres technologies:

  • FileAPI : Spécification HTML5 permettant l’interaction avec des fichiers, coté client.
  • XHR2 : XMLHTTPRequest2 objet permettant de récupérer du XML, JSON,  HTML ou texte via requêtes HTTP.

Pour commencer, il faut configurer son Bucket S3 pour ajouter les règles CORS :

<CORSConfiguration>
    <CORSRule>
        <AllowedOrigin>*</AllowedOrigin>
        <AllowedMethod>PUT</AllowedMethod>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
</CORSConfiguration>

Ceci est la configuration minimale, pour activer le Cross-Domain depuis tous les domaines, pour les requêtes PUT, avec tous les header.

Pour plus de sécurité, il est nécessaire d’enrichir la configuration de CORS : Doc S3 CORS

Pour le HTML de vote page il faut un input de type file et un EventListener sur le changement d’état de ce dernier.

<inputtype="file"id="files"name="files[]"multiple/>
<script type="text/javascript">
document.getElementById('files').addEventListener('change', handleFileSelect, false);
</script>

Pour le script JavaScript qui va réaliser le XHR

function handleFileSelect(evt) //event trigé lors du choix d'un ou plusieurs fichiers 
{
  var files = evt.target.files; 
  for (var i = 0, f; f = files[i]; i++) 
  {executeOnSignedUrl(f, function(signedURL) 
              {uploadToS3(f, signedURL);});
  }
}
function executeOnSignedUrl(file, callback) //requete vers le script qui s'execute coté serveur pouur recuperer les URLs signé S3
{
  var xhr = new XMLHttpRequest();
  xhr.open('GET', 'sign?name=' + file.name + '&type=' + file.type, true); //la vue qui genere l'url signé s'appelle sign
  xhr.overrideMimeType('text/plain; charset=x-user-defined');
  xhr.onreadystatechange = function(e) 
  {
    if (this.readyState == 4 && this.status == 200) 
    callback(decodeURIComponent(this.responseText));
    else if(this.readyState == 4 && this.status != 200)
    alert('Could not contact signing script. Status = ' + this.status);
  };
  xhr.send();
}
function uploadToS3(file, url)
{
  var xhr = createCORSRequest('PUT', url);// creation de l'objet xhr CORS
  if (!xhr) 
    alert('CORS not supported'); //navigateur ne supporte pas CORS
  else
  {
    xhr.onload = function() 
    {
      if(xhr.status == 200)
      alert('Upload completed.');
      else
      alert('Upload error: ' + xhr.status);
    };
    xhr.onerror = function() 
    {alert('XHR error.');};
    xhr.setRequestHeader('Content-Type', file.type);
    xhr.setRequestHeader('x-amz-acl', 'public-read');
    xhr.send(file); //envoi du fichier
  }
}
function createCORSRequest(method, url) 
{
  var xhr = new XMLHttpRequest();
  if ("withCredentials" in xhr) 
    xhr.open(method, url, true);
  else if (typeof XDomainRequest != "undefined") 
  {
    xhr = new XDomainRequest();
    xhr.open(method, url);
  } 
  else
  {
    xhr = null;
  }
  return xhr;
}

Et finalement le script exécuté coté serveur pour générer l’url signé (peut être fait coté client mais très déconseillé de laisser trainer ses Credentials AWS entre les mains de n’importe qui).

public String Upload(String name, String type){
      String S3_KEY="VotreS3Key";
        String S3_SECRET="VotreS3Secret";
        String S3_BUCKET="/VotreBucketName"; 
        long EXPIRE_TIME=(60 * 5); // 5 minutes
        String S3_URL="http://s3.amazonaws.com";
        String objectName="/" + name; //nom de l'objet dans le bucket S3
        String mimeType=type; //mime type recupéré depuis la requete XMLHTTPRequest
        long expires = (System.currentTimeMillis()/1000) + EXPIRE_TIME;
        String amzHeaders= "x-amz-acl:public-read";
        String stringToSign = "PUT\n\n"+mimeType+"\n"+expires+"\n"+amzHeaders+"\n"+S3_BUCKET+objectName;        
        //Debut algorithme de signature d'URL pour AWS S3.
            Mac mac = Mac.getInstance("HmacSHA1");
            SecretKeySpec secret = new SecretKeySpec(S3_SECRET.getBytes(),"HmacSHA1");
            mac.init(secret);
            byte[] digest = mac.doFinal(stringToSign.getBytes());
            digest= Base64.encodeBase64(digest);  
            String sig = URLEncoder.encode(new String(digest),"UTF-8");  
            String url = URLEncoder.encode(S3_URL+S3_BUCKET+objectName+"?AWSAccessKeyId="+S3_KEY+"&Expires="+expires+"&Signature="+sig,"UTF-8");
        //Fin algorithme de signature d'URL pour AWS S3.
        return (url);
    }

Vous pouvez également retrouver les sources de notre petit Sample AWS-CORS utilisant l’excellent Framework SCALA/JAVA Play 2.0, avec d’autres fonctionnalités telles que la Progress bar sur le GitHub NEOXIA : https://github.com/neoxia/AWS-CORS