For English readers, Sander translated in English part of this my post here  (Thank you Sander !)

Bonjour tout le monde,

Si vous développez en ASP.NET 2 et que vous faites de l'url rewriting, alors prenez 2 minutes pour lire ce post, il peut sauver votre site Internet d'une disparition des moteurs de recherche...

En Février dernier (il y a 4 mois) j'ai rapporté un bug que j'avais trouvé. Si je décide d'en parler aujourd'hui sur mon blog c'est parce que je considère que ce bug a atteint un niveau de conséquence catastrophique, comme l'exemple que j'ai signalé pour CommunityServer 2.0. N'ayant toujours pas de patch disponible je pense qu'il est bon pour les développeurs web ASP.NET 2 qui feraient du rewriting de savoir que leur(s) site(s) pourrai(en)t ne pas être indéxé dans Google & co à cause de ce bug. (En tout cas les pages de contenu, une home page ne devrait pas être touchée ou alors le site est bizarrement codé Stick out tongue <img src=" />)

Pour remettre dans le contexte et bien comprendre le problème, j'avais mis en place sur CodeS-SourceS du rewriting pour avoir des url plus "humainement" lisibles et pourquoi pas améliorer le référencement de ces mêmes pages au passage.
Donc aux alentours de Février j'ai décidé de rewriter la section des codes (si mes souvenirs sont bons). Tout se passait bien, cela marchait à merveille.
Au bout de quelques jours, j'ai regardé le gestionnaire d'évènement et j'avais un tas d'erreurs étranges du genre :

Exception information: 
   Exception type: HttpException
   Exception message: Cannot use a leading .. to exit above the top directory.
...
   Stack trace:    at System.Web.Util.UrlPath.ReduceVirtualPath(String path)
...

Je ne m'inquiétais pas trop jusqu'au moment où je me suis aperçu que j'avais +2000 par heure !

C'est là que tout à commencé : "Mais bon sang ! C'est quoi cette erreur ?"

Le pire, c'est qu'en local je n'avais pas ce genre d'erreur...
Je vous passerai les détails de tout le temps passé à essayer de comprendre ce mystérieux crash.
Après plusieurs heures jours de tests, bidouilles j'en passe et des meilleurs pour essayer de comprendre le problème, j'ai enfin cerné l'ensemble du problème et surtout j'ai compris que j'avais mis le doigt sur un bug assez critique (et pas si facile à détecter si on est pas assez consciencieux en regardant les logs régulièrement par exemple, vous pouvez être victime de ce bug sans vous en rendre compte)

Voici donc ce qui se passe :

Attention, ce bug n'affecte pas cassini (le server web qui est lancé avec Visual Studio 2005) donc n'essayez pas de reproduire en local, cela ne marchera pas, il faut que le site soit sur un IIS 6 sous Windows 2003 (En gros en production).


Lorsque l'on fait du rewriting en .NET 2 on utilise la méthode Context.RewritePath.
(J'ai mis à disposition un zip ICI avec un projet de test pour reproduire le problème)

Créez une page page.aspx
Mettez ce que vous voulez dedans cela ne changera rien au résultat. Par exemple
<%=Request("ID")%>

Voici un exemple de code à mettre dans un HTTPModule :

Public Class Rewriter

    Implements System.Web.IHttpModule

 

    Public Sub Dispose() Implements System.Web.IHttpModule.Dispose

 

    End Sub

 

    Public Sub Init(ByVal context As System.Web.HttpApplication) Implements System.Web.IHttpModule.Init

        AddHandler context.BeginRequest, AddressOf Me.HandleBeginRequest

    End Sub

 

    Private Sub HandleBeginRequest(ByVal [source] As [Object], ByVal e As EventArgs)

        Dim app As System.Web.HttpApplication = CType([source], System.Web.HttpApplication)

        app.Context.RewritePath(“~/page.aspx?ID=1”, False) ' L'effet est le même pour “/page.aspx?ID=1”

    End Sub

End Class

(Cette exemple réécrira toutes les urls vers page.aspx?ID=1 dans tous les cas, c'est juste pour vous montrer et vous permettre de reproduire/comprendre le problème)

Rajoutez le HTTPModule dans le web.config.

Maintenant installez Fiddler (
www.fiddlertool.com). Cet outil permet d'analyser les la pages récupérées par votre navigateur. Mais il permet également de construire un "Request" personnalisé en paramétrant le User-Agent par exemple...

Faite un publish de votre site et mettez-le sur un server IIS6 (sur un Win 2003), (Encore un fois, ce bug ne fonctionne pas sur un IIS 5.1 de Windows XP)

Une fois tout installé, ouvrez IE (ou firefox, ou un autre navigateur) et allez sur la page
http://VotreMachine/LeDossierDuSite/Default.aspx 

La page default.aspx devrait être rewrité sur Page.aspx?ID=1
Maintenant dans fiddler mettez ceci dans le request builder :

Accept: */*
Accept-Encoding: gzip, x-gzip
User-Agent: Mozilla/4.0

Et mettez l'url
http://VotreMachine/LeDossierDuSite/Default.aspx en haut puis cliquez sur le bouton "Execute!".
La tout va bien, vous devriez avoir un code erreur 200

Maintenant mettez ceci dans votre navigateur :
http://VotreMachine/LeDossierDuSite/UnDossierQuiNexistePas/Default.aspx (la page n'existe pas mais sera rewrité vers ../page.aspx?ID=1 grace au HTTPModule)

Maintenant faites de même dans Fiddler. Là encore tout va bien code erreur 200

Attention, c'est là que tout va commencer, dans Fiddler mettez ceci :

User-Agent: Mozilla/5.0
Au lieu de User-Agent: Mozilla/4.0

Appuyez sur le bouton "Execute!" (la même pas que précédemment) et BOOOOOM erreur 500 !

Voilà vous êtes désormais victime du bug !
Voici un liste de User-Agent qui génèrera des erreurs 500 (Liste non exhaustive bien sur !)

Mozilla/1.0
Mozilla/2.0
Mozilla/5.0
Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)
Mozilla/5.0 (compatible; Yahoo! Slurp;
http://help.yahoo.com/help/us/ysearch/slurp)
Yahoo-Blogs/v3.9 (compatible; Mozilla 4.0; MSIE 5.5;
http://help.yahoo.com/help/us/ysearch/crawling/crawling-02.html )
Mozilla/2.0 (compatible; Ask Jeeves/Teoma; +http://sp.ask.com/docs/about/tech_crawling.html)
Mozilla/5.0 (compatible; BecomeBot/3.0; MSIE 6.0 compatible; +http://www.become.com/site_owners.html)
Mozilla/5.0 (compatible; Konqueror/.... (Tous les users agent de Konqueror que j'ai testés plantent)
Etc...

Voici un cas "amusant"
Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.8.0.1) Gecko/20060111 Firefox/1.5.0.1 <= Ne crash pas
Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.8.0.1) <= Crash !

Voilà le tableau est dressé, je vais maintenant tenter de vous expliquer ce qui se passe (et surtout ce que j'en ai compris).

Si dans le HTTPModule, vous mettez "True" en 2eme paramètre (rebaseClientPath) de RewritePath, votre page rewritée ne plantera pas.
Alors vous me direz mais pourquoi tu mets "False" dans ce cas ? (Moi je poserai plutot la question pourquoi ça plante si je met "False" mais bon, passons Stick out tongue <img src=" />)

Et bien ce fameux rebaseClientPath permet d'avoir un "action" du tag form de la page "correct"
Admettons que notre page est http://monsite/mondossier/mapage.aspx et que je rewrite cette page en réalité sur http://monsite/page.aspx?id=mapage

Si je met True mon action de mon tag form ressemblera à ca :
<form name="form1" method="post" action="page.aspx?ID=mapage" id="form1">
Si je met False, mon action de mon tag <form> ressemblera à ça :
<form name="form1" method="post" action="../page.aspx?ID=mapage" id="form1">

En cas de postback le 2eme action sera correct mais pas le 1er car la page page.aspx n'existe pas dans le dossier /mondossier !

Alors vous me direz "oui mais là tu exagères, tu pourrais mettre des UrlPostback = "../Page.aspx?id=mapage" dans tes controls et garder True comme ça, ça ne plantera plus". Bah oui voyons c'est tellement simple comme solution. Je l'ai essayé cette solution et bien elle est TRES mauvaise, vous vous en rendrez compte quand un tas de visiteurs de vos sites vous dirons qu'ils n'arrivent pas poster de messages (parce que le javascript qui gère le URLPostback est mal exécuté dans leur navigateurs, ou pire que le javascript généré provoque une erreur et ne s'exécute carrement pas !)

Bref, j'ai passé un temps fou sur ce bug (près d'une semaine entière pour trouver, comprendre, trouver la cause, comprendre la cause et trouver un bidouille)

Déjà, pour bien comprendre la compléxité à débusquer ce bug, il faut bien comprendre que ce bug n'affecte qu'un millieu de production. En effet ce bug ne se produit QUE sur un IIS6 sur Windows 2003. Il ne se produit pas sur Cassini (le server web intégré dans Visual Studio 2005) et non plus sur IIS 5.1 de Windows XP Pro.
En sachant cela, vous comprendrez mieux pourquoi j'ai passé tant de temps à comprendre ce bug (voir même à me rendre compte qu'il existait), car j'ai du faire un tas de tests pour arriver à la conclusion que ce problème n'arrivait qu'en production et que la cause était le User-Agent...

Je ne vais pas vous ennuyer avec tous les "petits" tests que j'ai pu faire pour trouver le problème et une "solution" (Le post que vous lisez a déjà été assez long à écrire Big Smile <img src=" />). Une solution qui est une bien belle bidouille bien "pas propre" comme il faut que je m'en vais vous conter Big Smile <img src=" />

Donc sachant maintenant que le bug vient du navigateur utilisé, il faut essayer de savoir "pourquoi ?". Car à aucun moment mon code ne touche au user agent et à aucun moment ce user agent n'est exploité dans ma page. Donc aucun risque de ce côté, ce n'est pas le code de ma page qui pose problème.

En faisant un tas de tests je me suis rendu compte que selon le user-agent utilisé, les "capabilities" ne sont pas forcément les mêmes et donc visiblement certaines signatures doivent avoir une "capabilitie" qui génère une belle exception (non catchée dans le Framework .NET 2 ! bouuuuuuh !)

Il en resulte que quand vous utilisez le User-Agent "Mozilla/5.0" par exemple, le TagWriter utilisé est

System.Web.UI.Html32TextWriter

Alors que pour un User-Agent de IE ou FIrefox sera utilisé :

System.Web.UI.HtmlTextWriter

Visiblement le problème doit venir Html32TextWriter mais je ne peux pas le dire avec certitude. Honnêtement j'ai passé suffisement de temps sur ce bug et à trouver une solution pour en plus passer des heures à jouer avec reflector. Si vous avez du temps pour jouer avec et que vous trouvez la ligne qui pose problème envoyez votre feedback à Microsoft on ne sait jamais cela les aidera peut-être à nous sortir un patch plus vite...

Bref voici la solution pour fixer le problème avec (par exemple) le bot de Yahoo! Slurp (Il faudra faire le même genre de choses pour TOUTES les signatures posant problème)
Depuis la version 2005 de Visual Studio vous pouvez créer vos propres "capabilities" en créant des fichiers .browser
Dans votre projet web, créez un dossier de type "App_Browser" et créez un nouveau fichier (ex : yahooslurp.browser).

Dedans mettez ceci :

<!--

Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)

-->

<

browsers>
<
browser id="Slurp" parentID="Mozilla">
<
identification>
<
userAgent match="Slurp" />
</
identification>
<
capabilities>
<
capability name="browser" value="Yahoo!Slurp" />
<
capability name="Version" value="4.0" />
<
capability name="MajorVersion" value="4" />
<
capability name="MinorVersionString" value="" />
<
capability name="MinorVersion" value=".0" />
<
capability name="activexcontrols" value="true" />
<
capability name="backgroundsounds" value="true" />
<
capability name="cookies" value="true" />
<
capability name="css1" value="true" />
<
capability name="css2" value="true" />
<
capability name="ecmascriptversion" value="1.2" />
<
capability name="frames" value="true" />
<
capability name="javaapplets" value="true" />
<
capability name="javascript" value="true" />
<
capability name="jscriptversion" value="5.0" />
<
capability name="supportsCallback" value="true" />
<
capability name="supportsFileUpload" value="true" />
<
capability name="supportsMultilineTextBoxDisplay" value="true" />
<
capability name="supportsMaintainScrollPositionOnPostback" value="true" />
<
capability name="supportsVCard" value="true" />
<
capability name="supportsXmlHttp" value="true" />
<
capability name="tables" value="true" />
<
capability name="vbscript" value="true" />
<
capability name="w3cdomversion" value="1.0" />
<
capability name="xml" value="true" />
<
capability name="tagwriter" value="System.Web.UI.HtmlTextWriter" />
</
capabilities>
</
browser>
</
browsers>

Relancez votre application et refaites des tests avec Fiddler en utilisant ce User-Agent :
Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)

Et là Ô magie ! Plus d'erreur 500 !

(Il vous suffira de renouveller l'opération pour les différents User-Agent qui plantent.)

Il y a surement d'autres possibilités pour fixer le problème mais celle-ci est la plus simple et ne demande pas de re-compiler le code pour des solutions comme ComminyServer 2.0. Il vous suffit juste de rajouter le dossier "App_Browser" avec vos fichiers .browser et cela marchera (la preuve, faite le test sur ce post, vous verrez que cela marche et je n'ai pas eu à re-compiler quoi que ce soit.

Une précision importante qui peut vous aider à "contourner" ce bug. Ce plantage ne se produit que dans le cas ou vous rewrité depuis un sous dossier vers un fichier qui se trouve dans un dossier parent. Donc la solution consiste à rewriter depuis la racine et vous n'aurez pas de plantage.
Par exemple au lieu de
-http://....../codes/mapagerewrité.aspx
Préférez ceci :
-http://....../codes_mapagerewrité.aspx
Dans ce cas là, pas de plantage.

Autre "détail" ce bug est lié à un pb de (soit-disant) "sécurité" donc n'essayez pas en local sur votre Win2003. Si vous êtes admin vous n'aurez pas l'erreur, vous devez y accéder en anonymous depuis une autre machine pour reproduire le pb. (Oui c'est un vrai casse-tête pour trouver et comprendre ce bug et surtout pour comprendre comment reproduire)

Encore un autre "détail" que vient de tester cyril et dont j'avais oublié l'importance, le site ne doit pas être dans un "Virtual Directory", cela doit être un "Site Web" dans IIS6 (Ce qui complique encore pour reproduire et trouver le problème !)


Poppyto nous proposait de changer le User-Agent dans le header MAIS :
1) Je n'ai pas testé
2) Si on le change ce n'est pas dit que la capabilities n'a pas déjà été prise en compte avant qu'il ne soit changé, ce qui n'empêcherait donc pas le plantage
3) Cela nécessite une (re)compilation de votre projet si cela marche

ATTENTION : Je considère cette solution comme une BIDOUILLE donc ne croyez pas que c'est un correctif, le seul correctif qu'il peut y avoir à ce problème serait que Microsoft sorte un patch officiel et si possible un patch public.
De plus cette solution ne corrigera pas à 100% le problème car vous devez identifier les signatures des user-agent qui font planter les pages (Et ça, ce n'est pas gagné, il faut surveiller vos logs et passer un temps fou à tester et créer les .browser adaptés)

Merci à tous ceux qui m'avaient aidé à débusquer/comprendre ce bug à l'époque et particulièrement à Poppyto qui avait même passé du temps pour essayer de comprendre et qui avait reproduit le problème sur son server de prod pour vérifier mes théories.

J'ai ouvert une discussion avant hier à propos de ce bug que le forum de CommunityServer qui est touché par ce bug à cette adresse : http://communityserver.org/forums/thread/536350.aspx

P.S : Vous l'aurez compris ce bug m'a tellement pourrie la vie que je le connais comme si je l'avais codé moi-même. Je pourrais écrire 2 ou 3 pages en plus sur mes constats mais je pense que le principal a été dis et ce post m'a pris quelques heures à écrire donc désolé de ne pas en dire plus que ça.

Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :