Registrazione richiesta / risposta HTTP non elaborata in ASP.NET MVC e IIS7


141

Sto scrivendo un servizio web (usando ASP.NET MVC) e per scopi di supporto vorremmo essere in grado di registrare le richieste e le risposte il più vicino possibile al formato raw, on-the-wire (cioè incluso HTTP metodo, percorso, tutte le intestazioni e il corpo) in un database.

Ciò di cui non sono sicuro è come ottenere questi dati nel modo meno "alterato". Posso ricostituire ciò che credo assomigli alla richiesta controllando tutte le proprietà HttpRequestdell'oggetto e costruendo una stringa da loro (e similmente per la risposta), ma mi piacerebbe davvero ottenere i dati effettivi di richiesta / risposta che sono inviato sul filo.

Sono felice di utilizzare qualsiasi meccanismo di intercettazione come filtri, moduli, ecc. E la soluzione può essere specifica per IIS7. Tuttavia, preferirei tenerlo solo nel codice gestito.

Qualche consiglio?

Modifica: noto che HttpRequestha un SaveAsmetodo che può salvare la richiesta su disco ma questo ricostruisce la richiesta dallo stato interno usando un carico di metodi di supporto interno a cui non è possibile accedere pubblicamente (proprio perché questo non consente il salvataggio in un utente fornito stream non lo so). Quindi inizia a sembrare che dovrò fare del mio meglio per ricostruire il testo richiesta / risposta dagli oggetti ... gemere.

Modifica 2: Nota che ho detto che l' intera richiesta include metodo, percorso, intestazioni, ecc. Le risposte attuali guardano solo i flussi del corpo che non includono queste informazioni.

Modifica 3: nessuno legge domande qui? Cinque risposte finora, eppure nessuno suggerisce nemmeno un modo per ottenere l'intera richiesta non elaborata. Sì, so di poter acquisire i flussi di output, le intestazioni e l'URL e tutto il resto dall'oggetto richiesta. Ho già detto che nella domanda, vedi:

Posso ricostituire ciò che credo assomigli alla richiesta controllando tutte le proprietà dell'oggetto HttpRequest e costruendo una stringa da loro (e similmente per la risposta) ma mi piacerebbe davvero ottenere i dati effettivi di richiesta / risposta che viene inviato sul filo.

Se conosci i dati grezzi completi (inclusi intestazioni, url, metodo http, ecc.) Semplicemente non possono essere recuperati, sarebbe utile saperlo. Allo stesso modo se sai come ottenere tutto nel formato non elaborato (sì, intendo ancora includere intestazioni, url, metodo http, ecc.) Senza doverlo ricostruire, che è quello che ho chiesto, sarebbe molto utile. Ma dirmi che posso ricostruirlo da HttpRequest/ HttpResponseoggetti non è utile. Lo so. L'ho già detto.


Nota: prima che qualcuno inizi a dire che questa è una cattiva idea, o limiterà la scalabilità, ecc., Implementeremo anche meccanismi di limitazione, consegna sequenziale e anti-replay in un ambiente distribuito, quindi è comunque richiesta la registrazione del database. Non sto cercando una discussione se questa è una buona idea, sto cercando come può essere fatto.


1
@Kev - No, è un servizio RESTful implementato usando ASP.NET MVC
Greg Beech,

Probabilmente è possibile fare usando IIS7 e un modulo nativo - msdn.microsoft.com/en-us/library/ms694280.aspx
Daniel Crenna

Sei riuscito a implementarlo? Solo curioso, hai adottato qualche strategia di buffer per scrivere su db?
systempuntoout

1
Progetto interessante ... se finisci per farlo, con la soluzione finale?
PreguntonCojoneroCabrón,

Risposte:


91

Sicuramente utilizzare IHttpModulee implementare gli eventi BeginRequeste EndRequest.

Tutti i dati "grezzi" sono presenti tra HttpRequeste HttpResponse, semplicemente non sono in un unico formato raw. Ecco le parti necessarie per costruire dump in stile Fiddler (più o meno vicini all'HTTP grezzo):

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

Per la risposta:

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

Si noti che non è possibile leggere il flusso di risposta, quindi è necessario aggiungere un filtro al flusso di output e acquisire una copia.

Nel tuo BeginRequest, dovrai aggiungere un filtro di risposta:

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

Conservare filterdove è possibile accedervi nel EndRequestgestore. Suggerisco di HttpContext.Items. È quindi possibile ottenere i dati di risposta completi filter.ReadStream().

Quindi implementa OutputFilterStreamusando il modello Decorator come un wrapper attorno a un flusso:

/// <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
Bella risposta. Un commento però: hai detto "È quindi possibile ottenere tutti i dati di risposta in filter.ToString ()." -non intendi filter.ReadStream ()? (Sto implementando in vb.net non c # ma se eseguo ToString ottengo solo il nome della classe come stringa. .ReadStream restituisce il corpo di risposta desiderato.
Adam

Sono d'accordo, buona risposta. L'ho usato come base per un logger personalizzato, ma ora ho riscontrato un problema in cui mancano alcune intestazioni e, soprattutto, quando si utilizza la compressione IIS non riesco ad accedere alla risposta compressa finale. Ho iniziato una nuova domanda correlata ( stackoverflow.com/questions/11084459/… ) per questo.
Chris,

2
Penso che mckamey sia un genio. Puoi andare a lavorare per Microsoft in modo da ottenere soluzioni intelligenti invece di dover avere soluzioni alternative brillanti?
Abacus

4
Fai attenzione a request.RawUrl potrebbe attivare un'eccezione di convalida della richiesta. In 4.5 è possibile utilizzare request.Unvalidated.RawUrl per impedirlo. In 4.0 ho finito per usare un po 'di riflessione per imitare Request.SaveAs
Freek il

1
@mckamey Cerco di implementare la tua soluzione nel mio global.asax con Application_BeginRequest e Application_EndRequest ma non sono sicuro di quale codice dovrei scrivere in EndRequest, puoi fornire un esempio nella tua risposta per favore?
Jerome2606,

48

Il seguente metodo di estensione su HttpRequest creerà una stringa che può essere incollata in fiddler e riprodotta.

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
Codice molto buono! Ma perché questo lavoro con MVC 4 ho dovuto cambiare il nome della classe per HttpRequestBaseExtensionse cambiare HttpRequesta HttpRequestBasein ogni luogo.
Dmitry,

35

È possibile utilizzare la variabile del server ALL_RAW per ottenere le intestazioni HTTP originali inviate con la richiesta, quindi è possibile ottenere InputStream come al solito:

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

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


Questo ha funzionato anche per me. Non era nemmeno necessario essere in un gestore. Sono stato ancora in grado di accedervi dalla pagina.
Helephant

3
O nel contesto del server ASP.NET, utilizzare: this.Request.ServerVariables ["ALL_RAW"];
Peter Stegnar,

Non riesco a ottenere il corpo della richiesta da Request.InputStream, restituisce "" per me ogni volta, tuttavia ALL_RAW funziona benissimo per restituire le intestazioni della richiesta, quindi questa risposta è metà giusta.
Giustino,

1
Puoi anche usare HttpContext.Current.Requestper afferrare il contesto attuale al di fuori dei controller MVC, delle pagine ASPX, ecc ... assicurati solo che non sia prima null;)
jocull

16

Bene, sto lavorando a un progetto e ho fatto, forse non troppo in profondità, un registro usando i parametri di richiesta:

Guarda:

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

Puoi decorare la tua classe controller per registrarla interamente:

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

o registra solo alcuni metodi di azione individuali

[Log]
public ActionResult LoggedAction(){...}

12

Qual è il motivo per cui è necessario tenerlo nel codice gestito?

Vale la pena ricordare che è possibile abilitare la registrazione della traccia non riuscita in IIS7 se non ti piace reinventare la ruota. Ciò registra le intestazioni, il corpo della richiesta e della risposta e molte altre cose.

Registrazione traccia non riuscita


E se non fosse un fallimento?
Sinaesthetic,

7
È possibile utilizzare anche la registrazione della traccia non riuscita con HTTP 200 OK, quindi è ancora possibile registrare i non guasti
JoelBellot,

2
Questa è di gran lunga la soluzione più semplice.
Kehlan Krumme,

Per vedere l'intera traccia dello stack, era necessario aggiungere GlobalConfiguration.Configuration.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;alla fine di Register(..)in WebApiConfig.cs, ma ciò può variare tra le versioni.
Evgeni Sergeev,

8

Sono andato con l'approccio di McKAMEY. Ecco un modulo che ho scritto che ti farà iniziare e spero di farti risparmiare un po 'di tempo. Ovviamente dovrai collegare il Logger con qualcosa che funzioni per te:

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
Non puoi trasmettere in modo sicuro app.Response.Filter a qualcosa di diverso da uno stream. Altri HttpModules possono racchiudere il proprio filtro di risposta con il proprio e in questo caso si otterrà un'eccezione di cast non valida.
Micah Zoltu,

Non dovrebbe essere Encoding.UTF8o forse Encoding.Defaultdurante la lettura del flusso di richieste? O semplicemente usa un StreamReader( con avvertenze per lo smaltimento )
drzaus,

5

OK, quindi sembra che la risposta sia "no, non puoi ottenere i dati grezzi, devi ricostruire la richiesta / risposta dalle proprietà degli oggetti analizzati". Oh bene, ho fatto la ricostruzione.


3
Hai visto il commento di Vineus su ServerVariables ["ALL_RAW"]? Non l'ho ancora provato da solo, ma è documentato per restituire le informazioni di intestazione non elaborate esattamente come inviate dal client. Anche se il documento risulta essere sbagliato, e sta facendo una ricostruzione, ehi, ricostruzione gratuita :-)
Jonathan Gilbert,

3

utilizzare un 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
Per Alex e la mia esperienza, non credo che tu possa leggere da HttpResponse.OutputStream, quindi il tuo metodo di accesso al metodo ProcessResponse probabilmente non funzionerà.
William Gross,

2
William ha ragione. HttpResponse.OutputStream non è leggibile. Trovo una soluzione che consiste nell'utilizzare HttpResponse.Filter e sostituire il flusso di output predefinito con il tuo.
Eric Fan,


3

se per un uso occasionale, per aggirare un angolo stretto, che ne dici di qualcosa di grezzo come sotto?

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

È possibile eseguire ciò in a DelegatingHandlersenza utilizzare le OutputFiltermenzionate in altre risposte in .NET 4.5 utilizzando la Stream.CopyToAsync()funzione.

Non sono sicuro sui dettagli, ma non innesca tutte le cose brutte che accadono quando si tenta di leggere direttamente il flusso di risposta.

Esempio:

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

So che non è un codice gestito, ma suggerirò un filtro ISAPI. Sono passati un paio d'anni da quando ho avuto il "piacere" di mantenere il mio ISAPI, ma da quello che ricordo è possibile accedere a tutte queste cose, sia prima che dopo ASP.Net ha fatto la sua cosa.

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

Se un HTTPModule non è abbastanza buono per quello che ti serve, allora non penso che ci sia un modo gestito per farlo nella quantità richiesta di dettagli. Sarà doloroso però.



0

Potrebbe essere meglio farlo al di fuori della tua applicazione. Puoi impostare un proxy inverso per fare cose del genere (e molto altro). Un proxy inverso è fondamentalmente un server Web che si trova nella stanza del server e si trova tra i server Web e il client. Vedi http://en.wikipedia.org/wiki/Reverse_proxy


0

Concordo con FigmentEngine, IHttpModulesembra essere la strada da percorrere.

Se vuoi in httpworkerrequest, readentitybodye GetPreloadedEntityBody.

Per ottenere ciò httpworkerrequestè necessario fare questo:

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

dove si inApptrova l'oggetto httpapplication.


1
Ho già detto che la risposta non è adatta perché non acquisisce la maggior parte delle informazioni richieste. In che modo questa risposta è in qualche modo utile?
Greg Beech,

Altre spiegazioni, in che modo questa risposta è in qualche modo utile?
PreguntonCojoneroCabrón,

0

HttpRequeste HttpResponsepre MVC aveva un GetInputStream()e GetOutputStream()che poteva essere usato a tale scopo. Non ho esaminato quelle parti in MVC, quindi non sono sicuro che siano disponibili ma potrebbe essere un'idea :)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.