Injection HTML à base d'HTTPModule

Publié par Fabrice Michellonet sous le(s) label(s) , le 18 janvier 2011

Dernièrement j'ai échangé avec mon Boss, Jérôme, sur les possibilités d'enrichir un site web non dévellopé en .NET mais qui serait hosté dans IIS.
On pourrait se mettre à apprendre le Python... mais voila on est un peu fainéant :)

Une des solutions que je lui ai proposé étais de tirer parti du mécanisme d'extensibilité du pipeline de IIS7 afin d'intervenir directement sur le code HTML renvoyé au navigateur.

Green Injection

Je me propose donc de présenter dans les grandes lignes comment mettre en place ce genre de solution.
Pour illustrer mes propos simplement nous allons nous rajouter un div en fin de page, le div contenant l'heure du système.

On commence par créer notre HTTPModule et on répond à l'évenement BeginRequest :

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

namespace InjectorModule
{
    public class Injector : IHttpModule
    {
        private HttpApplication _application;

        public void Init(HttpApplication context)
        {
            context.BeginRequest += OnBeginRequest;
            _application = context;
        }

        private void OnBeginRequest(object sender, EventArgs e)
        {
            Stream filter = FilterFactory.GetFilter(_application);
            if (filter == null)
                return;

            _application.Response.Filter = filter;
        }

        public void Dispose() {}
    }
}

On délègue à une factory le role de créer ou non un filter en fonction de règles qui ne sont pas connues par le Module.
Le module reste ainsi une brique uniquement technique technique.

Passons à la factory

using System.IO;
using System.Web;

namespace InjectorModule
{
    internal static class FilterFactory
    {
        public static Stream GetFilter(HttpApplication application)
        {
            if (application == null || application.Response.ContentType == null)
                return null;

            if (application.Response.ContentType.ToUpperInvariant().Contains("HTML"))
                return new AppendDateTimeFilter(application.Response.Filter, application.Request.ContentEncoding);

            return null;
        }
    }
}

Ici pas de règles métiers compliquées, on ajoute un Filter si le content type est de type HTML.

Et finalement voici l'implémentation du Filter qui tranforme la réponse envoyée aux navigateurs :

using System;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;

namespace InjectorModule
{
    internal class AppendDateTimeFilter : Stream
    {

        private readonly Stream _inputStream;
        private readonly Encoding _encoding;
        private readonly StringBuilder _responseHtml;

        public AppendDateTimeFilter(Stream input, Encoding contentEncoding)
        {
            _inputStream = input;
            _encoding = contentEncoding;
            _responseHtml = new StringBuilder();
        }

        #region Filter overrides

        public override bool CanRead
        {
            get { return true; }
        }

        public override bool CanSeek
        {
            get { return true; }
        }

        public override bool CanWrite
        {
            get { return true; }
        }

        public override void Close()
        {
            _inputStream.Close();
        }

        public override void Flush()
        {
            _inputStream.Flush();
        }

        public override long Length
        {
            get { return 0; }
        }

        public override long Position { get; set; }

        public override long Seek(long offset, SeekOrigin origin)
        {
            return _inputStream.Seek(offset, origin);
        }

        public override void SetLength(long length)
        {
            _inputStream.SetLength(length);
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            return _inputStream.Read(buffer, offset, count);
        }
        #endregion

        public override void Write(byte[] buffer, int offset, int count)
        {
            try
            {
                string bufferContent = _encoding.GetString(buffer);

                // Wait for the closing tag
                Regex eof = new Regex("", RegexOptions.IgnoreCase);

                _responseHtml.Append(bufferContent);
                if (!eof.IsMatch(bufferContent))
                    return;

                // Transform the response and write it back out
                string finalHtml = _responseHtml
                                        .Replace("", string.Format(@"
{0}
", DateTime.Now)) .ToString(); // Send. byte[] data = _encoding.GetBytes(finalHtml); _inputStream.Write(data, 0, data.Length); } catch (Exception) { } } } }

Pour tester rien de plus simple, on compile l'assembly, on la place dans le dossier bin du site web que l'on veux trafiquer.
Dans notre cas, nous avons aussi du créer le dossier bin car non existant dans l'appli Web Python.
Le fait de placer l'assembly dans un dossier bin, est une contrainte technique imposée par IIS; je n'ai pas trouvé d'alternatives et a priorit il n'y a pas de configuration qui pourrait influer sur cette contrainte.
Finalement dans le gestionnaire IIS, rendez vous dans la section Modules de votre application Web.
En ouvrant la fenetre d'ajout de modules, votre assembly sera désormais présente; Sélectionnez la, et pointez votre navigateur sur ce dernier.

Tadam!!! l'heure s'affiche tout en bas :)

J'esperes que cette solutions vous ouvre de nouvelles possibilités.

Un dernier mot pour vous conseiller l'excellente librairie htmlagilitypack qui vous donnera les moyens de parser facilement un document HTML, même malformé.