Реєстрація необмеженого запиту / відповіді HTTP в ASP.NET MVC & IIS7


141

Я пишу веб-службу (використовую 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
Daniel

Вам вдалося це здійснити? Цікаво, чи прийняли ви будь-яку буферну стратегію для запису на db?
systempuntoout

1
Цікавий проект ... якщо ви все-таки робите це, з остаточним рішенням?
PreguntonCojoneroCabrón

Відповіді:


91

Однозначно використовувати 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використовуючи шаблон декоратора як обгортку навколо потоку:

/// <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/… ) для цього.
Кріс

2
Я думаю, що mckamey - геній. Чи можете ви працювати на Майкрософт, щоб ми просто отримували розумні рішення замість того, щоб мати блискучі шляхи вирішення?
Абак

4
Остерігайтеся цього запиту.RawUrl може викликати виключення перевірки запиту. У 4.5 ви можете використовувати request.Unvalidated.RawUrl, щоб запобігти цьому. В 4.0 я в кінцевому підсумку використовувала деякі відображення для мімічних Request.SaveAs
Фрік

1
@mckamey Я намагаюся реалізувати ваше рішення в моєму global.asax за допомогою Application_BeginRequest та Application_EndRequest, але я не впевнений, який код потрібно написати в EndRequest, чи можете ви навести приклад у своїй відповіді PLZ?
Jerome2606

48

Наступний метод розширення на HttpRequest створить рядок, який можна вставити в fiddler і відтворити.

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
Дуже хороший код! Але для цього , щоб працювати з 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


Це працювало і для мене. Навіть не потрібно було бути обробником. Я все ще зміг отримати доступ до нього зі сторінки.
Гелефант

3
Або в контексті сервера ASP.NET використовуйте: this.Request.ServerVariables ["ALL_RAW"];
Петро Стегнар

Я не в змозі отримати тіло запиту від 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(){...}

12

Будь-яка причина, що вам потрібно зберегти його в керованому коді?

Варто зазначити, що ви можете ввімкнути невдалий запис трасування в IIS7, якщо вам не подобається переосмислити колесо. Це журнали журналів, орган запиту та відповіді, а також багато інших речей.

Помилка реєстрації слідів


Що робити, якщо це не провал?
Синестетичний

7
Ви також можете користуватися журналом Failed Trace за допомогою HTTP 200 OK, тому невдачі все ще можна реєструвати
JoelBellot

2
Це, безумовно, найпростіше рішення.
Келан Крумме

Щоб побачити весь слід стека, потрібно було додати GlobalConfiguration.Configuration.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;в кінці Register(..)в WebApiConfig.cs, але це може відрізнятися між версіями.
Євгеній Сергєєв

8

Я пішов із підходом Маккемі. Ось модуль, який я написав, який почне вас розпочати, і, сподіваюся, заощадить вам час. Вам потрібно буде підключити 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
    }
}

3
Ви не можете безпечно передавати app.Response.Filter ні до чого іншого, крім потоку. Інші HttpModules можуть обернути ваш фільтр відповідей власним, і в цьому випадку ви отримаєте недійсне виняток з кадра.
Міхей Золту

Чи не повинно бути Encoding.UTF8, чи, може, Encoding.Defaultчитаючи потік запитів? Або просто скористайтеся StreamReader( із розпорядженням )
drzaus

5

Гаразд, так виглядає відповідь "ні, ви не можете отримати необроблені дані, ви повинні реконструювати запит / відповідь із властивостей проаналізованих об'єктів". Ну добре, я зробив реконструкцію.


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

3
За Алексом і моїм власним досвідом, я не думаю, що ви можете прочитати з HttpResponse.OutputStream, тому ваш метод входу в метод ProcessResponse, ймовірно, не буде працювати.
Вільям Гросс

2
Вільям має рацію. HttpResponse.OutputStream не читабельний. Я знаходжу рішення, яке полягає у використанні HttpResponse.Filter та заміні вихідного потоку за замовчуванням на власний.
Ерік Фан


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.
    }
}

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.


1
Я вже казав, що відповідь не підходить, оскільки вона не охоплює більшість інформації, яку я просив. Чим ця відповідь якимось чином корисна?
Грег Бук

Детальніше поясніть: чим ця відповідь якимось чином корисна?
PreguntonCojoneroCabrón

0

HttpRequestі HttpResponseпопередньо MVC використовується , щоб мати GetInputStream()і GetOutputStream()які можуть бути використані для цієї мети. Не заглядав у цю частину MVC, тому я не впевнений, що вони є доступними, але це може бути ідеєю :)

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.