Registrar solicitação / resposta HTTP bruta no ASP.NET MVC e IIS7

140

Estou escrevendo um serviço da Web (usando o ASP.NET MVC) e, para fins de suporte, gostaríamos de poder registrar as solicitações e respostas o mais próximo possível do formato bruto on-the-wire (ou seja, incluindo HTTP método, caminho, todos os cabeçalhos e o corpo) em um banco de dados.

O que não tenho certeza é como se apossar desses dados da maneira menos 'confusa'. Posso reconstituir a aparência da solicitação, inspecionando todas as propriedades do HttpRequestobjeto e construindo uma string a partir delas (e da mesma forma para a resposta), mas eu realmente gostaria de me apossar dos dados reais de solicitação / resposta que são enviado no fio.

É um prazer usar qualquer mecanismo de interceptação, como filtros, módulos, etc., e a solução pode ser específica ao IIS7. No entanto, eu preferiria mantê-lo apenas no código gerenciado.

Alguma recomendação?

Editar: notei que HttpRequestpossui um SaveAsmétodo que pode salvar a solicitação em disco, mas isso reconstrói a solicitação a partir do estado interno usando uma carga de métodos auxiliares internos que não podem ser acessados ​​publicamente (por que isso não permite salvar em um usuário fornecido) fluxo que eu não sei). Então está começando a parecer que vou ter que fazer o meu melhor para reconstruir o texto da solicitação / resposta dos objetos ... gemer.

Editar 2: Observe que eu disse que toda a solicitação, incluindo método, caminho, cabeçalhos, etc. As respostas atuais apenas examinam os fluxos do corpo que não incluem essas informações.

Edit 3: Ninguém lê perguntas por aqui? Cinco respostas até o momento e ainda nem uma delas sugere uma maneira de obter todo o pedido bruto on-the-wire. Sim, eu sei que posso capturar os fluxos de saída e os cabeçalhos, a URL e todas essas coisas do objeto de solicitação. Eu já disse isso na pergunta, veja:

Posso reconstituir a aparência da solicitação, inspecionando todas as propriedades do objeto HttpRequest e construindo uma string a partir delas (e da mesma forma para a resposta), mas eu realmente gostaria de me apossar dos dados reais da solicitação / resposta que é enviado pelo fio.

Se você souber que os dados brutos completos (incluindo cabeçalhos, URL, método http etc.) simplesmente não podem ser recuperados, isso seria útil. Da mesma forma, se você souber como obter tudo no formato bruto (sim, eu ainda quero dizer incluindo cabeçalhos, URL, método http, etc.) sem precisar reconstruí-lo, como pedi, isso seria muito útil. Mas me dizer que posso reconstruí-lo a partir de HttpRequest/ HttpResponseobjects não é útil. Eu sei disso. Eu já disse isso.


Observação: antes que alguém comece a dizer que é uma má idéia ou que limite a escalabilidade, etc., também estaremos implementando mecanismos de otimização, entrega sequencial e anti-replay em um ambiente distribuído, de modo que o log do banco de dados seja necessário. Não estou à procura de uma discussão sobre se essa é uma boa ideia, estou procurando como isso pode ser feito.

Greg Beech
fonte
1
@Kev - Não, é um serviço RESTful implementada usando ASP.NET MVC
Greg Beech
Provavelmente é possível usar o IIS7 e um módulo nativo - msdn.microsoft.com/en-us/library/ms694280.aspx
Daniel Crenna
Você conseguiu implementar isso? Apenas curioso, você adotou alguma estratégia de buffer para gravar no db?
systempuntoout
1
Projeto interessante ... se você acabar fazendo isso, com solução final?
PreguntonCojoneroCabrón

Respostas:

91

Definitivamente usar um IHttpModulee implementar as BeginRequeste EndRequesteventos.

Todos os dados "brutos" estão presentes entre HttpRequeste HttpResponsesimplesmente não estão em um único formato bruto. Aqui estão as partes necessárias para criar dumps no estilo Fiddler (o mais próximo possível do HTTP bruto):

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 a resposta:

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

Observe que você não pode ler o fluxo de resposta, portanto, você deve adicionar um filtro ao fluxo de Saída e capturar uma cópia.

No seu BeginRequest, você precisará adicionar um filtro de resposta:

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

Armazene filteronde você pode acessá-lo no EndRequestmanipulador. Eu sugiro HttpContext.Items. Em seguida, é possível obter os dados completos da resposta filter.ReadStream().

Em seguida, implemente OutputFilterStreamusando o padrão Decorator como um wrapper em torno de um fluxo:

/// <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
fonte
1
Boa resposta. Um comentário: você disse "Em seguida, é possível obter os dados completos da resposta em filter.ToString ()." -Você não quer dizer filter.ReadStream ()? (Estou implementando em vb.net não c #, mas se eu executar ToString acabei de receber o nome da classe como uma string .ReadStream retorna o corpo da resposta desejada..
Adam
Eu concordo, boa resposta. Usei-o como base para um criador de logs personalizado, mas agora deparei com um problema em que alguns cabeçalhos estão ausentes e, o mais importante ao usar a compactação do IIS, não consigo acessar a resposta final compactada. Comecei uma nova pergunta relacionada ( stackoverflow.com/questions/11084459/… ) para isso.
19412 Chris
2
Eu acho que mckamey é um gênio. Você pode trabalhar na Microsoft para obter soluções inteligentes em vez de precisar de soluções alternativas brilhantes?
Abacus
4
Cuidado com o pedido.RawUrl pode acionar uma exceção de validação do pedido. Na 4.5, você pode usar request.Unvalidated.RawUrl para evitar isso. Em 4.0 acabei usando alguma reflexão para Request.SaveAs imitar
Freek
1
@mckamey Eu tento implementar sua solução no meu global.asax com Application_BeginRequest e Application_EndRequest, mas não tenho certeza de qual código devo escrever no EndRequest, você pode fornecer um exemplo na sua resposta plz?
precisa saber é o seguinte
48

O seguinte método de extensão no HttpRequest criará uma sequência que pode ser colada no violinista e reproduzida.

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
fonte
5
Código muito bom! Mas para que isso funcione com MVC 4 eu tive que mudar o nome da classe de HttpRequestBaseExtensionse para mudar HttpRequestpara HttpRequestBasea cada lugar.
Dmitry
35

Você pode usar a variável do servidor ALL_RAW para obter os cabeçalhos HTTP originais enviados com a solicitação e obter o InputStream como de costume:

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

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

Vincent de Lagabbe
fonte
Isso funcionou para mim também. Nem precisava estar em um manipulador. Ainda consegui acessá-lo a partir da página.
Helephant
3
Ou no contexto do servidor ASP.NET, use: this.Request.ServerVariables ["ALL_RAW"];
Peter Stegnar
Não consigo obter o corpo da solicitação em Request.InputStream, ele "" retorna sempre para mim, no entanto, ALL_RAW funciona muito bem para retornar os cabeçalhos da solicitação, portanto esta resposta está certa.
23714 Justin
1
Você também pode usar HttpContext.Current.Requestpara pegar o exterior contexto atual de controladores MVC, páginas ASPX, etc ... apenas certifique-se que não é nulo o primeiro;)
jocull
16

Bem, estou trabalhando em um projeto e fiz, talvez não muito profundo, um log usando os parâmetros de solicitação:

Dê uma olhada:

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

Você pode decorar sua classe de controladores para registrá-la inteiramente:

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

ou registre apenas alguns métodos de ação individuais

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

Algum motivo para você precisar mantê-lo no código gerenciado?

Vale ressaltar que você pode habilitar o log do Failed Trace no IIS7 se não gostar de reinventar a roda. Isso registra os cabeçalhos, o corpo da solicitação e resposta, além de muitas outras coisas.

Log de rastreamento com falha

JoelBellot
fonte
E se não for um fracasso?
Sinaesthetic
7
Você também pode usar o log de rastreamento com falha com HTTP 200 OK, para que as não falhas ainda possam ser registradas
JoelBellot
2
Esta é de longe a solução mais simples.
Kehlan Krumme
Para ver todo o rastreamento da pilha, era necessário adicionar GlobalConfiguration.Configuration.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;no final de Register(..)in WebApiConfig.cs, mas isso pode variar entre as versões.
Evgeni Sergeev
8

Eu fui com a abordagem de McKAMEY. Aqui está um módulo que escrevi para você começar e, com sorte, economizar algum tempo. Você precisará conectar o Logger obviamente a algo que funcione para você:

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
fonte
3
Você não pode transmitir app.Response.Filter com segurança para algo que não seja um Stream. Outros HttpModules podem agrupar seu filtro de resposta com seus próprios e, nesse caso, você receberá uma exceção de conversão inválida.
Micah Zoltu 17/10/2013
Não deveria ser Encoding.UTF8, ou talvez Encoding.Defaultao ler o fluxo de solicitações? Ou apenas use um StreamReader( com ressalvas )
drzaus
5

OK, então parece que a resposta é "não, você não pode obter os dados brutos, é necessário reconstruir a solicitação / resposta a partir das propriedades dos objetos analisados". Oh, bem, eu fiz a coisa da reconstrução.

Greg Beech
fonte
3
Você viu o comentário de Vineus sobre ServerVariables ["ALL_RAW"]? Eu ainda não tentei, mas está documentado para retornar as informações brutas do cabeçalho exatamente como enviadas pelo cliente. Mesmo se o documento acaba por ser errado, e ele está fazendo uma reconstrução, hey, a reconstrução livre :-)
Jonathan Gilbert
3

use um 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
fonte
3
De acordo com Alex e minha própria experiência, acho que você não pode ler HttpResponse.OutputStream, portanto, seu método de logon no método ProcessResponse provavelmente não funcionará.
William Gross
2
William está certo. HttpResponse.OutputStream não é legível. Eu encontro uma solução que é usar HttpResponse.Filter e substituir o fluxo de saída padrão pelo seu.
Eric Fan
endurasoft.com/blog/post/… assíncrono Httpmodule melhor?
PreguntonCojoneroCabrón
3

se para uso ocasional, para contornar um canto apertado, que tal algo bruto como abaixo?

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

Você pode fazer isso DelegatingHandlersem usar o OutputFiltermencionado em outras respostas no .NET 4.5 usando a Stream.CopyToAsync()função

Não tenho certeza dos detalhes, mas ele não aciona todas as coisas ruins que acontecem quando você tenta ler diretamente o fluxo de resposta.

Exemplo:

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

Sei que não é um código gerenciado, mas vou sugerir um filtro ISAPI. Faz alguns anos desde que tive o "prazer" de manter meu próprio ISAPI, mas pelo que me lembro, você pode ter acesso a todas essas coisas, antes e depois do ASP.Net ter feito isso.

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

Se um HTTPModule não é bom o suficiente para o que você precisa, simplesmente não acho que exista uma maneira gerenciada de fazer isso na quantidade necessária de detalhes. Vai ser uma dor de fazer embora.

Chris
fonte
0

Eu concordo com os outros, use um IHttpModule. Dê uma olhada na resposta a esta pergunta, que faz quase a mesma coisa que você está fazendo. Ele registra a solicitação e a resposta, mas sem cabeçalhos.

Como rastrear solicitações de ScriptService WebService?

jrummell
fonte
0

Talvez seja melhor fazer isso fora do seu aplicativo. Você pode configurar um proxy reverso para fazer coisas assim (e muito mais). Um proxy reverso é basicamente um servidor da Web que fica na sua sala de servidores e fica entre o (s) servidor (es) da Web e o cliente. Veja http://en.wikipedia.org/wiki/Reverse_proxy

Lance Fisher
fonte
0

Concordo com o FigmentEngine, IHttpModuleparece ser o caminho a seguir.

Olhar para httpworkerrequest, readentitybodye GetPreloadedEntityBody.

Para obter o que httpworkerrequestvocê precisa, faça o seguinte:

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

onde inAppestá o objeto httpapplication.

stevenrcfox
fonte
1
Eu já disse que a resposta não é adequada porque não captura a maioria das informações solicitadas. Como essa resposta é de alguma forma útil?
Greg Beech
Mais explicações: Como essa resposta é útil?
PreguntonCojoneroCabrón
0

HttpRequeste o HttpResponseMVC pré costumava ter um GetInputStream()e GetOutputStream()que poderia ser usado para esse fim. Não olhei para as partes do MVC, então não tenho certeza de que elas estejam disponíveis, mas pode ser uma ideia :)

Rune FS
fonte