Registro de solicitud / respuesta HTTP sin procesar en ASP.NET MVC e IIS7

140

Estoy escribiendo un servicio web (usando ASP.NET MVC) y para fines de soporte, nos gustaría poder registrar las solicitudes y la respuesta lo más cerca posible del formato sin formato en el cable (es decir, incluido HTTP método, ruta, todos los encabezados y el cuerpo) en una base de datos.

De lo que no estoy seguro es de cómo obtener estos datos de la manera menos 'destrozada'. Puedo reconstituir el aspecto que creo que tiene la solicitud al inspeccionar todas las propiedades del HttpRequestobjeto y construir una cadena a partir de ellos (y de manera similar para la respuesta), pero realmente me gustaría obtener los datos reales de solicitud / respuesta que están enviado por cable.

Me complace utilizar cualquier mecanismo de intercepción, como filtros, módulos, etc., y la solución puede ser específica para IIS7. Sin embargo, preferiría mantenerlo solo en código administrado.

¿Alguna recomendación?

Editar: Observo que HttpRequesttiene un SaveAsmétodo que puede guardar la solicitud en el disco, pero esto reconstruye la solicitud desde el estado interno utilizando una carga de métodos auxiliares internos a los que no se puede acceder públicamente (bastante por qué esto no permite guardar en un archivo proporcionado por el usuario corriente no lo sé). Así que parece que tendré que hacer todo lo posible para reconstruir el texto de solicitud / respuesta de los objetos ... gemido.

Edición 2: Tenga en cuenta que dije toda la solicitud, incluido el método, la ruta, los encabezados, etc. Las respuestas actuales solo miran las secuencias del cuerpo que no incluyen esta información.

Edición 3: ¿Nadie lee las preguntas por aquí? Cinco respuestas hasta el momento y, sin embargo, ninguna sugiere siquiera una forma de obtener toda la solicitud cruda en el cable. Sí, sé que puedo capturar las secuencias de salida y los encabezados y la URL y todas esas cosas del objeto de solicitud. Ya dije eso en la pregunta, ver:

Puedo reconstituir lo que creo que se ve la solicitud al inspeccionar todas las propiedades del objeto HttpRequest y construir una cadena a partir de ellas (y de manera similar para la respuesta), pero realmente me gustaría obtener los datos reales de solicitud / respuesta eso se envía por cable.

Si sabe que los datos sin procesar completos (incluidos los encabezados, la URL, el método http, etc.) simplemente no se pueden recuperar, sería útil saberlo. Del mismo modo, si sabe cómo obtenerlo todo en formato sin formato (sí, todavía me refiero a incluir encabezados, url, método http, etc.) sin tener que reconstruirlo, que es lo que pedí, entonces sería muy útil. Pero decirme que puedo reconstruirlo desde los objetos HttpRequest/ HttpResponseno es útil. Yo sé eso. Ya lo dije


Tenga en cuenta: antes de que alguien comience a decir que es una mala idea, o limitará la escalabilidad, etc., también implementaremos mecanismos de aceleración, entrega secuencial y antirrepetición en un entorno distribuido, por lo que se requiere el registro de la base de datos de todos modos. No estoy buscando una discusión sobre si esta es una buena idea, estoy buscando cómo se puede hacer.

Greg Beech
fuente
1
@Kev - No, es un servicio RESTful implementado usando ASP.NET MVC
Greg Beech
Probablemente sea posible hacerlo usando IIS7 y un módulo nativo - msdn.microsoft.com/en-us/library/ms694280.aspx
Daniel Crenna
¿Has logrado implementar esto? Por curiosidad, ¿adoptó alguna estrategia de búfer para escribir en db?
systemmpuntoout
1
Proyecto interesante ... si terminas haciéndolo, ¿con la solución final?
PreguntonCojoneroCabrón

Respuestas:

91

Definitivamente use un IHttpModulee implemente el BeginRequestyEndRequest eventos .

Todos los datos "sin procesar" están presentes entre HttpRequesty HttpResponse, simplemente, no están en un solo formato sin procesar. Estas son las partes necesarias para crear volcados de estilo Fiddler (lo más cerca posible de HTTP sin procesar):

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

Para la respuesta:

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

Tenga en cuenta que no puede leer la secuencia de respuesta, por lo que debe agregar un filtro a la secuencia de salida y capturar una copia.

En su BeginRequest, deberá agregar un filtro de respuesta:

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

Almacene filterdonde pueda acceder en el EndRequestcontrolador. Sugiero en HttpContext.Items. Luego puede obtener los datos de respuesta completa filter.ReadStream().

Luego implemente OutputFilterStreamusando el patrón Decorator como envoltorio alrededor de una secuencia:

/// <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
fuente
1
Buena respuesta. Sin embargo, un comentario: usted dijo "Entonces puede obtener los datos de respuesta completa en filter.ToString ()". -no te refieres a filter.ReadStream ()? (Estoy implementando en vb.net, no en C #, pero si ejecuto ToString solo obtengo el nombre de la clase como una cadena. .ReadStream devuelve el cuerpo de respuesta deseado.
Adam
Estoy de acuerdo, buena respuesta. Lo he usado como base para un registrador personalizado, pero ahora me he encontrado con un problema en el que faltan algunos encabezados y, lo que es más importante, cuando uso la compresión IIS, no puedo acceder a la respuesta comprimida final. Comencé una nueva pregunta relacionada ( stackoverflow.com/questions/11084459/… ) para esto.
Chris
2
Creo que mckamey es un genio. ¿Puedes ir a trabajar para Microsoft para que podamos obtener soluciones inteligentes en lugar de tener que tener soluciones alternativas brillantes?
Abacus
44
Tenga en cuenta que request.RawUrl podría desencadenar una excepción de validación de solicitud. En 4.5 puede usar request.Unvalidated.RawUrl para evitar esto. En 4.0 terminé usando un poco de reflexión para imitar Request.SaveAs
Freek
1
@mckamey Intento implementar su solución en mi global.asax con Application_BeginRequest y Application_EndRequest pero no estoy seguro de qué código debo escribir en EndRequest, ¿puede proporcionar un ejemplo en su respuesta por favor?
Jerome2606
48

El siguiente método de extensión en HttpRequest creará una cadena que puede pegarse en el violinista y reproducirse.

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
fuente
55
Muy buen código! Pero para que esto funcione con MVC 4 tuve que cambiar el nombre de la clase HttpRequestBaseExtensionsy cambiar HttpRequesta HttpRequestBaseen cada lugar.
Dmitry
35

Puede usar la variable de servidor ALL_RAW para obtener los encabezados HTTP originales enviados con la solicitud, luego puede obtener InputStream como de costumbre:

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

echa un vistazo: http://msdn.microsoft.com/en-us/library/ms524602%28VS.90%29.aspx

Vincent de Lagabbe
fuente
Esto funcionó para mí también. Ni siquiera necesitaba estar en un controlador. Todavía podía acceder desde la página.
Helephant
3
O en el contexto del servidor ASP.NET, use: this.Request.ServerVariables ["ALL_RAW"];
Peter Stegnar
No puedo obtener el cuerpo de solicitud de Request.InputStream, me devuelve "" cada vez, sin embargo, ALL_RAW funciona muy bien para devolver los encabezados de solicitud, por lo que esta respuesta es la mitad correcta.
Justin
1
También puede usar HttpContext.Current.Requestpara tomar el contexto actual fuera de los controladores MVC, páginas ASPX, etc., solo asegúrese de que no sea nulo primero;)
jocull
16

Bueno, estoy trabajando en un proyecto e hice, tal vez no demasiado profundo, un registro usando los parámetros de solicitud:

Echar un vistazo:

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

Puede decorar su clase de controladores para registrarla por completo:

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

o registrar solo algunos métodos de acción individuales

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

¿Alguna razón para mantenerlo en código administrado?

Vale la pena mencionar que puede habilitar el registro de Failed Trace en IIS7 si no le gusta reinventar la rueda. Esto registra los encabezados, el cuerpo de solicitud y respuesta, así como muchas otras cosas.

Registro de seguimiento fallido

JoelBellot
fuente
¿Qué pasa si no es un fracaso?
Sinaesthetic
77
También puede utilizar el registro de seguimiento fallido con HTTP 200 OK, por lo que aún se pueden registrar los
fallos
2
Esta es, con mucho, la solución más simple.
Kehlan Krumme
Para ver el seguimiento completo de la pila, fue necesario agregar GlobalConfiguration.Configuration.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;al final de Register(..)in WebApiConfig.cs, pero esto puede variar entre versiones.
Evgeni Sergeev
8

Seguí el enfoque de McKAMEY. Aquí hay un módulo que escribí que lo ayudará a comenzar y, con suerte, le ahorrará algo de tiempo. Obviamente, deberá conectar el Logger con algo que funcione para usted:

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
fuente
3
No puede enviar de forma segura app.Response.Filter a otra cosa que no sea Stream. Otros HttpModules pueden ajustar su filtro de respuesta con los suyos y, en este caso, obtendrá una excepción de conversión no válida.
Micah Zoltu
¿No debería ser así Encoding.UTF8, o tal vez Encoding.Defaultal leer la secuencia de solicitud? O simplemente use un StreamReader( con advertencias de eliminación )
drzaus
5

De acuerdo, parece que la respuesta es "no, no puede obtener los datos sin procesar, debe reconstruir la solicitud / respuesta a partir de las propiedades de los objetos analizados". Oh bueno, he hecho lo de la reconstrucción.

Greg Beech
fuente
3
¿Viste el comentario de Vineus sobre ServerVariables ["ALL_RAW"]? Todavía no lo he probado, pero está documentado para devolver la información del encabezado sin procesar exactamente como la envió el cliente. Incluso si el documento resulta estar equivocado, y está haciendo una reconstrucción, oye, reconstrucción gratuita :-)
Jonathan Gilbert
3

use 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));
        }
    }
FigmentEngine
fuente
3
Según Alex y mi propia experiencia, no creo que pueda leer de HttpResponse.OutputStream, por lo que su método de inicio de sesión en el método ProcessResponse probablemente no funcionará.
William Gross el
2
William tiene razón. HttpResponse.OutputStream no es legible. Encuentro una solución que es usar HttpResponse.Filter y reemplazar la secuencia de salida predeterminada por la suya.
Eric Fan
endurasoft.com/blog/post/… async Httpmodule mejor?
PreguntonCojoneroCabrón
3

si para uso ocasional, para moverse en una esquina cerrada, ¿qué tal algo crudo como el de abajo?

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

Puede lograr esto DelegatingHandlersin usar lo OutputFiltermencionado en otras respuestas en .NET 4.5 usando la Stream.CopyToAsync()función.

No estoy seguro de los detalles, pero no activa todas las cosas malas que suceden cuando intenta leer directamente el flujo de respuesta.

Ejemplo:

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

Sé que no es código administrado, pero voy a sugerir un filtro ISAPI. Han pasado un par de años desde que tuve el "placer" de mantener mi propio ISAPI, pero por lo que recuerdo, puede tener acceso a todo esto, tanto antes como después de que ASP.Net haya hecho todo.

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

Si un HTTPModule no es lo suficientemente bueno para lo que necesita, entonces no creo que haya una forma administrada de hacerlo con la cantidad de detalles requerida. Sin embargo, será un dolor.

Chris
fuente
0

Puede ser mejor hacer esto fuera de su aplicación. Puede configurar un proxy inverso para hacer cosas como esta (y mucho más). Un proxy inverso es básicamente un servidor web que se encuentra en su sala de servidores y se interpone entre su (s) servidor (es) web y el cliente. Ver http://en.wikipedia.org/wiki/Reverse_proxy

Lance Fisher
fuente
0

De acuerdo con FigmentEngine, IHttpModuleparece ser el camino a seguir.

Mira en httpworkerrequest, readentitybodyy GetPreloadedEntityBody.

Para obtener lo httpworkerrequestque necesita hacer esto:

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

donde inAppestá el objeto httpapplication.

stevenrcfox
fuente
1
Ya dije que la respuesta no es adecuada porque no captura la mayor parte de la información que solicité. ¿De qué manera es útil esta respuesta?
Greg Beech
Más explique, ¿de qué manera es útil esta respuesta?
PreguntonCojoneroCabrón
0

HttpRequesty HttpResponsepre MVC solía tener un GetInputStream()y GetOutputStream()que podría usarse para ese propósito. No he examinado esas partes en MVC, así que no estoy seguro de que estén disponibles, pero podría ser una idea :)

Runa FS
fuente