Parsing e Modifica dell’output stream di Asp.Net

Talvolta può rendersi necessario "trattare" l'output di una applicazione asp.net che non si vuole modificare o, come nel mio caso, di un'applicazione sviluppata da terzi e di cui non si hanno i sorgenti; esempi posono essere l'aggiunta di un footer a tutte le pagine, di tag nell'header, esigenze di highlighting, o come nel mi caso dove l'esigenza era la modifica della modalità secondo la quale vennivano creati tutti gli hyperlink presenti nella pagina, al fine di rimuovere l'eventuale presenza di token di sessione che il sistema automaticamente aggiungeva nella GET (questi parametri infatti penalizzano particolarmente la creazione di siti con requisiti di posizionamento nei motori di ricerca.)

Cercando la tematica nel web, avevo trovato un post di Dino Esposito il quale introduceva ad una possibile strada, la quale leggendo nei commenti, presentava pro e contro.

Ho trovato un buono spunto nel seguente articolo il quale spiega in maniera abbastanza dettagliata la modalità.

Essenzialmente la logica è quella di inserirsi nella pipeline .net tramite un modulo o un handler, andando ad assegnare un filtro all'oggetto Response del contesto attuale; infatti l'oggetto response presente nel context presenta uno stream in "write-only", e quindi rende impossibile accedervi e modificarlo.

In maniera più pratica, nel caso di modulo, è necessario intercettare l'evento context_BeginRequest:

public class AuthRemoveModule : IHttpModule
{

    public void Init(HttpApplication context)
    {
        context.BeginRequest += new EventHandler (context_BeginRequest);
    }

    void context_BeginRequest(object sender, EventArgs e)
    {
        HttpContext context = ((HttpApplication)sender).Context;
        context.Response.Filter = new Filter(context.Response.Filter); 
    }

}

Nel caso invece di un handler, nel mio caso ho ereditato da System.Web.UI.PageHandlerFactory ed ho fatto l'override del metodo GetHandler

public class AuthRemoveHandler : System.Web.UI.PageHandlerFactory
{
    public override IHttpHandler GetHandler(HttpContext context, string
             requestType, string virtualPath, string path)
    {
        IHttpHandler handler;
        handler = base.GetHandler(context, requestType, virtualPath, path);
        context.Response.Filter = new Filter(context.Response.Filter);
        return handler;
    }

}

In entrambi i casi è stato impostato un filter, che per mia comodità è in una classe dello stesso namespace, il quale si occupa di fare l'override del metodo write.
Ogni qual volta un controllo della pagina effettuerà una write nell'oggetto response del contesto, verrà attivato il filtro affinchè ne faccia le sue opportune elaborazioni. 

Sostanzialmente, la classe si occupa di salvare in uno stream privato l'output del contesto, in quanto le operazioni di write possono essere più di una. Unica attenzione riguarda la verifica che l'output sia alla sua fine prima di procedere all'elaborazione del testo; in questa implementazione semplificata, banalmente si attende che sia stato scritto il tag </html>

internal class Filter : Stream
 {
     private Stream _inner;
     private StreamBuilter _toParse = new StreamBuilder (1024);
     internal Filter(Stream inner)
     {
         _inner = inner;
     }
     public override void Write(byte[] buffer, int offset, int count)
     {

         // Salvo la scrittura nello stream privato.
         String piece = Encoding.UTF8.GetString(buffer, offset, count);
         _toParse.Append(piece);

         // Verifico che sia stato già scritto il tag </html>
         // Altrimenti esco

         if (!Regex.IsMatch(piece, "</html>", RegexOptions.IgnoreCase))
             return;

         // Eseguo il match per rimuovere il link
         // es, cerco:
         //
auth=QvVB60r%2flnEijA2zEon1SldDD2crcE4QQgFuM7WU%2biA%3d

         String result = Regex.Replace(_toParse.ToString(),
           @"auth=[A-Za-z0-9%]*%[3][d]",
           new MatchEvaluator(RemoveFullSegment), RegexOptions.IgnoreCase);
         Byte[] all = Encoding.UTF8.GetBytes(result);
         _inner.Write(all, 0, all.GetLength(0));

     }

     private String RemoveFullSegment(Match match)
     {
         // Modifico il match come voglio o rimuovo tutto come nel mio caso
         return "";
     }

     public override
void
Flush()
     {         _inner.Flush();     } 
     public override long Seek(long offset, SeekOrigin origin)
     {         return _inner.Seek(offset, origin);     } 
     public override void SetLength(long value)
     {         _inner.SetLength(value);     } 
     public override int Read(byte[] buffer, int offset, int count)
     {         return _inner.Read(buffer, offset, count);     }

}

In questa maniera è possibile controllare le modalità di rendering della pagina.

Manca all'appello solo il pezzettorelativo alla configurazione del web.config
Il modulo e l'handler sono da utilizzare alternativamente.
Ovviamente il primo va a lavorare solo sulle pagine aspx. Il secondo va a lavorare su tutte le richieste che vengono processate dal runtime .net (ad esempio allegati o altri contenuti che vengono gestiti direttamente dal framework, come ad esempio fanno WSS e MOSS)

<system.web>
   <httpHandlers>
      <add verb="*" path="*.aspx"
             type="Alberto.Internet.AuthRemoveHandler"
             validate
="false"/>
   </httpHandlers>
   <httpModules>
      <add name="AuthRemoveModule"
        type="Alberto.Internet.AuthRemoveModule"/>
  
</httpModules>
</system.web>