Ведение журнала необработанного HTTP-запроса / ответа в ASP.NET MVC и IIS7

144

Я пишу веб-службу (используя ASP.NET MVC), и в целях поддержки мы хотели бы иметь возможность регистрировать запросы и ответы как можно ближе к необработанному, беспроводному формату (т.е. включая HTTP метод, путь, все заголовки и тело) в базу данных.

Я не уверен в том, как получить эти данные наименее "искаженным" способом. Я могу заново составить то, что, по моему мнению, выглядит как запрос, проверив все свойства HttpRequestобъекта и построив из них строку (и аналогично для ответа), но я действительно хотел бы получить фактические данные запроса / ответа, которые отправлено по телеграфу.

Я счастлив использовать любой механизм перехвата, такой как фильтры, модули и т. Д., И решение может быть специфичным для IIS7. Однако я бы предпочел оставить его только в управляемом коде.

Какие-нибудь рекомендации?

Изменить: я отмечаю, что у HttpRequestнего есть SaveAsметод, который может сохранить запрос на диск, но он восстанавливает запрос из внутреннего состояния, используя загрузку внутренних вспомогательных методов, к которым нельзя получить доступ публично (вот почему это не позволяет сохранить в предоставленный пользователем поток не знаю). Так что это начинает выглядеть так, как будто мне придется сделать все возможное, чтобы восстановить текст запроса / ответа из объектов ... стон.

Изменить 2: Обратите внимание, что я сказал весь запрос, включая метод, путь, заголовки и т. Д. Текущие ответы смотрят только на потоки тела, которые не включают эту информацию.

Изменить 3: здесь никто не читает вопросы? На данный момент пять ответов, но ни один из них даже не намекает на способ получить весь необработанный запрос по сети. Да, я знаю, что могу захватывать выходные потоки, заголовки, URL-адрес и все такое из объекта запроса. Я уже сказал, что в вопросе см .:

Я могу заново составить то, что, по моему мнению, выглядит как запрос, проверив все свойства объекта HttpRequest и построив из них строку (и аналогично для ответа), но я бы очень хотел получить фактические данные запроса / ответа. это отправлено по телеграфу.

Если вы знаете, что полные необработанные данные (включая заголовки, URL-адрес, метод http и т. Д.) Просто не могут быть получены, это было бы полезно знать. Точно так же, если вы знаете, как получить все это в необработанном формате (да, я все еще имею в виду заголовки, URL-адрес, метод http и т.д.) без необходимости его восстанавливать, о чем я просил, тогда это было бы очень полезно. Но рассказывать мне, что я могу восстановить его из HttpRequest/ HttpResponseобъектов, бесполезно. Я знаю это. Я уже сказал это.


Обратите внимание: прежде чем кто-либо начнет говорить, что это плохая идея или ограничит масштабируемость и т. Д., Мы также будем внедрять механизмы регулирования, последовательной доставки и предотвращения повторного воспроизведения в распределенной среде, поэтому в любом случае потребуется ведение журнала базы данных. Я не ищу обсуждения того, хорошая ли это идея, я ищу, как это можно сделать.

Грег Бич
источник
1
@Kev - Нет, это служба RESTful, реализованная с использованием ASP.NET MVC
Грег Бич,
Вероятно, можно использовать IIS7 и собственный модуль - msdn.microsoft.com/en-us/library/ms694280.aspx
Дэниел Кренна,
Удалось ли вам это реализовать? Просто любопытно, вы использовали какую-либо буферную стратегию для записи в db?
systempuntoout
1
Интересный проект ... если вы все-таки сделаете его, с окончательным решением?
PreguntonCojoneroCabrón

Ответы:

92

Определенно использовать IHttpModuleи реализовать BeginRequestи EndRequestсобытия.

Все «сырые» данные присутствуют между HttpRequestи HttpResponse, их просто нет в едином необработанном формате. Вот части, необходимые для создания дампов в стиле Fiddler (примерно как можно ближе к необработанному 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

Для ответа:

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

Обратите внимание, что вы не можете прочитать поток ответа, поэтому вам нужно добавить фильтр в выходной поток и захватить копию.

В вашем BeginRequestвам нужно будет добавить фильтр ответа:

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

Магазин, filterгде вы можете добраться до него в EndRequestобработчике. Предлагаю в HttpContext.Items. Затем можно получить полные данные ответа в формате filter.ReadStream().

Затем реализуйте OutputFilterStreamиспользование шаблона Decorator в качестве оболочки для потока:

/// <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
Хороший ответ. Один комментарий: вы сказали: «Затем можно получить полные данные ответа в filter.ToString ()». - разве вы не имеете в виду filter.ReadStream ()? (Я реализую в vb.net, а не на С #, но если я запустил ToString, я просто получаю имя класса в виде строки. .ReadStream возвращает желаемое тело ответа.
Адам
Согласен, хороший ответ. Я использовал его как основу для пользовательского регистратора, но теперь столкнулся с проблемой, когда некоторые заголовки отсутствуют, и, что наиболее важно, при использовании сжатия IIS я не могу получить доступ к окончательному сжатому ответу. Я задал для этого новый связанный вопрос ( stackoverflow.com/questions/11084459/… ).
Chris
2
Я считаю, что Макками гений. Можете ли вы пойти работать в Microsoft, чтобы мы просто получали интеллектуальные решения вместо того, чтобы искать блестящие обходные пути?
Abacus
4
Помните, что request.RawUrl может вызвать исключение проверки запроса. В 4.5 вы можете использовать request.Unvalidated.RawUrl, чтобы предотвратить это. В 4.0 я использовал рефлексию для имитации Request.SaveAs
Freek
1
@mckamey Я пытаюсь реализовать ваше решение в моем global.asax с помощью Application_BeginRequest и Application_EndRequest, но я не уверен, какой код мне следует написать в EndRequest, можете ли вы предоставить пример в своем ответе, пожалуйста?
Jerome2606
50

Следующий метод расширения в HttpRequest создаст строку, которую можно вставить в скрипт и воспроизвести.

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;
            }
        }
    }
}
Сэм Шайлз
источник
6
Очень хороший код! Но для этого , чтобы работать с MVC 4 я должен был изменить имя класса HttpRequestBaseExtensionsи изменить , HttpRequestчтобы HttpRequestBaseв каждом месте.
Дмитрий
35

Вы можете использовать серверную переменную ALL_RAW, чтобы получить исходные заголовки HTTP, отправленные с запросом, затем вы можете получить InputStream как обычно:

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

проверить: http://msdn.microsoft.com/en-us/library/ms524602%28VS.90%29.aspx

Винсент де Лагаббе
источник
Это сработало и для меня. Даже не нужно было быть обработчиком. Я все еще мог получить к нему доступ со страницы.
Helephant
3
Или в контексте сервера ASP.NET используйте: this.Request.ServerVariables ["ALL_RAW"];
Питер Стегнар 08
Я не могу получить тело запроса из Request.InputStream, он каждый раз возвращает "" для меня, однако ALL_RAW отлично подходит для возврата заголовков запроса, поэтому этот ответ наполовину правильный.
Джастин
1
Вы также можете использовать HttpContext.Current.Requestдля получения текущего контекста за пределами контроллеров MVC, страниц ASPX и т. Д., Просто убедитесь, что сначала он не
равен
16

Ну, я работаю над проектом и сделал, может быть, не слишком глубокий журнал, используя параметры запроса:

Взглянем:

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

Вы можете украсить свой класс контроллеров, чтобы полностью записать его:

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

или зарегистрируйте только некоторые отдельные методы действий

[Log]
public ActionResult LoggedAction(){...}
Джон Прадо
источник
13

По какой причине вам нужно сохранить его в управляемом коде?

Стоит упомянуть, что вы можете включить ведение журнала Failed Trace в IIS7, если вам не нравится изобретать колесо заново. При этом регистрируются заголовки, тело запроса и ответа, а также многое другое.

Неудачная запись трассировки

JoelBellot
источник
Что, если это не провал?
Sinaesthetic
7
Вы также можете использовать ведение журнала Failed Trace с HTTP 200 OK, чтобы все еще можно было регистрировать
исправления ошибок
2
Это, безусловно, самое простое решение.
Кехлан Крумме
Чтобы увидеть всю трассировку стека, необходимо было добавить GlobalConfiguration.Configuration.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;в конец Register(..)in WebApiConfig.cs, но это может варьироваться в зависимости от версии.
Евгений Сергеев
9

Я согласился с подходом McKAMEY. Вот модуль, который я написал, который поможет вам начать работу и, надеюсь, сэкономит вам время. Очевидно, вам нужно подключить Logger к чему-то, что вам подходит:

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
источник
3
Вы не можете безопасно использовать app.Response.Filter для чего-либо, кроме Stream. Другие HttpModules могут обернуть ваш фильтр ответов своим собственным, и в этом случае вы получите недопустимое исключение приведения.
Мика Золту
Не должно быть Encoding.UTF8, а может Encoding.Defaultпри чтении потока запроса? Или просто используйте StreamReader( с
предупреждениями об
6

Итак, похоже, ответ - «нет, вы не можете получить необработанные данные, вам нужно восстановить запрос / ответ из свойств проанализированных объектов». Ну что ж, я сделал реконструкцию.

Грег Бич
источник
3
Вы видели комментарий Vineus о ServerVariables ["ALL_RAW"]? Я сам еще не пробовал, но это задокументировано, чтобы возвращать необработанную информацию заголовка точно в том виде, в каком она была отправлена ​​клиентом. Даже если документ окажется неправильным, и он будет реконструирован, эй, бесплатная реконструкция :-)
Джонатан Гилберт
3

используйте 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
источник
3
Согласно Алексу и моему собственному опыту, я не думаю, что вы можете читать из HttpResponse.OutputStream, поэтому ваш метод входа в метод ProcessResponse, вероятно, не будет работать.
Уильям Гросс,
2
Уильям прав. HttpResponse.OutputStream не читается. Я нахожу решение, которое заключается в использовании HttpResponse.Filter и замене выходного потока по умолчанию вашим собственным.
Эрик Фан,
endurasoft.com/blog/post/… async Httpmodule лучше?
PreguntonCojoneroCabrón
3

если для случайного использования, чтобы обойти узкий угол, как насчет чего-нибудь грубого, как показано ниже?

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

Вы можете сделать это DelegatingHandlerбез использования OutputFilterупомянутых в других ответах в .NET 4.5 с помощью Stream.CopyToAsync()функции.

Я не уверен в деталях, но это не вызывает всех плохих вещей, которые происходят, когда вы пытаетесь напрямую прочитать поток ответа.

Пример:

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
источник
0

Я знаю, что это не управляемый код, но я собираюсь предложить фильтр ISAPI. Прошло несколько лет с тех пор, как я имел «удовольствие» поддерживать свой собственный ISAPI, но насколько я помню, вы можете получить доступ ко всему этому материалу как до, так и после того, как ASP.Net сделал это.

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

Если HTTPModule недостаточно хорош для того, что вам нужно, я просто не думаю, что есть какой-либо управляемый способ сделать это с требуемым количеством деталей. Хотя это будет больно.

Крис
источник
0

Возможно, лучше сделать это вне вашего приложения. Вы можете настроить обратный прокси, чтобы делать такие вещи (и многое другое). Обратный прокси-сервер - это в основном веб-сервер, который находится в вашей серверной комнате и стоит между вашим веб-сервером (-ами) и клиентом. См. Http://en.wikipedia.org/wiki/Reverse_proxy

Лэнс Фишер
источник
0

Согласитесь с FigmentEngine, IHttpModuleпохоже, это правильный путь.

Загляните внутрь httpworkerrequest, readentitybodyи GetPreloadedEntityBody.

Для получения httpworkerrequestвам необходимо сделать следующее:

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

где inAppнаходится объект httpapplication.

Stevenrcfox
источник
1
Я уже сказал, что этот ответ не подходит, потому что он не отражает большую часть информации, которую я просил. Чем полезен этот ответ?
Грег Бич,
Больше объясните, чем этот ответ хоть как-то полезен?
PreguntonCojoneroCabrón
0

HttpRequestи HttpResponsepre MVC использовали, GetInputStream()и GetOutputStream()это можно было использовать для этой цели. Я не изучал эту часть в MVC, поэтому я не уверен, что они доступны, но может быть идеей :)

Руна FS
источник