Đăng nhập yêu cầu / phản hồi HTTP thô trong ASP.NET MVC & IIS7


140

Tôi đang viết một dịch vụ web (sử dụng ASP.NET MVC) và cho các mục đích hỗ trợ, chúng tôi muốn có thể ghi nhật ký các yêu cầu và phản hồi gần nhất có thể với định dạng thô, trực tuyến (bao gồm cả HTTP phương thức, đường dẫn, tất cả các tiêu đề và phần thân) vào cơ sở dữ liệu.

Điều tôi không chắc chắn là làm thế nào để có được dữ liệu này theo cách ít bị sai lệch nhất. Tôi có thể tạo lại những gì tôi tin rằng yêu cầu trông giống như bằng cách kiểm tra tất cả các thuộc tính của HttpRequestđối tượng và tạo chuỗi từ chúng (và tương tự cho phản hồi) nhưng tôi thực sự muốn giữ dữ liệu yêu cầu / phản hồi thực tế gửi trên dây.

Tôi rất vui khi sử dụng bất kỳ cơ chế chặn nào như bộ lọc, mô-đun, v.v. và giải pháp có thể dành riêng cho IIS7. Tuy nhiên, tôi chỉ muốn giữ nó trong mã được quản lý.

Có khuyến nghị nào không?

Chỉnh sửa: Tôi lưu ý rằng HttpRequestcó một SaveAsphương thức có thể lưu yêu cầu vào đĩa nhưng điều này tái tạo lại yêu cầu từ trạng thái bên trong bằng cách sử dụng tải các phương thức của trình trợ giúp nội bộ không thể truy cập công khai (hoàn toàn tại sao điều này không cho phép lưu vào người dùng cung cấp stream tôi không biết). Vì vậy, nó bắt đầu giống như tôi sẽ phải cố gắng hết sức để xây dựng lại văn bản yêu cầu / phản hồi từ các đối tượng ... than thở.

Chỉnh sửa 2: Xin lưu ý rằng tôi đã nói toàn bộ yêu cầu bao gồm phương thức, đường dẫn, tiêu đề, v.v ... Các phản hồi hiện tại chỉ nhìn vào các luồng cơ thể không bao gồm thông tin này.

Chỉnh sửa 3: Không ai đọc câu hỏi quanh đây? Năm câu trả lời cho đến nay và thậm chí không một gợi ý nào về cách nhận được toàn bộ yêu cầu trực tuyến. Có, tôi biết tôi có thể nắm bắt các luồng đầu ra, các tiêu đề và URL và tất cả những thứ đó từ đối tượng yêu cầu. Tôi đã nói rằng trong câu hỏi, xem:

Tôi có thể tạo lại những gì tôi tin rằng yêu cầu trông giống như bằng cách kiểm tra tất cả các thuộc tính của đối tượng HttpRequest và xây dựng một chuỗi từ chúng (và tương tự cho phản hồi) nhưng tôi thực sự muốn nắm giữ dữ liệu yêu cầu / phản hồi thực tế đó là gửi trên dây.

Nếu bạn biết toàn bộ dữ liệu thô (bao gồm các tiêu đề, url, phương thức http, v.v.) đơn giản là không thể truy xuất được thì điều đó sẽ hữu ích khi biết. Tương tự như vậy nếu bạn biết cách lấy tất cả ở định dạng thô (vâng, tôi vẫn có nghĩa là bao gồm các tiêu đề, url, phương thức http, v.v.) mà không phải xây dựng lại, đó là những gì tôi đã hỏi, thì điều đó sẽ rất hữu ích. Nhưng nói với tôi rằng tôi có thể xây dựng lại nó từ HttpRequest/ HttpResponsecác đối tượng là không hữu ích. Tôi biết điều đó. Tôi đã nói rồi.


Xin lưu ý: Trước khi bất kỳ ai bắt đầu nói rằng đây là một ý tưởng tồi hoặc sẽ hạn chế khả năng mở rộng, v.v., chúng tôi cũng sẽ thực hiện các cơ chế điều tiết, phân phối tuần tự và chống phát lại trong một môi trường phân tán, vì vậy dù sao thì cũng cần phải đăng nhập cơ sở dữ liệu. Tôi không tìm kiếm một cuộc thảo luận về việc liệu đây có phải là một ý tưởng hay không, tôi đang tìm kiếm làm thế nào nó có thể được thực hiện.


1
@Kev - Không, đó là một dịch vụ RESTful được triển khai bằng ASP.NET MVC
Greg Beech

Có thể sử dụng IIS7 và mô-đun gốc - msdn.microsoft.com/en-us/l Library / ms694280.aspx
Daniel Crenna

Bạn đã quản lý để thực hiện điều này? Chỉ tò mò, bạn đã áp dụng bất kỳ chiến lược đệm để viết vào db?
systempuntoout

1
Dự án thú vị ... nếu bạn kết thúc việc đó, với giải pháp cuối cùng?
PreguntonCojoneroCabrón

Câu trả lời:


91

Chắc chắn sử dụng một IHttpModulevà thực hiện BeginRequestEndRequestcác sự kiện.

Tất cả dữ liệu "thô" có mặt giữa HttpRequestHttpResponse, nó chỉ không ở một định dạng thô. Dưới đây là các phần cần thiết để xây dựng các bãi chứa kiểu Fiddler (gần với HTTP thô như nó có):

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

Đối với câu trả lời:

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

Lưu ý rằng bạn không thể đọc luồng phản hồi để bạn phải thêm bộ lọc vào luồng đầu ra và chụp một bản sao.

Trong của bạn BeginRequest, bạn sẽ cần thêm bộ lọc phản hồi:

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

Lưu trữ filternơi bạn có thể nhận được nó trong EndRequestxử lý. Tôi đề nghị trong HttpContext.Items. Sau đó có thể nhận được dữ liệu phản hồi đầy đủ trong filter.ReadStream().

Sau đó triển khai OutputFilterStreambằng cách sử dụng mẫu Trình trang trí làm trình bao quanh luồng:

/// <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
Câu trả lời tốt đẹp. Mặc dù vậy, một nhận xét: bạn đã nói "Sau đó có thể nhận được dữ liệu phản hồi đầy đủ trong bộ lọc.ToString ()." -Không có nghĩa là bộ lọc.ReadStream ()? (Tôi đang triển khai trong vb.net không phải c # nhưng nếu tôi chạy ToString, tôi chỉ lấy tên lớp dưới dạng chuỗi .ReadStream trả về nội dung phản hồi mong muốn.
Adam

Tôi đồng ý, trả lời tốt. Tôi đã sử dụng nó làm cơ sở cho một trình ghi nhật ký tùy chỉnh nhưng bây giờ đã gặp phải một vấn đề trong đó một số tiêu đề bị thiếu và quan trọng nhất là khi sử dụng nén IIS, tôi không thể truy cập phản hồi được nén cuối cùng. Tôi bắt đầu một câu hỏi mới liên quan ( stackoverflow.com/questions/11084459/ săn ) cho việc này.
Chris

2
Tôi nghĩ mckamey là một thiên tài. Bạn có thể đi làm cho Microsoft để chúng tôi có được giải pháp thông minh thay vì cần có cách giải quyết xuất sắc không?
Bàn tính

4
Coi chừng yêu cầu đó.RawUrl có thể kích hoạt ngoại lệ xác thực yêu cầu. Trong 4.5 bạn có thể sử dụng request.Unvalidated.RawUrl để ngăn chặn điều này. Trong 4.0 tôi đã kết thúc bằng cách sử dụng một số phản chiếu để bắt chước Request.SaveAs
Freek

1
@mckamey Tôi cố gắng triển khai giải pháp của bạn trong global.asax với Application_BeginRequest và Application_EndRequest nhưng tôi không chắc chắn nên viết mã nào trong EndRequest, bạn có thể cung cấp một ví dụ trong câu trả lời của mình không?
Jerome2606

48

Phương thức mở rộng sau đây trên HttpRequest sẽ tạo ra một chuỗi có thể được dán vào fiddler và phát lại.

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
Mã rất tốt! Nhưng để làm việc với MVC 4, tôi đã phải thay đổi tên lớp thành HttpRequestBaseExtensionsvà thay đổi HttpRequestthành HttpRequestBaseở mọi nơi.
Dmitry

35

Bạn có thể sử dụng biến máy chủ ALL_RAW để nhận các tiêu đề HTTP gốc được gửi cùng với yêu cầu, sau đó bạn có thể nhận InputStream như bình thường:

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

kiểm tra: http://msdn.microsoft.com/en-us/l Library / ms524602% 28VS.90% 29.aspx


Điều này làm việc cho tôi quá. Thậm chí không cần phải ở trong một xử lý. Tôi vẫn có thể truy cập nó từ trang.
Helephant

3
Hoặc trong ngữ cảnh máy chủ ASP.NET, sử dụng: this.Request.ServerVariabled ["ALL_RAW"];
Peter Stegnar

Tôi không thể nhận nội dung yêu cầu từ Request.InputStream, nó trả về "" cho tôi mọi lúc, tuy nhiên ALL_RAW hoạt động rất tốt để trả về các tiêu đề yêu cầu nên câu trả lời này đúng một nửa.
Justin

1
Bạn cũng có thể sử dụng HttpContext.Current.Requestđể lấy bối cảnh hiện tại bên ngoài các bộ điều khiển MVC, các trang ASPX, v.v ... chỉ cần đảm bảo rằng nó không phải là null trước tiên;)
jocull

16

Chà, tôi đang làm việc trên một dự án và đã làm, có thể không quá sâu, một bản ghi sử dụng thông số yêu cầu:

Hãy xem:

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

Bạn có thể trang trí lớp trình điều khiển của mình để đăng nhập hoàn toàn:

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

hoặc đăng nhập chỉ một số phương thức hành động cá nhân

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

12

Bất kỳ lý do bạn cần phải giữ nó trong mã được quản lý?

Điều đáng nói là bạn có thể kích hoạt tính năng ghi nhật ký thất bại trong IIS7 nếu bạn không muốn phát minh lại bánh xe. Điều này ghi nhật ký tiêu đề, cơ quan yêu cầu và phản hồi cũng như nhiều thứ khác.

Ghi nhật ký không thành công


Nếu đó không phải là một thất bại thì sao?
Sinaesthetic

7
Bạn cũng có thể sử dụng ghi nhật ký theo dõi thất bại với HTTP 200 OK, do đó, các lỗi không bị lỗi vẫn có thể được ghi lại
JoelBellot

2
Đây là giải pháp đơn giản nhất.
Kehlan Krumme

Để xem toàn bộ stack trace, nó là cần thiết để thêm GlobalConfiguration.Configuration.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;vào cuối Register(..)năm WebApiConfig.cs, nhưng điều này có thể thay đổi giữa các phiên bản.
Evgeni Sergeev

8

Tôi đã đi với phương pháp của McKAMEY. Đây là một mô-đun tôi đã viết sẽ giúp bạn bắt đầu và hy vọng giúp bạn tiết kiệm thời gian. Bạn sẽ cần phải cắm Logger rõ ràng với thứ gì đó phù hợp với bạn:

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
Bạn không thể truyền ứng dụng một cách an toàn.Response.Filter cho bất kỳ thứ gì ngoài Luồng. Các httpModules khác có thể bao bọc bộ lọc phản hồi của riêng bạn và trong trường hợp này, bạn sẽ nhận được một ngoại lệ truyền không hợp lệ.
Micah Zoltu

Không nên Encoding.UTF8, hoặc có thể Encoding.Defaultkhi đọc luồng yêu cầu? Hoặc chỉ cần sử dụng một StreamReader( với sự cẩn thận )
drzaus 16/12/15

5

OK, do đó, có vẻ như câu trả lời là "không bạn không thể lấy dữ liệu thô, bạn phải xây dựng lại yêu cầu / phản hồi từ các thuộc tính của các đối tượng được phân tích cú pháp". Oh tốt, tôi đã làm điều tái thiết.


3
Bạn có thấy bình luận của Vineus về ServerVariabled ["ALL_RAW"] không? Tôi chưa thử nó lần nào, nhưng nó được ghi lại để trả về thông tin tiêu đề thô chính xác như được gửi bởi khách hàng. Ngay cả khi tài liệu hóa ra là sai và nó đang được xây dựng lại, hey, tái thiết miễn phí :-)
Jonathan Gilbert

3

sử dụng 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
Theo Alex và kinh nghiệm của riêng tôi, tôi không nghĩ bạn có thể đọc từ HttpResponse.OutputStream, vì vậy phương pháp đăng nhập của bạn trong phương thức ProcessResponse có thể sẽ không hiệu quả.
William Gross

2
William nói đúng. Không thể đọc được httpResponse.OutputStream. Tôi tìm thấy một giải pháp đó là sử dụng HttpResponse.Filter và thay thế luồng đầu ra mặc định bằng luồng của riêng bạn.
Eric Fan


3

Nếu thỉnh thoảng sử dụng, để đi vòng quanh một góc hẹp, làm thế nào về một cái gì đó thô thiển như dưới đây?

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

Bạn có thể thực hiện việc này trong một DelegatingHandlermà không cần sử dụng OutputFiltercác câu trả lời được đề cập trong .NET 4.5 bằng cách sử dụng Stream.CopyToAsync()chức năng.

Tôi không chắc chắn về các chi tiết, nhưng nó không kích hoạt tất cả những điều tồi tệ xảy ra khi bạn cố đọc trực tiếp luồng phản hồi.

Thí dụ:

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

Tôi biết đó không phải là mã được quản lý, nhưng tôi sẽ đề xuất bộ lọc ISAPI. Đã một vài năm kể từ khi tôi có "niềm vui" duy trì ISAPI của riêng mình nhưng từ những gì tôi nhớ bạn có thể có quyền truy cập vào tất cả những thứ này, cả trước và sau khi ASP.Net đã làm điều đó.

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

Nếu HTTPModule không đủ tốt cho những gì bạn cần, thì tôi không nghĩ có bất kỳ cách quản lý nào để thực hiện việc này với số lượng chi tiết cần thiết. Nó sẽ là một nỗi đau để làm mặc dù.



0

Nó có thể là tốt nhất để làm điều này bên ngoài ứng dụng của bạn. Bạn có thể thiết lập một proxy ngược để làm những việc như thế này (và nhiều hơn nữa). Một proxy ngược về cơ bản là một máy chủ web nằm trong phòng máy chủ của bạn và đứng giữa (các) máy chủ web của bạn và máy khách. Xem http://en.wikipedia.org/wiki/Reverse_proxy


0

Đồng ý với FigmentEngine, IHttpModule dường như là con đường để đi.

Nhìn vào httpworkerrequest, readentitybodyGetPreloadedEntityBody .

Để có được httpworkerrequestbạn cần phải làm điều này:

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

nơi inApplà đối tượng HttpApplication.


1
Tôi đã nói rằng câu trả lời không phù hợp vì nó không nắm bắt được hầu hết các thông tin tôi yêu cầu. Làm thế nào là câu trả lời này trong bất kỳ cách nào hữu ích?
Greg Beech

Giải thích thêm, câu trả lời này theo cách nào hữu ích?
PreguntonCojoneroCabrón

0

HttpRequestHttpResponseMVC trước đây được sử dụng để có một GetInputStream()GetOutputStream()có thể được sử dụng cho mục đích đó. Không nhìn vào phần đó trong MVC vì vậy tôi không chắc chúng có sẵn nhưng có thể là một ý tưởng :)

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.