Rejestrowanie surowego żądania / odpowiedzi HTTP w ASP.NET MVC i IIS7

141

Piszę usługę sieciową (używając ASP.NET MVC) i dla celów wsparcia chcielibyśmy móc rejestrować żądania i odpowiedzi w jak najbardziej zbliżonym do surowego formacie on-the-wire (tj. W tym HTTP metoda, ścieżka, wszystkie nagłówki i treść) do bazy danych.

Nie jestem pewien, jak zdobyć te dane w najmniej „zniekształcony” sposób. Mogę ponownie określić, jak moim zdaniem wygląda żądanie, sprawdzając wszystkie właściwości HttpRequestobiektu i budując z nich ciąg (i podobnie w przypadku odpowiedzi), ale naprawdę chciałbym uzyskać rzeczywiste dane żądania / odpowiedzi, które wysłane za pośrednictwem drutu.

Z przyjemnością używam dowolnego mechanizmu przechwytywania, takiego jak filtry, moduły itp., A rozwiązanie może być specyficzne dla IIS7. Jednak wolałbym zachować go tylko w kodzie zarządzanym.

Jakieś zalecenia?

Edycja: Zauważyłem, że HttpRequestma SaveAsmetodę, która może zapisać żądanie na dysku, ale to rekonstruuje żądanie ze stanu wewnętrznego przy użyciu obciążenia wewnętrznych metod pomocniczych, do których nie można uzyskać dostępu publicznie (dlaczego nie pozwala to na zapisanie do podanego przez użytkownika strumień nie wiem). Zaczyna więc wyglądać, że będę musiał zrobić co w mojej mocy, aby zrekonstruować tekst żądania / odpowiedzi z obiektów ... jęk.

Edycja 2: Proszę zauważyć, że powiedziałem, że całe żądanie, w tym metoda, ścieżka, nagłówki itp. Bieżące odpowiedzi dotyczą tylko strumieni treści, które nie zawierają tych informacji.

Edycja 3: Czy nikt tutaj nie czyta pytań? Jak dotąd pięć odpowiedzi, a jednak żadna z nich nie wskazuje nawet sposobu na uzyskanie całego surowego żądania w sieci. Tak, wiem, że mogę przechwytywać strumienie wyjściowe, nagłówki, adres URL i wszystko inne z obiektu żądania. Powiedziałem już, że w pytaniu zobacz:

Mogę odtworzyć to, jak moim zdaniem wygląda żądanie, sprawdzając wszystkie właściwości obiektu HttpRequest i budując z nich ciąg (i podobnie w przypadku odpowiedzi), ale naprawdę chciałbym uzyskać rzeczywiste dane żądania / odpowiedzi to jest wysłane przez kabel.

Jeśli znasz kompletne nieprzetworzone dane (w tym nagłówki, adres URL, metodę http itp.), Po prostu nie możesz ich pobrać, wtedy warto to wiedzieć. Podobnie, jeśli wiesz, jak uzyskać to wszystko w formacie surowym (tak, nadal mam na myśli włączenie nagłówków, adresu URL, metody http itp.) Bez konieczności rekonstrukcji, o co prosiłem, wtedy byłoby to bardzo przydatne. Ale mówienie mi, że mogę to zrekonstruować z HttpRequest/ HttpResponseobiektów, nie jest przydatne. Wiem to. Już to powiedziałem.


Uwaga: zanim ktokolwiek zacznie mówić, że to zły pomysł lub ograniczy skalowalność itp., Zaimplementujemy również mechanizmy ograniczania przepustowości, dostarczania sekwencyjnego i zapobiegania powtórzeniom w środowisku rozproszonym, więc i tak rejestrowanie bazy danych jest wymagane. Nie szukam dyskusji, czy to dobry pomysł, szukam, jak można to zrobić.

Greg Beech
źródło
1
@Kev - Nie, to usługa RESTful zaimplementowana przy użyciu ASP.NET MVC
Greg Beech
Prawdopodobnie jest to możliwe przy użyciu IIS7 i natywnego modułu - msdn.microsoft.com/en-us/library/ms694280.aspx
Daniel Crenna
Czy udało Ci się to wdrożyć? Ciekawe, czy przyjęliście jakąś strategię buforowania, aby pisać do bazy danych?
systempuntoout
1
Ciekawy projekt ... jeśli w końcu to zrobisz, z ostatecznym rozwiązaniem?
PreguntonCojoneroCabrón

Odpowiedzi:

91

Zdecydowanie użyj IHttpModulei zaimplementuj zdarzenia BeginRequesti EndRequest.

Wszystkie „surowe” dane są obecne między HttpRequesta HttpResponse, po prostu nie są w jednym nieprzetworzonym formacie. Oto części potrzebne do zbudowania zrzutów w stylu Fiddlera (prawie tak blisko surowego HTTP, jak to tylko możliwe):

request.HttpMethod + " " + request.RawUrl + " " + request.ServerVariables["SERVER_PROTOCOL"]
request.Headers // loop through these "key: value"
request.InputStream // make sure to reset the Position after reading or later reads may fail

Aby uzyskać odpowiedź:

"HTTP/1.1 " + response.Status
response.Headers // loop through these "key: value"

Pamiętaj, że nie możesz odczytać strumienia odpowiedzi, więc musisz dodać filtr do strumienia wyjściowego i przechwycić kopię.

W swoim BeginRequestbędziesz musiał dodać filtr odpowiedzi:

HttpResponse response = HttpContext.Current.Response;
OutputFilterStream filter = new OutputFilterStream(response.Filter);
response.Filter = filter;

Przechowaj, filtergdzie możesz się do niego dostać w programie EndRequestobsługi. Proponuję w HttpContext.Items. Tam można uzyskać pełne dane odpowiedzi w formacie filter.ReadStream().

Następnie zaimplementuj OutputFilterStreamza pomocą wzorca dekoratora jako opaski wokół strumienia:

/// <summary>
/// A stream which keeps an in-memory copy as it passes the bytes through
/// </summary>
public class OutputFilterStream : Stream
{
    private readonly Stream InnerStream;
    private readonly MemoryStream CopyStream;

    public OutputFilterStream(Stream inner)
    {
        this.InnerStream = inner;
        this.CopyStream = new MemoryStream();
    }

    public string ReadStream()
    {
        lock (this.InnerStream)
        {
            if (this.CopyStream.Length <= 0L ||
                !this.CopyStream.CanRead ||
                !this.CopyStream.CanSeek)
            {
                return String.Empty;
            }

            long pos = this.CopyStream.Position;
            this.CopyStream.Position = 0L;
            try
            {
                return new StreamReader(this.CopyStream).ReadToEnd();
            }
            finally
            {
                try
                {
                    this.CopyStream.Position = pos;
                }
                catch { }
            }
        }
    }


    public override bool CanRead
    {
        get { return this.InnerStream.CanRead; }
    }

    public override bool CanSeek
    {
        get { return this.InnerStream.CanSeek; }
    }

    public override bool CanWrite
    {
        get { return this.InnerStream.CanWrite; }
    }

    public override void Flush()
    {
        this.InnerStream.Flush();
    }

    public override long Length
    {
        get { return this.InnerStream.Length; }
    }

    public override long Position
    {
        get { return this.InnerStream.Position; }
        set { this.CopyStream.Position = this.InnerStream.Position = value; }
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        return this.InnerStream.Read(buffer, offset, count);
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
        this.CopyStream.Seek(offset, origin);
        return this.InnerStream.Seek(offset, origin);
    }

    public override void SetLength(long value)
    {
        this.CopyStream.SetLength(value);
        this.InnerStream.SetLength(value);
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        this.CopyStream.Write(buffer, offset, count);
        this.InnerStream.Write(buffer, offset, count);
    }
}
mckamey
źródło
1
Niezła odpowiedź. Jeden komentarz: powiedziałeś: „Następnie można uzyskać pełne dane odpowiedzi w funkcji filter.ToString ()”. -nie masz na myśli filter.ReadStream ()? (Implementuję w vb.net nie c #, ale jeśli uruchomię ToString, po prostu otrzymam nazwę klasy jako ciąg. .ReadStream zwraca żądaną treść odpowiedzi.
Adam
Zgadzam się, dobra odpowiedź. Użyłem go jako podstawy dla niestandardowego rejestratora, ale teraz napotkałem problem, w którym brakuje niektórych nagłówków, a co najważniejsze, podczas korzystania z kompresji IIS nie mogę uzyskać dostępu do końcowej skompresowanej odpowiedzi. Zacząłem nowe powiązane pytanie ( stackoverflow.com/questions/11084459/ ... ) w tym zakresie.
Chris,
2
Myślę, że McKamey jest geniuszem. Czy możesz pracować dla firmy Microsoft, abyśmy po prostu otrzymywali inteligentne rozwiązania, zamiast potrzebować genialnych obejść?
Abacus
4
Uważaj na to żądanie. RawUrl może wywołać wyjątek weryfikacji żądania. W 4.5 możesz użyć request.Unvalidated.RawUrl, aby temu zapobiec. W 4.0 wykorzystałem trochę refleksji, aby naśladować Request.SaveAs
Freek
1
@mckamey Próbuję zaimplementować Twoje rozwiązanie w moim global.asax z Application_BeginRequest i Application_EndRequest, ale nie jestem pewien, jaki kod powinienem napisać w EndRequest, czy możesz podać przykład w swojej odpowiedzi. plz?
Jerome2606
48

Poniższa metoda rozszerzenia w HttpRequest utworzy ciąg, który można wkleić do programu Fiddler i odtworzyć.

namespace System.Web
{
    using System.IO;

    /// <summary>
    /// Extension methods for HTTP Request.
    /// <remarks>
    /// See the HTTP 1.1 specification http://www.w3.org/Protocols/rfc2616/rfc2616.html
    /// for details of implementation decisions.
    /// </remarks>
    /// </summary>
    public static class HttpRequestExtensions
    {
        /// <summary>
        /// Dump the raw http request to a string. 
        /// </summary>
        /// <param name="request">The <see cref="HttpRequest"/> that should be dumped.       </param>
        /// <returns>The raw HTTP request.</returns>
        public static string ToRaw(this HttpRequest request)
        {
            StringWriter writer = new StringWriter();

            WriteStartLine(request, writer);
            WriteHeaders(request, writer);
            WriteBody(request, writer);

            return writer.ToString();
        }

        private static void WriteStartLine(HttpRequest request, StringWriter writer)
        {
            const string SPACE = " ";

            writer.Write(request.HttpMethod);
            writer.Write(SPACE + request.Url);
            writer.WriteLine(SPACE + request.ServerVariables["SERVER_PROTOCOL"]);
        }

        private static void WriteHeaders(HttpRequest request, StringWriter writer)
        {
            foreach (string key in request.Headers.AllKeys)
            {
                writer.WriteLine(string.Format("{0}: {1}", key, request.Headers[key]));
            }

            writer.WriteLine();
        }

        private static void WriteBody(HttpRequest request, StringWriter writer)
        {
            StreamReader reader = new StreamReader(request.InputStream);

            try
            {
                string body = reader.ReadToEnd();
                writer.WriteLine(body);
            }
            finally
            {
                reader.BaseStream.Position = 0;
            }
        }
    }
}
Sam Shiles
źródło
5
Bardzo dobry kod! Ale żeby to działało z MVC 4, musiałem zmienić nazwę klasy na HttpRequestBaseExtensionsi HttpRequestna HttpRequestBasew każdym miejscu.
Dmitry,
35

Możesz użyć zmiennej serwera ALL_RAW, aby pobrać oryginalne nagłówki HTTP wysłane z żądaniem, a następnie możesz normalnie pobrać InputStream:

string originalHeader = HttpHandler.Request.ServerVariables["ALL_RAW"];

sprawdź: http://msdn.microsoft.com/en-us/library/ms524602%28VS.90%29.aspx

Vincent de Lagabbe
źródło
Na mnie też to zadziałało. Nie musiał nawet być w treserze. Nadal mogłem uzyskać do niego dostęp ze strony.
Helephant
3
Lub w kontekście serwera ASP.NET użyj: this.Request.ServerVariables ["ALL_RAW"];
Peter Stegnar
Nie mogę pobrać treści żądania z Request.InputStream, za każdym razem zwraca „”, jednak ALL_RAW świetnie sprawdza się przy zwracaniu nagłówków żądań, więc ta odpowiedź jest w połowie poprawna.
Justin
1
Możesz także użyć, HttpContext.Current.Requestaby pobrać bieżący kontekst poza kontrolerami MVC, stronami ASPX itp ... tylko upewnij się, że nie jest on null pierwszy;)
jocull
16

Cóż, pracuję nad projektem i zrobiłem, może niezbyt głęboki, dziennik używając parametrów żądania:

Spójrz:

public class LogAttribute : ActionFilterAttribute
{
    private void Log(string stageName, RouteData routeData, HttpContextBase httpContext)
    {
        //Use the request and route data objects to grab your data
        string userIP = httpContext.Request.UserHostAddress;
        string userName = httpContext.User.Identity.Name;
        string reqType = httpContext.Request.RequestType;
        string reqData = GetRequestData(httpContext);
        string controller = routeData["controller"];
        string action = routeData["action"];

        //TODO:Save data somewhere
    }

    //Aux method to grab request data
    private string GetRequestData(HttpContextBase context)
    {
        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < context.Request.QueryString.Count; i++)
        {
            sb.AppendFormat("Key={0}, Value={1}<br/>", context.Request.QueryString.Keys[i], context.Request.QueryString[i]);
        }

        for (int i = 0; i < context.Request.Form.Count; i++)
        {
            sb.AppendFormat("Key={0}, Value={1}<br/>", context.Request.Form.Keys[i], context.Request.Form[i]);
        }

        return sb.ToString();
    }

Możesz udekorować swoją klasę kontrolerów, aby całkowicie ją rejestrować:

[Log]
public class TermoController : Controller {...}

lub zarejestruj tylko niektóre indywidualne metody akcji

[Log]
public ActionResult LoggedAction(){...}
John Prado
źródło
12

Czy jest jakiś powód, dla którego musisz zachować go w kodzie zarządzanym?

Warto wspomnieć, że możesz włączyć rejestrowanie nieudanego śledzenia w IIS7, jeśli nie lubisz ponownie wymyślać koła. To rejestruje nagłówki, treść żądania i odpowiedzi, a także wiele innych rzeczy.

Nieudane rejestrowanie śledzenia

JoelBellot
źródło
A jeśli to nie jest awaria?
Sinaesthetic
7
Możesz również użyć rejestrowania nieudanego śledzenia z HTTP 200 OK, więc nadal można rejestrować
niepowodzenia
2
To zdecydowanie najprostsze rozwiązanie.
Kehlan Krumme,
Aby zobaczyć cały ślad stosu, konieczne było dodanie GlobalConfiguration.Configuration.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;na końcu Register(..)in WebApiConfig.cs, ale może się to różnić w zależności od wersji.
Evgeni Sergeev
8

Poszedłem z podejściem McKAMEYA. Oto moduł, który napisałem, który pomoże Ci zacząć i miejmy nadzieję, pozwoli Ci zaoszczędzić trochę czasu. Będziesz oczywiście musiał podłączyć Logger do czegoś, co działa dla Ciebie:

public class CaptureTrafficModule : IHttpModule
{
    public void Init(HttpApplication context)
    {
        context.BeginRequest += new EventHandler(context_BeginRequest);
        context.EndRequest += new EventHandler(context_EndRequest);
    }

    void context_BeginRequest(object sender, EventArgs e)
    {
        HttpApplication app = sender as HttpApplication;

        OutputFilterStream filter = new OutputFilterStream(app.Response.Filter);
        app.Response.Filter = filter;

        StringBuilder request = new StringBuilder();
        request.Append(app.Request.HttpMethod + " " + app.Request.Url);
        request.Append("\n");
        foreach (string key in app.Request.Headers.Keys)
        {
            request.Append(key);
            request.Append(": ");
            request.Append(app.Request.Headers[key]);
            request.Append("\n");
        }
        request.Append("\n");

        byte[] bytes = app.Request.BinaryRead(app.Request.ContentLength);
        if (bytes.Count() > 0)
        {
            request.Append(Encoding.ASCII.GetString(bytes));
        }
        app.Request.InputStream.Position = 0;

        Logger.Debug(request.ToString());
    }

    void context_EndRequest(object sender, EventArgs e)
    {
        HttpApplication app = sender as HttpApplication;
        Logger.Debug(((OutputFilterStream)app.Response.Filter).ReadStream());
    }

    private ILogger _logger;
    public ILogger Logger
    {
        get
        {
            if (_logger == null)
                _logger = new Log4NetLogger();
            return _logger;
        }
    }

    public void Dispose()
    {
        //Does nothing
    }
}
GrokSrc
źródło
3
Nie możesz bezpiecznie przesyłać app.Response.Filter do niczego innego niż Stream. Inne HttpModules mogą opakować twój filtr odpowiedzi swoim własnym iw tym przypadku otrzymasz nieprawidłowy wyjątek rzutowania.
Micah Zoltu
Nie powinno tak być Encoding.UTF8, a może Encoding.Defaultpodczas czytania strumienia żądań? Lub po prostu użyj StreamReader( z usuwaniem zastrzeżeń )
drzaus
5

OK, więc wygląda na to, że odpowiedź brzmi "nie, nie możesz pobrać surowych danych, musisz zrekonstruować żądanie / odpowiedź z właściwości przeanalizowanych obiektów". No cóż, zrobiłem rekonstrukcję.

Greg Beech
źródło
3
Czy widziałeś komentarz firmy Vineus na temat zmiennych ServerVariables ["ALL_RAW"]? Sam jeszcze tego nie próbowałem, ale udokumentowano, że zwraca nieprzetworzone informacje nagłówka dokładnie tak, jak zostały przesłane przez klienta. Nawet jeśli dokument okaże się błędny, a robi rekonstrukcję, hej, bezpłatna rekonstrukcja :-)
Jonathan Gilbert
3

użyj IHttpModule :

    namespace Intercepts
{
    class Interceptor : IHttpModule
    {
        private readonly InterceptorEngine engine = new InterceptorEngine();

        #region IHttpModule Members

        void IHttpModule.Dispose()
        {
        }

        void IHttpModule.Init(HttpApplication application)
        {
            application.EndRequest += new EventHandler(engine.Application_EndRequest);
        }
        #endregion
    }
}

    class InterceptorEngine
    {       
        internal void Application_EndRequest(object sender, EventArgs e)
        {
            HttpApplication application = (HttpApplication)sender;

            HttpResponse response = application.Context.Response;
            ProcessResponse(response.OutputStream);
        }

        private void ProcessResponse(Stream stream)
        {
            Log("Hello");
            StreamReader sr = new StreamReader(stream);
            string content = sr.ReadToEnd();
            Log(content);
        }

        private void Log(string line)
        {
            Debugger.Log(0, null, String.Format("{0}\n", line));
        }
    }
FigmentEngine
źródło
3
Według Alexa i mojego własnego doświadczenia nie sądzę, abyś mógł czytać z HttpResponse.OutputStream, więc Twoja metoda logowania w metodzie ProcessResponse prawdopodobnie nie zadziała.
William Gross
2
William ma rację. HttpResponse.OutputStream jest nieczytelny. Znajduję rozwiązanie, które polega na wykorzystaniu HttpResponse.Filter i zastąpieniu domyślnego strumienia wyjściowego własnym.
Eric Fan,
endurasoft.com/blog/post/… async Httpmodule lepiej?
PreguntonCojoneroCabrón
3

jeśli do okazjonalnego użytku, aby ominąć ciasny róg, co powiesz na coś prymitywnego jak poniżej?

Public Function GetRawRequest() As String
    Dim str As String = ""
    Dim path As String = "C:\Temp\REQUEST_STREAM\A.txt"
    System.Web.HttpContext.Current.Request.SaveAs(path, True)
    str = System.IO.File.ReadAllText(path)
    Return str
End Function
Baburaj
źródło
1

Możesz to zrobić DelegatingHandlerbez korzystania z OutputFilterwymienionych w innych odpowiedziach w .NET 4.5 za pomocą Stream.CopyToAsync()funkcji.

Nie jestem pewien szczegółów, ale nie powoduje to wszystkich złych rzeczy, które mają miejsce, gdy próbujesz bezpośrednio odczytać strumień odpowiedzi.

Przykład:

public class LoggingHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        DoLoggingWithRequest(request);
        var response = await base.SendAsync(request, cancellationToken);
        await DoLoggingWithResponse(response);
        return response;
    }

    private async Task DologgingWithResponse(HttpResponseMessage response) {
        var stream = new MemoryStream();
        await response.Content.CopyToAsync(stream).ConfigureAwait(false);     
        DoLoggingWithResponseContent(Encoding.UTF8.GetString(stream.ToArray()));

        // The rest of this call, the implementation of the above method, 
        // and DoLoggingWithRequest is left as an exercise for the reader.
    }
}
Talonj
źródło
0

Wiem, że to nie jest kod zarządzany, ale mam zamiar zasugerować filtr ISAPI. Minęło kilka lat, odkąd miałem „przyjemność” utrzymywać własny ISAPI, ale z tego, co pamiętam, możesz uzyskać dostęp do wszystkich tych rzeczy, zarówno przed, jak i po tym, jak ASP.Net już to zrobił.

http://msdn.microsoft.com/en-us/library/ms524610.aspx

Jeśli moduł HTTPModule nie jest wystarczająco dobry dla tego, czego potrzebujesz, po prostu nie sądzę, aby można było to zrobić w wymaganej ilości szczegółów. Jednak będzie to trudne.

Chris
źródło
0

Zgadzam się z innymi, użyj IHttpModule. Spójrz na odpowiedź na to pytanie, która robi prawie to samo, o co pytasz. Rejestruje żądanie i odpowiedź, ale bez nagłówków.

Jak śledzić żądania ScriptService WebService?

jrummell
źródło
0

Najlepiej zrobić to poza aplikacją. Możesz skonfigurować odwrotne proxy, aby robić takie rzeczy (i wiele więcej). Odwrotny serwer proxy to w zasadzie serwer WWW, który znajduje się w twojej serwerowni i znajduje się między twoim serwerem (serwerami) WWW a klientem. Zobacz http://en.wikipedia.org/wiki/Reverse_proxy

Lance Fisher
źródło
0

Zgadzam się z FigmentEngine, IHttpModulewydaje się być drogą do zrobienia.

Spójrz w httpworkerrequest, readentitybodyi GetPreloadedEntityBody.

Aby uzyskać httpworkerrequest, musisz to zrobić:

(HttpWorkerRequest)inApp.Context.GetType().GetProperty("WorkerRequest", bindingFlags).GetValue(inApp.Context, null);

gdzie inAppjest obiekt httpapplication.

stevenrcfox
źródło
1
Powiedziałem już, że ta odpowiedź nie jest odpowiednia, ponieważ nie zawiera większości informacji, o które prosiłem. W jakim stopniu ta odpowiedź jest pomocna?
Greg Beech
Bardziej wyjaśnij, w jaki sposób ta odpowiedź jest w jakikolwiek sposób pomocna?
PreguntonCojoneroCabrón
0

HttpRequesta HttpResponsewcześniej MVC miał GetInputStream()i GetOutputStream()może być użyty do tego celu. Nie zaglądałem do tych części w MVC, więc nie jestem pewien, czy są dostępne, ale może to być pomysł :)

Rune FS
źródło