Déploiement de pages ASP.NET avec Apache : Du rêve à la réalité

ans l'article précédent sur l'outil WebMatrix, nous évoquions la possibilité d'une cohabitation entre Apache et Cassini, le serveur ASP.NET léger de Microsoft. Si personne n'osait jusqu'alors réellement croire à cette perspective, cet article nous démontre qu'elle représente aujourd'hui une réalité inébranlable. 

Qui aurait cru il y a encore quelques mois que le numéro un des serveurs Web Open Source pourrait cohabiter avec son ennemi juré ? Le Duo Apache-ASP.NET, représente en quelque sorte le mariage de l'eau et du feu, du monde libre et de l'univers commercial ou simplement d'un serveur web d'exception associé à un Framework d'avenir.

L'idée de ce mariage indécent nous a trotté dans la tête dès lors que nous avions eu Cassini et WebMatrix entre nos mains. Nous savions qu'il existait de nombreuses similitudes entre Tomcat et Cassini. Alors que Tomcat coopère sans la moindre difficulté avec Apache, pourquoi en serait-il autrement avec le supposé mini serveur Web de Microsoft ?

Non seulement cet article vous donne la recette vous permettant de configurer Apache et Cassini, mais vous livre par la même occasion les secrets les plus intimes sur l'utilisation du Hosting ASP.NET. Vous découvrirez ainsi que la réalisation d'un serveur de pages ASP.NET minimal avec le Framework peut s'écrire en 10 lignes de codes ! Enfin, nous verrons que certaines idées reçues à propos de Cassini s'avèrent totalement infondées, preuves à l'appui. Vous n'êtes pas au bout de vos surprises ...

Configuration d'Apache 2.0.39 

Depuis très récemment, Apache est disponible en version 2.X sur le site http://httpd.apache.org. Nous nous sommes basés sur cette version afin de réaliser nos tests. Une fois l'outil téléchargé, l'objectif consiste à mettre en place une redirection par proxy ou encore proxy_redirect. Le schéma suivant nous illustre le principe de fonctionnement général de la redirection par proxy. 

Lorsqu'un utilisateur émet une requête de type ASPX à destination du serveur Web, les opérations suivantes sont réalisées :

  1. Apache récupère sur le port 80 le répertoire virtuel spécifié dans la requête. 

  2. Si le répertoire virtuel correspond à celui qui se trouve dans son fichier de configuration, en l'occurrence /aspnet, il redirige la requête vers le serveur chargé de comprendre cette requête, en l'occurrence ici Cassini sur le port 8080.

  3. Pour finir, la requête est traitée par Cassini qui renvoie simplement la réponse sous la forme d'un flux HTML à Apache. Le flux est ensuite redirigé au client qui se charge de l'afficher par le biais de son navigateur. 

 

Dans cette configuration, le serveur Apache tourne dans un processus et Cassini aussi. Il n'y a donc aucun risque qu'une erreur fatale dans un des processus n'entraîne une chute des deux services.

Pour mettre en place une telle mécanique, il est nécessaire de modifier le fichier de configuration d'Apache : HTTP.CONF, dans le but d'y insérer les entrées correspondantes au Proxy Redirect. Pour ce faire, il suffit simplement de dé-commenter les modules mod_proxy et mod_rewrite et d'ajouter les lignes suivantes :

# Configuration Cassini 
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_connect_module modules/mod_proxy_connect.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule rewrite_module modules/mod_rewrite.so

# Configuration Cassini 
ProxyPass /aspnet http://127.0.0.1:8080/
ProxyPassReverse /aspnet http://127.0.0.1:8080/

httpd.conf

Bien entendu, Cassini et Apache doivent se trouver sur la même machine sinon la protection du loopback nous rappellerait aussitôt à l'ordre.

Une fois le fichier HTTP.CONF modifié, il suffit de lancer Apache et le serveur ASP.NET Cassini.

Tout d'abord, pour tester Cassini seul, rien de plus simple. Si vous avez préalablement créé un fichier test.aspx à la racine de cassini (chez nous inetpub/wwwroot), lancez Internet Explorer avec l'URL suivante : http://localhost:8080/Test.aspx
Ensuite, nous nous servons du même fichier à l'intérieur duquel nous avons ajouté quelques mots pour Apache. Les copies d'écrans suivantes nous illustre les deux modes de fonctionnement. Jugez-en par vous-même, l'intégration marche à la perfection ! 

Impressionné ? on le serait à moins. Qui aurait cru qu'un jour les ASP.NET cohabiteraient aussi simplement avec Apache ... 

La protection du loopback vole en éclat avec Apache

Malgré tout, il subsiste encore énormément de points obscurs. Il est bien mentionné dans la documentation officielle de WebMatrix que les requêtes provenant d'une source extérieure à la machine hébergeant le serveur web seraient purement et simplement rejetées pour s'assurer que seule une utilisation locale de Cassini serait faite. Cette opération est plus communément appelée "Protection du Loopback". D'ailleurs, comme nous le montre cette copie d'écran, nous avons essayé d'appeler une page ASPX à partir d'un navigateur externe et l'opération s'est soldée par un échec (Forbiden).

Intrigués mais non résignés, nous nous lançons dans le test consistant à vérifier cette affirmation lorsque Apache est configuré comme intermédiaire. Nous effectuons l'appel de la page ASPX à partir d'une machine située sur le même réseau. Voici les résultats .... pour le moins...stupéfiant ! 

 A gauche, le test effectué à partir d'une machine externe d'adresse IP 192.168.0.1. A droite, la machine hébergeant le serveur Cassini d'adresse IP 192.168.0.61 (ou localhost). Alors que nous aurions pu nous attendre au fameux message Forbidden, le serveur accepte sans le moindre problème notre requête externe. Un rapide coup d'œil sur les traces du flux HTTP semble indiquer qu'Apache, avec son opération de proxy redirect et de manière totalement involontaire, masque à Cassini le fait que la requête est externe. Plus précisément, il déporte cette information cruciale pour la protection du loopback dans un entête HTTP totalement inconnu jusqu'alors de Cassini : X-Forwarded-For. Les équipes de Microsoft auraient-elles omis de tester l'association Cassini+Apache ? Tout semble l'indiquer ;-) ...

Toujours est-il qu'à ce stade, nous sommes en présence d'une configuration fonctionnelle mais totalement déconseillée par Microsoft dans le cadre d'une mise en production. Les raisons évoquées par l'éditeur de Redmond à travers le site www.asp.net sont multiples :

Pourtant, à y regarder de près, toutes ces affirmations semblent contradictoires avec un certain nombre de faits ayant attirés notre attention. Tout d'abord, comment serait-il possible de faire tenir un moteur de pages ASP.NET dans 40Ko de code ? Pour ceux qui connaissent la complexité du Framework ASP.NET, même une version allégée aurait du mal à tenir dans si peu d'espace. Par ailleurs, quelles sont techniquement les caractéristiques qui font qu'un serveur Web serait moins performant ou moins sécurisé qu'un autre ? Y-a t-il de la part de Microsoft une volonté délibérée de brider ce mini serveur Web ou tout simplement cette nouvelle version n'intègre aucune notion de sécurité et de performances ? Bref, un certain nombre de questions auxquelles la suite de l'article trouvera des réponses, pour le moins surprenantes ...

La vérité à propos de Cassini ...

Avoir de l'information détaillée sur Cassini une semaine après sa sortie officielle est une opération difficile. Il existe bel et bien un livre édité par l'équipe de développement de l'outil WebMatrix, mais rien n'y évoque les caractéristiques techniques de Cassini. 

Pourtant, au fin fond des abîmes sinueuses du Web, une documentation totalement inédite semble attirer notre attention. Écrite par Ted Neward, aficionado de la plate-forme .NET et Java, il semblerait qu'il ait eu en sa possession l'outil Cassini plus d'un mois avant sa sortie officielle. Suzan Warren, du Team ASP.NET, lui aurait remis les sources à titre d'information et dans le cadre de ses recherches. Toute cette histoire n'aurait eu aucun intérêt pour nous si Ted n'avait pas eu l'ingénieuse idée de retranscrire par écrit l'ensemble des fonctionnalités réunies dans les sources de Cassini sous la forme d'un article fascinant que vous trouverez ici. On y apprend notamment que l'outil est simplement un lanceur ou bootstrappeur effectuant les opérations suivantes :

Pour résumé, Cassini n'est ni plus ni moins qu'un simple lanceur dont le rôle est d'encapsuler la requête d'appel d'une page ASP.NET pour la rediriger vers des classes internes au Framework ! Exceptée la protection du Loopback, il n'y a dans Cassini aucun parseur ASP.NET léger, aucun développement spécifique, aucune restriction particulière sur les performances. Cassini est le même serveur de pages que vous utilisez habituellement lorsque vous développez avec IIS et Visual Studio .NET. Qui l'aurait cru ?

Quel lien entre IIS et ASP.NET ?

Vous devez dans ce cas vous demandez quel est le lien existant entre IIS et ASP.NET ? Et Bien, IIS possède un filtre ISAPI appelé aspnet_isapi.dll. Son seul rôle est d'orienter les requêtes (statiques ou dynamiques) vers IIS ou le moteur de pages ASP.NET afin de générer un flux HTML au client. Il n'y a rien qui soit spécifique à IIS dans cette configuration excepté le fait que le domaine d'application créé par le filtre ISAPI s'exécute dans le même processus qu'IIS. 

Pour résumer voici les traitements effectués lorsqu'une requête arrive à destination d'IIS.

  1. La requête est envoyée au filtre ISAPI : ASPNET_ISAPI.DLL
  2. Le filtre ISAPI passe la requête sous la forme de canaux nommés (named pipes) à un processus appelé ASPNET_WP.EXE (asp working process)
  3. Le working process effectue l'appel à l'objet HttpRuntime en invoquant sa méthode ProcessRequest prenant la page en paramètre.

Vous trouverez ici une centaine de transparents au format PDF vous détaillant ce processus.

Par ailleurs, le rôle joué par le serveur Web en frontal d'un conteneur de pages consiste à traiter les gros volumes de requêtes sous la forme de Pool de Threads ou de processus. Si IIS joue ce rôle avec ASP.NET, cet article nous montre que Apache peut sans aucun doute jouer le même rôle s'il s'exécute dans le même processus que le serveur ASP.NET. A noter que le paramétrage sous la forme d'un Proxy Redirect ne permet pas de bénéficier de telles conditions. Seul un filtre ISAPI pourrait pallier à ce déficit car il possède l'avantage de tourner dans le même processus que le serveur Web. Nous y reviendrons dans les sections suivantes...

Créer son propre serveur sur le principe de Cassini

Après ces quelques explications, il apparaît évident que Cassini joue un rôle minimal dans le processus de déploiement et d'exécution des pages ASP.NET. Dans cette logique, si Microsoft juge inutile de divulguer les sources de Cassini, il suffira simplement de développer en collaboration avec la communauté .NET un lanceur équivalent. Ce serait l'occasion d'éliminer la protection inutile du loopback et de créer un serveur Web complet, gratuit et sans aucune limitation. D'ailleurs, rien dans la licence du Framework .NET n'interdit une telle implémentation. 

Le source suivant a été testé par nos soins, il permet de reproduire le même principe de fonctionnement que Cassini. En tout et pour tout une dizaine de lignes de codes (!) ont été nécessaire pour disposer d'un bootstrapper succinct. Le programme prend en paramètre un fichier ASPX et l'envoie au parseur ASP.NET chargé de générer le flux de réponse en HTML sur la sortie standard. Pas de Cassini, ni de WebMatrix pour faire fonctionner cet exemple. Seul le Framework .NET suffit. On comprend mieux maintenant pourquoi Cassini tient dans 40Ko de code ;-).

Libre à vous d'enrichir ces sources afin d'y ajouter les fonctionnalités réseaux (ThreadPools, ...), explorateur de fichiers, etc ...

using System;
using System.IO;
using System.Web;
using System.Web.Hosting;

// Author : Dark Evil ;-)
// Licence : TODO

public class MyExeHost : MarshalByRefObject
{
        // most important call, send page parsing to ASPNET engine
        public void ProcessRequest(String page)
        {
                HttpRuntime.ProcessRequest(new SimpleWorkerRequest(page, null, Console.Out));
        }
}


public class MySimpleASPNetServer
{
        // To test MySimpleASPNetServer : MyASPNetServer test.aspx
        public static void Main(string[] args)
        {
                MyExeHost host = null ;
                Console.WriteLine(\"Directory : \"   Directory.GetCurrentDirectory());
                host =
                (MyExeHost) ApplicationHost.CreateApplicationHost(typeof(MyExeHost),
                "/",
                Directory.GetCurrentDirectory());
                if (args.Length>0) host.ProcessRequest(args[0]);
                else Console.WriteLine(\"usage : MySimpleASPNetServer <page.aspx>\");

        }
}

MySimpleASPNetServer.cs

Quelques explications sont toutefois nécessaires. La ligne ApplicationHost.CreateApplicationHost(...)  crée un domaine d'application chargé d'héberger le serveur de pages ASP.NET. Habituellement, le filtre ISAPI associé à IIS prend en charge cette opération. 

Ensuite, il suffit simplement d'invoquer la méthode HttpRuntime.ProcessRequest(...) prenant en paramètre la page à compiler. Qui aurait cru cette opération aussi triviale ?

Enfin, il est à noter que le domaine d'application ainsi créé résout ses références  (Assembly externes) à partir du répertoire spécifié dans la méthode CreateApplicationHost(...). Si vos pages font appel à du code behind, il est impératif que les Assembly de ces programmes se trouvent à la portée du domaine d'application qui essaiera de les charger. Soit vous les installez dans le GAC, soit dans un sous-répertoire \bin situé à l'endroit où le serveur a été lancé. A travers cet exemple, nous comprenons mieux pourquoi chaque répertoire virtuel IIS contient un répertoire /bin contenant les références externes manipulées dans les applications.

Le filtre ISAPI pour Apache

La configuration abordée précédemment fait appel à deux processus : Apache et Cassini. Nous pourrions très bien envisager une alternative où Apache réaliserait la redirection vers le moteur ASP.NET sur le principe d'IIS et de aspnet_isapi.dll. Cette librairie étant la propriété de Microsoft et ne disposant pas des sources, nous avons trouvé au détour d'une mailing list sur le Web une version Open Source de ce filtre développé par l'équipe Mono (.NET sur Linux). A noter qu'Apache utilise un module spécifique pour faire fonctionner les ISAPI propres à IIS : mod_isapi.so.

Après cela, vous n'avez plus que l'embarras du choix pour configurer Apache et un serveur ASP.NET ! 

Cassini et la sécurité    

Ces dernières années, la plupart des trous de sécurité d'IIS étaient plus ou moins liés au phénomène de Buffer Overflow. Sans entrer dans les détails techniques d'une telle faille, le débordement de mémoire tampon apparaît surtout en présence de code non managé (C,C++ natif). Lorsqu'une valeur saisie dépasse la taille qui lui a été allouée initialement, le système s'expose à des attaques permettant à une personne mal intentionnée de placer des instructions en assembleur en lieu et place de votre code applicatif. 

Ce type d'attaque est totalement inefficace dans un contexte totalement managé car la machine virtuelle, en l'occurrence la CLR, joue le rôle de chef d'orchestre du byte code MSIL et s'inscrit en qualité d'intermédiaire. Dans le cas précédent, un débordement de tampon se traduirait simplement par une Exception de type BufferOverflow sans aucun impact sur le déroulement du programme. 

Pour résumer, la meilleure parade contre les attaques de ce type consiste à faire appel à une application totalement managée. D'ailleurs, à cet égard, vous remarquerez que les BufferOverflow n'ont guère d'efficacité dans le monde Java.

Or, à en croire la documentation de Cassini et les divers forums sur Internet, l'ensemble du code serait écrit en C#. Que penser d'une telle affirmation après la lecture de cet article, pensez-vous réellement que Cassini soit un serveur de pages ASP.NET totalement managé ? Cassini fait référence au namespace System.Web sur lequel il s'appuie pour implémenter sa mécanique d'exécution des pages. System.Web est-il implémenté à 100% en code managé ? 

Nous répondrons simplement à cette question par NON

Pour le vérifier, faites vous-même le test. Utilisez un décompilateur d'Assembly tel que Salamander et vous verrez que bon nombre de classes utilisées par System.Web font appel à System.Runtime.InteropServices. Pour confirmer encore plus ces propos, System.Web contient une classe dont le nom est sans équivoque : System.Web.UnsafeNativeMethods contenant un ensemble de méthodes natives chargées de récupérer des informations systèmes : heap courant, session distribuée (source d'un bug découvert récemment).

Bref, dire aujourd'hui que Cassini est un serveur Web totalement managé nous paraît quelque peu risqué, sinon totalement inexact. Cassini fait appel au Hosting ASP.NET du Framework, lui-même intégrant du code unsafe

Nous vous avions promis des révélations ;-) ...

Conclusion

Que de nouvelles ! Alors que Cassini était annoncé comme un serveur ultra léger, non protégé et peu à même de monter en charge, nous nous apercevons que c'est en réalité un bootstrapper (lanceur) d'applications ASP.NET. Alors que les sources du produit n'ont pas été publiées, nous avons déjà une idée bien précise de leur contenu grâce à notre ami Ted ;-). Après ces quelques explications, il apparaît plus que jamais possible d'utiliser Apache et Cassini afin de mettre en production un environnement ASP.NET complet. Si cette idée n'est pas accueillie favorablement par Microsoft qui privilégie naturellement son serveur Web maison IIS, il n'en demeure pas moins que les aficionados d'Apache ou les IIS sceptiques trouveront leur bonheur dans le vaste choix de configurations proposées ici. 

Pour finir, l'article pourrait se résumer par : Nous pensions disposer avec Cassini d'une magnifique 2 CV et finalement nous nous apercevons que c'est en fait une Porsche 911 Bi-turbo bridée par Microsoft :-)... 

 

Auteur : Sami Jaber

Copyright : Juin 2002 - DotNetGuru ©

 

Téléchargez

Le projet SimpleASPNetServer : N'hésitez pas à l'enrichir ...

 

Ressources

http://www.dotnet-fr.org/ : Christophe Lauer (article sur WebMatrix/Cassini)

http://www.clrgeeks.com/Papers/HostingASPNET/ : Ted Neward (merci à toi ô Ted ;-))