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ć.


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);
    }
}

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;
            }
        }
    }
}

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


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(){...}

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


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
    }
}

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ę.


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));
        }
    }

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,


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

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.
    }
}

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.



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


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.


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ł :)

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.