Upload de fichier (cross-domain)

Ce sujet n’a rien de récent et plusieurs solutions existent depuis longtemps. Ces solutions varient selon notamment les conditions suivantes :

  • Compatibilité des navigateurs (notamment IE < 10)
  • Cross-domain: domaine ou sous-domaine différent ?

Ces deux conditions préfigurent les deux problèmes à résoudre pour mettre en oeuvre un upload de fichier à partir d’une page web, vers un serveur d’une origine différente.


Si vous êtes en train de développer et que les détails ne vous intéressent pas, vous pouvez sauter directement à la solution technique, plus bas.

Il y a beaucoup d’informations sur le sujet, bien qu’éparses. J’ai voulu dresser une petite synthèse, en prenant en compte les nouvelles possibilités d’HTML 5. L’exemple de mise en oeuvre fourni dans cette page est adapté à .NET WebAPI 2. Je pense que cet exemple est facilement transposable sur d’autres technologies, l’essentiel du travail étant fait côté client, en javascript.
J’aborde seulement les solutions les plus répandues à ce jour (en 2014), cet article n’est pas exhaustif sur toutes les techniques possibles pour le problème posé. Je pense aussi que les techniques présentées ici sont les plus simples.

L’origine du problème

XHR: La méthode standard avec XMLHttpRequest

La méthode « standard » pour déposer un fichier sur un serveur à partir d’un formulaire web est à travers un XMLHttpRequest. Dans ce cas, comme pour toute requête cross-domain, le serveur doit répondre avec une entête HTTP Access-Control-Allow-Origin. Ce n’est pas l’objet de cet article, donc si vous souhaitez en savoir plus sur le support des requêtes cross-domain sur WebAPI 2, lisez cet excellent article de Brock Allen dans MSDN Magazine.

Seulement voilà (problème n°1) : tous les navigateurs ne supportent pas l’upload via XMLHttpRequest (Internet Explorer à partir de la version 10). À ce propos, un excellent site pour connaître la compatibilité d’une fonctionnalité avec les principaux navigateurs: Can I Use ?

La solution alternative est d’utiliser une iframe masquée, à partir de la page HTML contenant le formulaire d’upload.

iframe transport: le « hack » (et non le bal) de l’iframe masquée

L’expression iframe-based Ajax transport représente une technique qui consiste à utiliser un élément HTML iframe comme proxy pour communiquer avec un serveur HTTP. Cette technique est plus ancienne que XMLHttpRequest. Outre le fait que certains navigateurs ne supportent pas l’upload de fichiers via XMLHttpRequest, iframe transport est un bon candidat dans certains scénarios de communication car l’iframe est autorisée à recevoir des données en provenance d’une autre origine que sa page parente (sans impliquer CORS). Il y a cependant un autre problème qui se posera, mais avant d’y venir, je termine sur la technique iframe transport pour l’upload de fichier.

Le principe consiste à utiliser l’iframe comme proxy pour obtenir la réponse du serveur dans le contenu de l’iframe (généralement sous forme d’un Content-Type: text/html pour éviter que le navigateur n’interprète la réponse comme un fichier à télécharger).

L’upload de fichier doit toujours se faire à partir du formulaire web, de la même façon que sans iframe à ceci près que l’on ajoute un attribut target au formulaire afin de pointer l’iframe. Ainsi, le formulaire soumet les données au serveur via une requête POST et la réponse est affichée dans l’iframe.

Seulement voilà (problème n°2) : les navigateurs n’autorisent pas une page HTML à accéder au contenu d’une iframe ciblant un domaine différent de la page parente. Pour être précis, il n’est pas permis à une fenêtre (représentée par l’objet window en javascript, qui est le parent de l’objet document) d’accéder au contenu d’une autre fenêtre si celle-ci contient des données ayant une « origine » différente. Une fenêtre window faisant référence à la page HTML parente ou à une de ses iframes (autrement dit il y a un objet window pour la page HTML, et un autre pour une de ses iframes). Une origine est différente si le domaine (sous-domaine inclus) est différent, ou si le protocole est différent (http, https), ou encore si le port est différent (http://domain.com/page, http://domain.com:8080/page).
Une dernière façon de le dire: le code Javascript qui s’exécute dans un document HTML n’est pas autorisé à interagir avec un autre document HTML si son contenu n’est pas transmis par la même « origine ». Noter que l’important est l’origine du document HTML, et non l’origine du code exécuté (sinon il serait difficile d’utiliser des librairies tierces comme Google Analytics par exemple). Comme ces règles concernent la partie cliente de javascript, le support de CORS, qui implique les échanges avec un serveur, n’est pas une solution applicable.

Communiquer avec le contenu d’un autre sous-domaine

Si la différence d’origine est limitée à un sous-domaine différent (entre la page qui exécute le javascript et l’iframe qui contient la réponse), une solution simple est d’utiliser la propriété javascript [window.]document.domain. Si cette propriété est identique entre les deux documents, alors le code qui s’exécute dans l’un peut accéder au contenu de l’autre. Évidemment, il n’est pas autorisé de définir une valeur arbitraire: il doit s’agir d’une partie valide du domaine réel. Cette solution n’est donc pas viable pour un upload vers un domaine complètement différent de la page (plus précisément pour lire dans l’iframe la réponse ayant pour origine un domaine différent).

Obtenir la réponse grâce à HTML 5 Messaging API

HTML 5 propose une nouvelle API de communication inter-documents. Celle-ci est basée sur des messages asynchrones. Bien que très sexy pour nous, développeurs, mon sentiment premier est que cette fonctionnalité n’est pas un choix judicieux pour le problème qui nous occupe. En effet, le vrai problème n’est pas la communication entre deux documents mais le non support par certains navigateurs de XMLHttpRequest pour l’upload de fichiers. Si notre objectif est donc de supporter les navigateurs les plus anciens, le meilleur choix n’est pas de s’appuyer sur une technologie implémentée par les navigateurs les plus récents.
Cependant, il est à noter que cette solution semble viable à partir d’IE 8.

Obtenir la réponse grâce à une redirection

C’est la solution la plus inter-opérable, mais elle est limitée à une réponse de taille réduite. Le principe consiste à :

  • Demander au serveur auquel le fichier est envoyé de retourner sa réponse sous forme d’une redirection d’URL.
  • Le serveur devra ajouter en paramètre de l’URL de redirection sa réponse (encodée dans l’URL). L’intégralité de l’URL (avec la réponse encodée) ne devrait pas dépasser 2000 caractères.
  • L’URL de redirection doit cibler une page HTML existante, qui contient un script. Ce script interprètera la réponse à partir du paramètre d’URL. Comme la page est hébergée sur le même site que la page à l’origine de l’upload, le javascript qui s’exécute sur cette dernière peut accéder au contenu de l’iframe, et donc de la réponse.

Schema

Exemple de mise en oeuvre

Il y a différentes façons d’implémenter cet échange, aussi je me contenterai de proposer un exemple de mise en oeuvre, basée sur la librairie javascript jQuery-File-Upload et sur un serveur WebApi 2 avec OWIN.

Le projet est sur GitHub.

Il s’agit d’un programme console qui héberge deux serveurs web avec deux ports différents. Cela implique qu’ils ne sont pas reconnus comme servant des ressources de la même origine. Le premier serveur héberge deux pages, l’une contenant le formulaire d’upload, l’autre pour servir de proxy dans l’iframe. Le second serveur prend en charge l’upload proprement dit.

Le répertoire /Resources contient les pages d’exemples :

  • upload-form.html : formulaire basique démontrant le fonctionnement de cette technique.
  • upload-app.html : exemple « réel » avec la librairie jQuery-File-Upload. N’utilisez pas directement cet exemple: j’ai forcé l’utilisation de l’iframe et de la redirection. Consultez les options de cette API.
  • result.html : page proxy pour recevoir la réponse sous forme de redirection d’URL dans l’iframe. Cette page est est fournie par la librairie décrite au dessus.

L’upload côté serveur est géré par la classe UploadController.

En conclusion…

Ce qui me semble le plus viable (support d’IE 6) :

  • Côté client:
    • Utiliser une librairie javascript qui supporte l’upload via un formulaire multipart/form-data, avec support d’XHR et iframe en fonction des capacités du navigateur utilisé.
  • Côté serveur:
    • Évidemment le support de CORS.
    • Retourner la réponse avec un Content-Type: text/html quel que soit le type réel du contenu (le plus souvent JSON).
    • Supporter un champ de formulaire spécial permettant au client de définir l’URL de redirection à utiliser pour retourner la réponse sous forme encodée (dans cette URL).

Références