HTTP moduly prakticky

Doufám, že vás můj předchozí článek navnadil na psaní HTTP modulů a pohled do budoucnosti přesvědčil, že se jedná o užitečnou dovednost. Podívejme se tedy do útrob jednoho HTTP modulu, jak funguje.

Oním kouskem software bude můj modůlek SkinAnywhere. Princip jeho činnosti je jednoduchý: v jakékoliv aplikaci "pod ním" lze jeho prostřednictvím přepínat kaskádové styly. Funguje to tak, že můj modul prohlíží aplikací vygenerované HTML a hledá v něm odkaz na originální stylesheet. Pokud ho najde, modifikuje HTML tak, aby místo něj byl odkaz na nějaký jiný stylesheet, v závislosti na nastavení konfigurační cookie. Tuto cookie lze nastavit zavoláním speciální stránky s vhodnými parametry.

Z hlediska funkčnosti tedy kód obsahuje tři hlavní části:

  1. Načítání konfigurace (tato problematika byla již popsána dříve a nebudu se jí věnovat).
  2. Ve vstupní fázi (před generováním výstupu ASPX stránkami) kontrolovat, zda se nejedná o požadavek na speciální URL pro změnu stylu (nastavení cookies) a pokud ano, zpracovat ho.
  3. Ve výstupní fázi (po vygenerování výstupu ASPX stránkami) prohledávat vygenerovaný kód a nahradit v něm odkaz na CSS vlastním.

IHttpModule

Jádrem celé aplikace je vlastní HTTP modul. Jedná se o třídu která implementuje rozhraní System.Web.IHttpModule. To specifikuje dvě metody: Init a Dispose.

Význam metody Dispose je jasný - slouží k uvolnění všech používaných zdrojů při odstranění modulu. My žádné zdroje nealokujeme a tudíž není co uvolnit.

Klíčová je pro nás metoda Init, která se zavolá při inicializaci (zavedení) modulu. V ní je možno pověsit vlastní metody jako event handlery na obsluhu událostí, které se přihodí v průběhu zpracování požadavku na stránku. Těch je mnoho a každá má svůj význam, proto se na ně podíváme podrobněji.

Události při zpracování požadavku

Následující události nastanou při zpracování webového požadavku přes HTTP runtime, v uvedeném pořadí:

  • BeginRequest - pokud chcete provádět nějaké specifické věci týkající se přesměrování a podobně, učiňte tak nyní.
  • AuthenticateRequest - teď se hledá odpověď na otázku "kdo jsi" - pomocí dostupných (nakonfigurovaných) metod zabezpečení se ověřuje platnost uživatelského jména a hesla (nebo jiného autentizačního prostředku).
  • AuthorizeRequest - teď se zjišťuje "co tady děláš", tedy zda v předchozím kroku úspěšně identifikovaný uživatel má právo vznésti tento požadavek.
  • ResolveRequestCache - v tomto kroku se zjišťuje, zda je stránka odpovídající tomuto požadavku v cache a nebo zda se musí skutečně vykonat.
  • AcquireRequestState - v tomto kroku se načtou hodnoty session state apod.
  • PreRequestHandlerExecute - tento krok nastane těsně předtím, než se požadavek předá k vykonání patřičnému HTTP handleru.
  • V tomto okamžiku se zavolá příslušný HTTP handler a vykoná se vlastní kód stránky.
  • PostRequestHandlerExecute - tento krok nastane těsně potom, co se vykoná HTTP handler.
  • ReleaseRequestState - v tomto okamžiku se uloží změněná data zpět do session apod.
  • UpdateRequestCache - pokud to pravidla cacheování umožní, uloží se v tomto kroku vygenerovaná hodnota do cache.
  • EndRequest - poslední událost těsně před tím, než se vygenerovaná data pošlou na klienta.

Pomocí vlastních HTTP modulů můžeme tedy rozšířit nebo přepsat prakticky všechni činnosti, které při běhu .NET aplikace nastávají. Z výše uvedeného seznamu je jasné, kdy budeme vyřizovat jaké úkoly:

Na událost BeginRequest pověsíme vyhodnocení požadované adresy a přijetí vhodných opatření v případě, že se jedná o adresu pro změnu stylu. Na událost PostRequestHandlerExecute pověsíme zpracování a pozměnění vygenerovaných dat.

Za tímto účelem si vytvoříme metody HandleBeginRequest a HandlePostRequestHandlerExecute (mohou se jmenovat fakticky jakkoliv). V Init je pak přiřadíme k patřičným událostem:

Public Sub Init(ByVal context As System.Web.HttpApplication) Implements System.Web.IHttpModule.Init
    AddHandler context.PostRequestHandlerExecute, AddressOf HandlePostRequestHandlerExecute
    AddHandler context.BeginRequest, AddressOf HandleBeginRequest
End Sub

Přepisování výstupního HTML

K dalšímu zpracování vygenerovaných dat lze použít vlastní třídu, odvozenou (Inherits) od System.IO.Stream. Instanci této třídy jest přiřaditi vlastnosti Response.Filter. To náš HTTP modul provádí v rámci obsluhy události PostRequestHandlerExecute nějak takto:

Public Sub HandlePostRequestHandlerExecute(ByVal sender As Object, ByVal e As EventArgs)
    With System.Web.HttpContext.Current
        ' If output is not HTML, give up
        If .Response.ContentType.ToLower() <> "text/html" Then Return

        ' Append filter
        .Response.Filter = New Filter(.Response.Filter)
    End With
End Sub

Naše vlastní třída Filter slouží jako skutečný filtr: z jedné strany (metodou Write) jsou do něj data cpána a z druhé strany (metodou Read) z ní zase vytékají:

Public Class Filter
    Inherits System.IO.Stream

    Private Base As System.IO.Stream

    Public Overrides Function Read(ByVal buffer() As Byte, ByVal offset As Integer, ByVal count As Integer) As Integer
        Return Me.Base.Read(buffer, offset, count)
    End Function

    Public Sub New(ByVal ResponseStream As System.IO.Stream)
        If ResponseStream Is Nothing Then Throw New ArgumentNullException("ResponseStream")
        Me.Base = ResponseStream
    End Sub

    Public Overrides Sub Write(ByVal buffer() As Byte, ByVal offset As Integer, ByVal count As Integer)
        ' Get HTML code
        Dim HTML As String = System.Text.Encoding.UTF8.GetString(buffer, offset, count)

        ' Do something with output
        HTML = HTML.Replace("DEMO", "TEST")

        ' Send output
        buffer = System.Text.Encoding.UTF8.GetBytes(HTML)
        Me.Base.Write(buffer, 0, buffer.Length)
    End Sub
End Class

Skutečnou logiku nahrazování jsem pro přehlednost vypustil, tento kód jenom nahradí jakýkoliv výskyt řetězce "DEMO" řetězcem "TEST". Stejným způsobem jako s metodou Read je nutno naložit se všemi dalšími metodami a vlastnostmi, jimiž Stream oplývá - pokud nás jejich osud nezajímá, prostě zavoláme tutéž metodu se stejnými parametry, ovšem u "underlying" (vnitřního) streamu, který jsme mimo jiné za tímto účelem v konstruktoru zřídili.

Odchycení speciálního požadavku

V rámci HTTP handleru můžeme provést i odchycení speciálního požadavku, který například necheme předat níže ležící aplikaci. V našem případě se jedná o volání stránky pro přepnutí stylu. Odchycení provedeme v event handleru události BeginRequest. Skutečný kód je opět složitější, níže určený příklad jenom při požadavku na stránku /moduletest.aspx vrátí pevně definovaný text. Stránka moduletest.aspx přitom vůbec nemusí existovat (a i pokud existuje, vůbec na ní nezáleží, nikdy se nevykoná).

 Public Sub HandleBeginRequest(ByVal sender As Object, ByVal e As EventArgs) 
Dim Context As System.Web.HttpContext = System.Web.HttpContext.Current If Not Context.Request.Url.AbsolutePath.ToLower() = "/moduletest.aspx" Then Return

Context.Response.Clear() Context.Response.Write("<html><head><title>HTTP module test</title></head>")
Context.Response.Write("<body><h1>HTTP module test</h1></body></html>") Context.Response.End() End Sub

Registrace HTTP modulu

Aby byl HTTP modul aktivní (vykonán) je nutno ho nejenom napsat, ale též zaregistrovat v souboru web.config. Děje se tak v sekci /configuration/system.web/httpModules. Ukázkový konfigurační soubor může vypadat takto:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.web>
    <httpModules>
      <add name="SkinAnywhere" 
      type="AltairCommunications.SkinAnywhere.HttpModule, SkinAnywhere" />
    </httpModules>
  </system.web>
</configuration>

Přiřazení se děje pomocí elementu add a jeho dvou atributů:

  • name je uživatelsky přítulné jméno vašeho modulu. V kontextu našeho příkladu na něm nezáleží.
  • type je označení typu HTTP module (tedy té třídy která implementuje IHttpModule).

Závěr

  • Prostřednictvím HTTP modulů je možno se napojit na kteroukoliv událost HTTP request processing pipeline.
  • V ukázkách je návod jakým lze modifikovat požadavek předtím než je zpracován, i potom.
  • Aby se handler vykonal, je nutno ho zaregistrovat v souboru web.config (nebo machine.config pro celý server)

Kompletní komentovaný zdrojový kód jednoduchého ale užitečného HTTP modulu si můžete stáhnout na webu Altair Communications.

  • Altairis
  • Nemesis
  • Microsoft MVP
  • IIS
  • ASP.NET