Protokollieren der rohen HTTP-Anforderung / Antwort in ASP.NET MVC & IIS7

140

Ich schreibe einen Webdienst (mit ASP.NET MVC) und zu Supportzwecken möchten wir die Anforderungen und Antworten so nah wie möglich am unformatierten On-the-Wire-Format (dh einschließlich HTTP) protokollieren können Methode, Pfad, alle Header und der Body) in eine Datenbank.

Ich bin mir nicht sicher, wie ich diese Daten auf die am wenigsten "verstümmelte" Weise erreichen kann. Ich kann wieder herstellen, wie die Anfrage meiner Meinung nach aussieht, indem ich alle Eigenschaften des HttpRequestObjekts überprüfe und daraus eine Zeichenfolge erstelle (und ähnlich für die Antwort), aber ich möchte wirklich die tatsächlichen Anforderungs- / Antwortdaten erhalten, die es sind auf den Draht geschickt.

Ich verwende gerne Abfangmechanismen wie Filter, Module usw., und die Lösung kann spezifisch für IIS7 sein. Ich würde es jedoch vorziehen, es nur in verwaltetem Code zu belassen.

Irgendwelche Empfehlungen?

Bearbeiten: Ich stelle fest, dass HttpRequestes eine SaveAsMethode gibt, mit der die Anforderung auf der Festplatte gespeichert werden kann, die jedoch die Anforderung aus dem internen Status mithilfe einer Reihe interner Hilfsmethoden rekonstruiert, auf die nicht öffentlich zugegriffen werden kann (weshalb dies das Speichern auf einem vom Benutzer bereitgestellten Gerät nicht zulässt Stream weiß ich nicht). Es sieht also so aus, als müsste ich mein Bestes geben, um den Anforderungs- / Antworttext aus den Objekten zu rekonstruieren ... Stöhnen.

Bearbeiten 2: Bitte beachten Sie, dass ich die gesamte Anfrage einschließlich Methode, Pfad, Header usw. gesagt habe . Die aktuellen Antworten beziehen sich nur auf die Body-Streams, die diese Informationen nicht enthalten.

Edit 3: Liest hier niemand Fragen? Bisher fünf Antworten, von denen noch keine einen Hinweis darauf gibt, wie die gesamte rohe On-the-Wire-Anfrage beantwortet werden kann. Ja, ich weiß, dass ich die Ausgabestreams und die Header sowie die URL und all das Zeug vom Anforderungsobjekt erfassen kann. Ich habe das bereits in der Frage gesagt, siehe:

Ich kann wieder herstellen, wie die Anfrage meiner Meinung nach aussieht, indem ich alle Eigenschaften des HttpRequest-Objekts überprüfe und daraus eine Zeichenfolge (und ähnlich für die Antwort) erstelle, aber ich möchte wirklich die tatsächlichen Anforderungs- / Antwortdaten abrufen das ist auf dem Draht gesendet.

Wenn Sie wissen, dass die vollständigen Rohdaten (einschließlich Header, URL, http-Methode usw.) einfach nicht abgerufen werden können, ist dies hilfreich. Wenn Sie wissen, wie Sie alles im Rohformat erhalten (ja, ich meine immer noch Header, URL, http-Methode usw.), ohne es rekonstruieren zu müssen, was ich gefragt habe, dann wäre das sehr nützlich. Es ist jedoch nicht sinnvoll , mir zu sagen, dass ich es aus den HttpRequest/ HttpResponse-Objekten rekonstruieren kann. Ich weiß das. Ich habe es schon gesagt.


Bitte beachten Sie: Bevor jemand anfängt zu sagen, dass dies eine schlechte Idee ist oder die Skalierbarkeit usw. einschränkt, werden wir auch Drosselungs-, sequentielle Übermittlungs- und Anti-Replay-Mechanismen in einer verteilten Umgebung implementieren, sodass ohnehin eine Datenbankprotokollierung erforderlich ist. Ich suche keine Diskussion darüber, ob dies eine gute Idee ist, ich suche nach Möglichkeiten, wie dies getan werden kann.

Greg Beech
quelle
1
@ Kevin - Nein, es ist ein RESTful-Service, der mit ASP.NET MVC implementiert wurde
Greg Beech
Es ist wahrscheinlich möglich, IIS7 und ein natives Modul zu verwenden - msdn.microsoft.com/en-us/library/ms694280.aspx
Daniel Crenna
Haben Sie es geschafft, dies umzusetzen? Nur neugierig, haben Sie eine Pufferstrategie angewendet, um in db zu schreiben?
Systempuntoout
1
Interessantes Projekt ... wenn Sie es am Ende tun, mit endgültiger Lösung?
PreguntonCojoneroCabrón

Antworten:

91

Verwenden Sie auf jeden Fall ein IHttpModuleund implementieren Sie die BeginRequestund EndRequest-Ereignisse.

Alle "Rohdaten" sind zwischen HttpRequestund HttpResponsenur in einem einzigen Rohformat vorhanden. Hier sind die Teile, die zum Erstellen von Dumps im Fiddler-Stil benötigt werden (ungefähr so ​​nah wie möglich an rohem HTTP):

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

Für die Antwort:

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

Beachten Sie, dass Sie den Antwortstrom nicht lesen können, sodass Sie dem Ausgabestream einen Filter hinzufügen und eine Kopie erfassen müssen.

In Ihrem BeginRequestmüssen Sie einen Antwortfilter hinzufügen:

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

Speichern filterSie im EndRequestHandler, wo Sie darauf zugreifen können. Ich schlage vor HttpContext.Items. Dort können dann die vollständigen Antwortdaten abgerufen werden filter.ReadStream().

Implementieren Sie dann OutputFilterStreamdas Decorator-Muster als Wrapper um einen Stream:

/// <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
quelle
1
Gute Antwort. Ein Kommentar: Sie sagten: "In filter.ToString () können dann die vollständigen Antwortdaten abgerufen werden." - meinst du nicht filter.ReadStream ()? (Ich implementiere in vb.net nicht c #, aber wenn ich ToString ausführe, erhalte ich nur den Klassennamen als Zeichenfolge. .ReadStream gibt den gewünschten Antworttext zurück.
Adam
Ich stimme zu, gute Antwort. Ich habe es als Grundlage für einen benutzerdefinierten Logger verwendet, bin aber jetzt auf ein Problem gestoßen, bei dem einige Header fehlen, und vor allem kann ich bei Verwendung der IIS-Komprimierung nicht auf die endgültige komprimierte Antwort zugreifen. Ich habe dafür eine neue verwandte Frage ( stackoverflow.com/questions/11084459/… ) gestartet .
Chris
2
Ich denke, McKamey ist ein Genie. Können Sie für Microsoft arbeiten, damit wir nur intelligente Lösungen erhalten, anstatt brillante Problemumgehungen zu benötigen?
Abacus
4
Beachten Sie, dass request.RawUrl möglicherweise eine Ausnahme für die Anforderungsvalidierung auslöst. In 4.5 können Sie request.Unvalidated.RawUrl verwenden, um dies zu verhindern. In 4.0 habe ich einige Reflexionen verwendet, um Request.SaveAs
Freek
1
@mckamey Ich versuche, Ihre Lösung in meiner global.asax mit Application_BeginRequest und Application_EndRequest zu implementieren, bin mir aber nicht sicher, welchen Code ich in EndRequest schreiben soll. Können Sie ein Beispiel in Ihrer Antwort plz angeben?
Jerome2606
48

Die folgende Erweiterungsmethode für HttpRequest erstellt eine Zeichenfolge, die in Fiddler eingefügt und wiedergegeben werden kann.

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
quelle
5
Sehr guter Code! Aber für diese Arbeit mit MVC 4 Ich hatte die Klassennamen zu ändern HttpRequestBaseExtensionsund zu ändern , HttpRequestum HttpRequestBasean jedem Ort.
Dmitry
35

Sie können die Servervariable ALL_RAW verwenden, um die ursprünglichen HTTP-Header abzurufen, die mit der Anforderung gesendet wurden. Anschließend können Sie den InputStream wie gewohnt abrufen:

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

Überprüfen Sie: http://msdn.microsoft.com/en-us/library/ms524602%28VS.90%29.aspx

Vincent de Lagabbe
quelle
Das hat auch bei mir funktioniert. Musste nicht einmal in einem Handler sein. Ich konnte immer noch von der Seite darauf zugreifen.
Helephant
3
Oder verwenden Sie im ASP.NET-Serverkontext: this.Request.ServerVariables ["ALL_RAW"];
Peter Stegnar
Ich kann den Anforderungshauptteil nicht von Request.InputStream abrufen. Er gibt jedes Mal "" für mich zurück. ALL_RAW eignet sich jedoch hervorragend zum Zurückgeben der Anforderungsheader, sodass diese Antwort zur Hälfte richtig ist.
Justin
1
Sie können auch verwenden HttpContext.Current.Request, um den aktuellen Kontext außerhalb von MVC-Controllern, ASPX-Seiten usw.
abzurufen. Stellen
16

Nun, ich arbeite an einem Projekt und habe, vielleicht nicht zu tief, ein Protokoll mit den Anforderungsparametern erstellt:

Schau mal:

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

Sie können Ihre Controller-Klasse dekorieren, um sie vollständig zu protokollieren:

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

oder protokollieren Sie nur einige einzelne Aktionsmethoden

[Log]
public ActionResult LoggedAction(){...}
John Prado
quelle
12

Gibt es einen Grund, warum Sie es im verwalteten Code behalten müssen?

Es ist erwähnenswert , dass Sie aktivieren können Trace - Protokollierung können nicht in IIS7 , wenn Sie nicht wie neu zu erfinden das Rad. Dies protokolliert Header, den Anforderungs- und Antworttext sowie viele andere Dinge.

Trace-Protokollierung fehlgeschlagen

JoelBellot
quelle
Was ist, wenn es kein Fehler ist?
Sinaesthetic
7
Sie können die Protokollierung fehlgeschlagener Ablaufverfolgung auch mit HTTP 200 OK verwenden, sodass Nicht-Fehler weiterhin protokolliert werden können
JoelBellot
2
Dies ist bei weitem die einfachste Lösung.
Kehlan Krumme
Um den gesamten Stack-Trace zu sehen, musste GlobalConfiguration.Configuration.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;am Ende von Register(..)in hinzugefügt werden. WebApiConfig.csDies kann jedoch zwischen den Versionen variieren.
Evgeni Sergeev
8

Ich habe mich für McKAMEY entschieden. Hier ist ein Modul, das ich geschrieben habe, um Ihnen den Einstieg zu erleichtern und Ihnen hoffentlich etwas Zeit zu sparen. Sie müssen den Logger offensichtlich mit etwas verbinden, das für Sie funktioniert:

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
quelle
3
Sie können app.Response.Filter nicht sicher auf etwas anderes als einen Stream übertragen. Andere HttpModules können Ihren Antwortfilter mit einem eigenen umschließen. In diesem Fall erhalten Sie eine ungültige Besetzungsausnahme.
Micah Zoltu
Sollte es nicht sein Encoding.UTF8oder vielleicht Encoding.Defaultbeim Lesen des Anforderungsstroms? Oder verwenden Sie einfach eine StreamReader( mit Vorbehalt )
drzaus
5

OK, es sieht also so aus, als ob die Antwort "Nein, Sie können die Rohdaten nicht erhalten, Sie müssen die Anforderung / Antwort aus den Eigenschaften der analysierten Objekte rekonstruieren" lautet. Na ja, ich habe den Wiederaufbau gemacht.

Greg Beech
quelle
3
Haben Sie Vineus 'Kommentar zu ServerVariables ["ALL_RAW"] gesehen? Ich habe es selbst noch nicht ausprobiert, aber es ist dokumentiert, dass die Rohdaten des Headers genau so zurückgegeben werden, wie sie vom Client gesendet wurden. Auch wenn sich herausstellt, dass das Dokument falsch ist und eine Rekonstruktion durchführt, hey, kostenlose Rekonstruktion :-)
Jonathan Gilbert
3

Verwenden Sie ein 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
quelle
3
Laut Alex und meiner eigenen Erfahrung können Sie nicht aus HttpResponse.OutputStream lesen, sodass Ihre Methode zum Anmelden bei der ProcessResponse-Methode wahrscheinlich nicht funktioniert.
William Gross
2
William hat recht. HttpResponse.OutputStream ist nicht lesbar. Ich finde eine Lösung, die darin besteht, HttpResponse.Filter zu verwenden und den Standardausgabestream durch Ihren eigenen zu ersetzen.
Eric Fan
endurasoft.com/blog/post/… async Httpmodule besser?
PreguntonCojoneroCabrón
3

Wenn Sie es gelegentlich benutzen, um um eine enge Ecke zu kommen, wie wäre es dann mit etwas Grobem wie unten?

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
quelle
1

Sie können dies in a erreichen, DelegatingHandlerohne die OutputFilterin anderen Antworten in .NET 4.5 genannten zu verwendenStream.CopyToAsync() Funktionen zu verwenden.

Ich bin mir bei den Details nicht sicher, aber es löst nicht alle schlechten Dinge aus, die passieren, wenn Sie versuchen, den Antwortstrom direkt zu lesen.

Beispiel:

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
quelle
0

Ich weiß, dass es sich nicht um verwalteten Code handelt, aber ich werde einen ISAPI-Filter vorschlagen. Es ist ein paar Jahre her, seit ich das "Vergnügen" hatte, mein eigenes ISAPI zu pflegen, aber soweit ich mich erinnere, können Sie auf all diese Dinge zugreifen, sowohl vor als auch nachdem ASP.Net es getan hat.

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

Wenn ein HTTPModul nicht gut genug für das ist, was Sie brauchen, dann glaube ich einfach nicht, dass es eine verwaltete Möglichkeit gibt, dies in der erforderlichen Menge an Details zu tun. Es wird allerdings ein Schmerz sein.

Chris
quelle
0

Ich stimme den anderen zu, benutze ein IHttpModule. Schauen Sie sich die Antwort auf diese Frage an, die fast das Gleiche tut, was Sie stellen. Es protokolliert die Anforderung und Antwort, jedoch ohne Header.

Wie verfolge ich ScriptService WebService-Anforderungen?

jrummell
quelle
0

Es ist möglicherweise am besten, dies außerhalb Ihrer Anwendung zu tun. Sie können einen Reverse-Proxy einrichten, um solche Dinge (und vieles mehr) zu tun. Ein Reverse-Proxy ist im Grunde ein Webserver, der sich in Ihrem Serverraum befindet und zwischen Ihren Webservern und dem Client steht. Siehe http://en.wikipedia.org/wiki/Reverse_proxy

Lance Fisher
quelle
0

Stimmen Sie mit FigmentEngine überein, IHttpModule scheint der Weg zu sein.

Schauen Sie in httpworkerrequest, readentitybodyundGetPreloadedEntityBody .

Um das zu bekommen httpworkerrequest, müssen Sie dies tun:

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

Wo inAppist das httpapplication-Objekt?

stevenrcfox
quelle
1
Ich habe bereits gesagt, dass die Antwort nicht geeignet ist, da sie die meisten Informationen, nach denen ich gefragt habe, nicht erfasst. Wie ist diese Antwort in irgendeiner Weise hilfreich?
Greg Beech
Weitere Erklärungen: Wie ist diese Antwort in irgendeiner Weise hilfreich?
PreguntonCojoneroCabrón
0

HttpRequestund HttpResponsevor MVC hatte früher ein GetInputStream()und GetOutputStream()das könnte für diesen Zweck verwendet werden. Ich habe mir diesen Teil in MVC nicht angesehen, daher bin ich mir nicht sicher, ob er verfügbar ist, könnte aber eine Idee sein :)

Rune FS
quelle