/// <summary> /// Handler for every requested InspectionStream object's close callback. /// </summary> /// <param name="stream"> /// The originating stream. /// </param> /// <param name="buffer"> /// The data passed through the stream. Empty in this case. /// </param> /// <param name="dropConnection"> /// Whether or not to immediately terminate the stream. Ignored in this case. /// </param> private void OnWrappedStreamClose(InspectionStream stream, Memory <byte> buffer, out bool dropConnection) { dropConnection = false; _configuration.HttpMessageStreamedInspectionHandler?.Invoke(stream.MessageInfo, StreamOperation.Close, buffer, out dropConnection); }
/// <summary> /// Invoked when this handler is determined to be the best suited to handle the supplied connection. /// </summary> /// <param name="context"> /// The HTTP context. /// </param> /// <returns> /// The handling task. /// </returns> public override async Task Handle(HttpContext context) { HttpRequestMessage requestMsg = null; HttpClient upstreamClient = _client; try { // Use helper to get the full, proper URL for the request. var fullUrl = Microsoft.AspNetCore.Http.Extensions.UriHelper.GetDisplayUrl(context.Request); var fullUrl = Microsoft.AspNetCore.Http.Extensions.UriHelper.GetEncodedUrl(context.Request); // Next we need to try and parse the URL as a URI, because the web client requires // this for connecting upstream. if (!Uri.TryCreate(fullUrl, UriKind.RelativeOrAbsolute, out Uri reqUrl)) { LoggerProxy.Default.Error("Failed to parse HTTP URL."); return; } if (context.Connection.ClientCertificate != null) { // TODO - Handle client certificates. } bool requestHasZeroContentLength = false; foreach (var hdr in context.Request.Headers) { try { if (hdr.Key.Equals("Content-Length", StringComparison.OrdinalIgnoreCase) && hdr.Value.ToString().Equals("0")) { requestHasZeroContentLength = true; } } catch { } } // Match the HTTP version of the client on the upstream request. We don't want to // transparently pass around headers that are wrong for the client's HTTP version. Version upstreamReqVersionMatch = null; Match match = s_httpVerRegex.Match(context.Request.Protocol); if (match != null && match.Success) { upstreamReqVersionMatch = Version.Parse(match.Value); } // Let's do our first call to message begin for the request side. var requestMessageNfo = new HttpMessageInfo { Url = reqUrl, Method = new HttpMethod(context.Request.Method), IsEncrypted = context.Request.IsHttps, BodyContentType = context?.Request?.ContentType ?? string.Empty, Headers = context.Request.Headers.ToNameValueCollection(), HttpVersion = upstreamReqVersionMatch ?? new Version(1, 0), MessageProtocol = MessageProtocol.Http, MessageType = MessageType.Request, RemoteAddress = context.Connection.RemoteIpAddress, RemotePort = (ushort)context.Connection.RemotePort, LocalAddress = context.Connection.LocalIpAddress, LocalPort = (ushort)context.Connection.LocalPort }; _configuration.NewHttpMessageHandler?.Invoke(requestMessageNfo); if (requestMessageNfo.ProxyNextAction == ProxyNextAction.DropConnection) { // Apply whatever the user did here and then quit. context.Response.ClearAllHeaders(); await context.Response.ApplyMessageInfo(requestMessageNfo, context.RequestAborted); return; } if (requestMessageNfo.ProxyNextAction == ProxyNextAction.AllowButDelegateHandler) { // Apply whatever the user did here and then quit. await _configuration.HttpExternalRequestHandlerCallback?.Invoke(requestMessageNfo, context); return; } // Check to see if the user has set their own client to fulfill the request with. upstreamClient = requestMessageNfo.FulfillmentClient ?? _client; // Create the message AFTER we give the user a chance to alter things. requestMsg = new HttpRequestMessage(requestMessageNfo.Method, requestMessageNfo.Url); var initialFailedHeaders = requestMsg.PopulateHeaders(requestMessageNfo.Headers, requestMessageNfo.ExemptedHeaders); // Ensure that we match the protocol of the client! if (upstreamReqVersionMatch != null) { requestMsg.Version = upstreamReqVersionMatch; } // Check if we have a request body. if (context.Request.Body != null) { // Now check what the user wanted to do. switch (requestMessageNfo.ProxyNextAction) { // We have a body and the user previously instructed us to give them the // content, if any, for inspection. case ProxyNextAction.AllowButRequestContentInspection: { // Get the request body into memory. using (var ms = new MemoryStream()) { await Microsoft.AspNetCore.Http.Extensions.StreamCopyOperation.CopyToAsync(context.Request.Body, ms, s_maxInMemoryData, context.RequestAborted); var requestBody = ms.ToArray(); // If we don't have a body, there's no sense in calling the // message end callback. if (requestBody.Length > 0) { // We'll now call the message end function for the request side. requestMessageNfo = new HttpMessageInfo { Url = reqUrl, // We recycle the very first unique message ID (auto // generated) in order to enable the library user to // track this single connection across multiple callbacks. MessageId = requestMessageNfo.MessageId, BodyContentType = context?.Request?.ContentType ?? string.Empty, Method = new HttpMethod(context.Request.Method), IsEncrypted = context.Request.IsHttps, Headers = context.Request.Headers.ToNameValueCollection(), HttpVersion = upstreamReqVersionMatch ?? new Version(1, 0), MessageProtocol = MessageProtocol.Http, MessageType = MessageType.Request, RemoteAddress = context.Connection.RemoteIpAddress, RemotePort = (ushort)context.Connection.RemotePort, LocalAddress = context.Connection.LocalIpAddress, LocalPort = (ushort)context.Connection.LocalPort, BodyInternal = requestBody }; _configuration.HttpMessageWholeBodyInspectionHandler?.Invoke(requestMessageNfo); if (requestMessageNfo.ProxyNextAction == ProxyNextAction.DropConnection) { // User wants to block this request after inspecting the // content. Apply whatever the user did here and then quit. context.Response.ClearAllHeaders(); await context.Response.ApplyMessageInfo(requestMessageNfo, context.RequestAborted); return; } // Check to see if the user has set their own client to fulfill the request with. upstreamClient = requestMessageNfo.FulfillmentClient ?? _client; // Since the user may have modified things, we'll now // re-create the request no matter what. requestMsg = new HttpRequestMessage(requestMessageNfo.Method, requestMessageNfo.Url); initialFailedHeaders = requestMsg.PopulateHeaders(requestMessageNfo.Headers, requestMessageNfo.ExemptedHeaders); // Set our content, even if it's empty. Don't worry about // ByteArrayContent and friends setting other headers, we're // gonna blow relevant headers away below and then set them properly. requestMsg.Content = new ByteArrayContent(requestBody); requestMsg.Content.Headers.Clear(); requestMsg.Content.Headers.TryAddWithoutValidation("Content-Length", requestBody.Length.ToString()); } else { if (requestHasZeroContentLength) { requestMsg.Content = new ByteArrayContent(requestBody); requestMsg.Content.Headers.Clear(); requestMsg.Content.Headers.TryAddWithoutValidation("Content-Length", "0"); } } } } break; case ProxyNextAction.AllowButRequestStreamedContentInspection: { requestMessageNfo = new HttpMessageInfo { Url = reqUrl, MessageId = requestMessageNfo.MessageId, BodyContentType = context?.Request?.ContentType ?? string.Empty, Method = new HttpMethod(context.Request.Method), IsEncrypted = context.Request.IsHttps, Headers = context.Request.Headers.ToNameValueCollection(), HttpVersion = upstreamReqVersionMatch ?? new Version(1, 0), MessageProtocol = MessageProtocol.Http, MessageType = MessageType.Request, RemoteAddress = context.Connection.RemoteIpAddress, RemotePort = (ushort)context.Connection.RemotePort, LocalAddress = context.Connection.LocalIpAddress, LocalPort = (ushort)context.Connection.LocalPort }; // We have a body and the user wants to just stream-inspect it. var wrappedStream = new InspectionStream(requestMessageNfo, context.Request.Body) { StreamRead = OnWrappedStreamRead, StreamWrite = OnWrappedStreamWrite, StreamClosed = OnWrappedStreamClose }; requestMsg.Content = new StreamContent(wrappedStream); } break; default: { if (context.Request.Body != null && (context.Request.Headers.ContainsKey("Transfer-Encoding") || (context.Request.ContentLength.HasValue && context.Request.ContentLength.Value > 0))) { // We have a body, but the user doesn't want to inspect it. So, // we'll just set our content to wrap the context's input stream. requestMsg.Content = new StreamContent(context.Request.Body); } } break; } } // Ensure that content type is set properly because ByteArrayContent and friends will // modify these fields. To explain these further, these headers almost always fail // because they apply to the .Content property only (content-specific headers), so // once we have a .Content property created, we'll go ahead and pour over the failed // headers and try to apply to them to the content. initialFailedHeaders = requestMsg.PopulateHeaders(initialFailedHeaders, requestMessageNfo.ExemptedHeaders); #if VERBOSE_WARNINGS foreach (string key in initialFailedHeaders) { LoggerProxy.Default.Warn(string.Format("Failed to add HTTP header with key {0} and with value {1}.", key, initialFailedHeaders[key])); } #endif // Lets start sending the request upstream. We're going to ask the client to return // control to us when the headers are complete. This way we're not buffering entire // responses into memory, and if the user doesn't request to inspect the content, we // can just async stream the content transparently and Kestrel is so cool and sweet // and nice, it'll automatically stream as chunked content. HttpResponseMessage response = null; try { try { response = await upstreamClient.SendAsync(requestMsg, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted); } catch (Exception e) { LoggerProxy.Default.Error(e); } if (response == null) { return; } // Blow away all response headers. We wanna clone these now from our upstream request. context.Response.ClearAllHeaders(); // Ensure our client's response status code is set to match ours. context.Response.StatusCode = (int)response.StatusCode; var responseHeaders = response.ExportAllHeaders(); bool responseHasZeroContentLength = false; bool responseIsFixedLength = false; foreach (var kvp in responseHeaders.ToIHeaderDictionary()) { foreach (var value in kvp.Value) { if (kvp.Key.Equals("Content-Length", StringComparison.OrdinalIgnoreCase)) { responseIsFixedLength = true; if (value.Length <= 0 && value.Equals("0")) { responseHasZeroContentLength = true; } } } } // For later reference... bool upstreamIsHttp1 = upstreamReqVersionMatch != null && upstreamReqVersionMatch.Major == 1 && upstreamReqVersionMatch.Minor == 0; // Let's call the message begin handler for the response. Unless of course, the // user has asked us NOT to do this. if (requestMessageNfo.ProxyNextAction != ProxyNextAction.AllowAndIgnoreContentAndResponse) { var responseMessageNfo = new HttpMessageInfo { Url = reqUrl, OriginatingMessage = requestMessageNfo, MessageId = requestMessageNfo.MessageId, BodyContentType = response?.Content?.Headers?.ContentType?.ToString() ?? string.Empty, IsEncrypted = context.Request.IsHttps, Headers = response.ExportAllHeaders(), MessageProtocol = MessageProtocol.Http, HttpVersion = upstreamReqVersionMatch ?? new Version(1, 0), StatusCode = response.StatusCode, MessageType = MessageType.Response, RemoteAddress = context.Connection.RemoteIpAddress, RemotePort = (ushort)context.Connection.RemotePort, LocalAddress = context.Connection.LocalIpAddress, LocalPort = (ushort)context.Connection.LocalPort }; _configuration.NewHttpMessageHandler?.Invoke(responseMessageNfo); if (responseMessageNfo.ProxyNextAction == ProxyNextAction.DropConnection) { // Apply whatever the user did here and then quit. context.Response.ClearAllHeaders(); await context.Response.ApplyMessageInfo(responseMessageNfo, context.RequestAborted); return; } if (responseMessageNfo.ProxyNextAction == ProxyNextAction.AllowButDelegateHandler) { // Apply whatever the user did here and then quit. await _configuration.HttpExternalRequestHandlerCallback.Invoke(responseMessageNfo, context); return; } context.Response.ClearAllHeaders(); context.Response.PopulateHeaders(responseMessageNfo.Headers, responseMessageNfo.ExemptedHeaders); context.Response.StatusCode = (int)response.StatusCode; switch (responseMessageNfo.ProxyNextAction) { case ProxyNextAction.AllowButRequestContentInspection: { using (var upstreamResponseStream = await response?.Content.ReadAsStreamAsync()) { using (var ms = new MemoryStream()) { await Microsoft.AspNetCore.Http.Extensions.StreamCopyOperation.CopyToAsync(upstreamResponseStream, ms, s_maxInMemoryData, context.RequestAborted); var responseBody = ms.ToArray(); responseMessageNfo = new HttpMessageInfo { Url = reqUrl, OriginatingMessage = requestMessageNfo, MessageId = requestMessageNfo.MessageId, BodyContentType = response?.Content?.Headers?.ContentType?.ToString() ?? string.Empty, IsEncrypted = context.Request.IsHttps, Headers = response.ExportAllHeaders(), MessageProtocol = MessageProtocol.Http, HttpVersion = upstreamReqVersionMatch ?? new Version(1, 0), StatusCode = response.StatusCode, MessageType = MessageType.Response, RemoteAddress = context.Connection.RemoteIpAddress, RemotePort = (ushort)context.Connection.RemotePort, LocalAddress = context.Connection.LocalIpAddress, LocalPort = (ushort)context.Connection.LocalPort, BodyInternal = responseBody }; _configuration.HttpMessageWholeBodyInspectionHandler?.Invoke(responseMessageNfo); if (responseMessageNfo.ProxyNextAction == ProxyNextAction.DropConnection) { // Apply whatever the user did here and then quit. context.Response.ClearAllHeaders(); await context.Response.ApplyMessageInfo(responseMessageNfo, context.RequestAborted); return; } context.Response.ClearAllHeaders(); context.Response.PopulateHeaders(responseMessageNfo.Headers, responseMessageNfo.ExemptedHeaders); // User inspected but allowed the content. Just write to // the response body and then move on with your life fam. // // However, don't try to write a body if it's zero // length. Also, do not try to write a body, even if // present, if the status is 204. Kestrel will not let us // do this, and so far I can't find a way to remove this // technically correct strict-compliance. if (!responseHasZeroContentLength && (responseBody.Length > 0 && context.Response.StatusCode != 204)) { // If the request is HTTP1.0, we need to pull all the // data so we can properly set the content-length by // adding the header in. if (upstreamIsHttp1) { context.Response.Headers.Add("Content-Length", responseBody.Length.ToString()); } await context.Response.Body.WriteAsync(responseBody, 0, responseBody.Length); } else { if (responseHasZeroContentLength) { context.Response.Headers.Add("Content-Length", "0"); } } // Ensure we exit here, because if we fall past this // scope then the response is going to get mangled. return; } } } case ProxyNextAction.AllowButRequestStreamedContentInspection: { responseMessageNfo = new HttpMessageInfo { Url = reqUrl, OriginatingMessage = requestMessageNfo, MessageId = requestMessageNfo.MessageId, BodyContentType = response?.Content?.Headers?.ContentType?.ToString() ?? string.Empty, IsEncrypted = context.Request.IsHttps, Headers = response.ExportAllHeaders(), MessageProtocol = MessageProtocol.Http, StatusCode = response.StatusCode, HttpVersion = upstreamReqVersionMatch ?? new Version(1, 0), MessageType = MessageType.Response, RemoteAddress = context.Connection.RemoteIpAddress, RemotePort = (ushort)context.Connection.RemotePort, LocalAddress = context.Connection.LocalIpAddress, LocalPort = (ushort)context.Connection.LocalPort, BodyInternal = new Memory <byte>() }; using (var responseStream = await response?.Content.ReadAsStreamAsync()) { // We have a body and the user wants to just stream-inspect it. using (var wrappedStream = new InspectionStream(responseMessageNfo, responseStream)) { wrappedStream.StreamRead = OnWrappedStreamRead; wrappedStream.StreamWrite = OnWrappedStreamWrite; wrappedStream.StreamClosed = OnWrappedStreamClose; await Microsoft.AspNetCore.Http.Extensions.StreamCopyOperation.CopyToAsync(wrappedStream, context.Response.Body, null, context.RequestAborted); } } return; } case ProxyNextAction.AllowButRequestResponseReplay: { responseMessageNfo = new HttpMessageInfo { Url = reqUrl, OriginatingMessage = requestMessageNfo, MessageId = requestMessageNfo.MessageId, BodyContentType = response?.Content?.Headers?.ContentType?.ToString() ?? string.Empty, IsEncrypted = context.Request.IsHttps, Headers = response.ExportAllHeaders(), MessageProtocol = MessageProtocol.Http, StatusCode = response.StatusCode, HttpVersion = upstreamReqVersionMatch ?? new Version(1, 0), MessageType = MessageType.Response, RemoteAddress = context.Connection.RemoteIpAddress, RemotePort = (ushort)context.Connection.RemotePort, LocalAddress = context.Connection.LocalIpAddress, LocalPort = (ushort)context.Connection.LocalPort }; using (var responseStream = await response?.Content.ReadAsStreamAsync()) { var replay = _replayFactory.CreateReplay(responseMessageNfo, context.RequestAborted); // Lambda for handling this specific replay object. HttpReplayTerminationCallback cancellationFunction = (bool closeSourceStream) => { replay.ReplayAborted = true; if (closeSourceStream) { // Close down the source stream if requested. context.Abort(); } }; // We have a body and the user wants to just stream-inspect it. using (var wrappedStream = new InspectionStream(responseMessageNfo, responseStream)) { wrappedStream.StreamRead = (InspectionStream stream, Memory <byte> buffer, out bool dropConnection) => { dropConnection = false; if (buffer.Length > 0) { replay.WriteBodyBytes(buffer); } }; wrappedStream.StreamClosed = (InspectionStream stream, Memory <byte> buffer, out bool dropConnection) => { dropConnection = false; replay.BodyComplete = true; }; _configuration.HttpMessageReplayInspectionCallback?.Invoke(replay.ReplayUrl, cancellationFunction); await Microsoft.AspNetCore.Http.Extensions.StreamCopyOperation.CopyToAsync(wrappedStream, context.Response.Body, null, context.RequestAborted); } } return; } } } // if (requestMessageNfo.ProxyNextAction != ProxyNextAction.AllowAndIgnoreContentAndResponse) // If we made it here, then the user just wants to let the response be streamed // in without any inspection etc, so do exactly that. using (var responseStream = await response?.Content.ReadAsStreamAsync()) { context.Response.StatusCode = (int)response.StatusCode; context.Response.PopulateHeaders(response.ExportAllHeaders(), s_emptyExemptedHeaders); if (!responseHasZeroContentLength && (upstreamIsHttp1 || responseIsFixedLength)) { using (var ms = new MemoryStream()) { await Microsoft.AspNetCore.Http.Extensions.StreamCopyOperation.CopyToAsync(responseStream, ms, s_maxInMemoryData, context.RequestAborted); var responseBody = ms.ToArray(); context.Response.Headers.Remove("Content-Length"); context.Response.Headers.Add("Content-Length", responseBody.Length.ToString()); await context.Response.Body.WriteAsync(responseBody, 0, responseBody.Length); } } else { context.Response.Headers.Remove("Content-Length"); if (responseHasZeroContentLength) { context.Response.Headers.Add("Content-Length", "0"); } else { await Microsoft.AspNetCore.Http.Extensions.StreamCopyOperation.CopyToAsync(responseStream, context.Response.Body, null, context.RequestAborted); } } } } finally { if (response != null) { // Blow away the managed response before we leave, always! try { response.Dispose(); } catch { } } } } catch (Exception e) { if (!(e is TaskCanceledException) && !(e is OperationCanceledException)) { // Ignore task cancelled exceptions. LoggerProxy.Default.Error(e); } } finally { if (requestMsg != null) { // Blow away the managed response before we leave, always! try { requestMsg.Dispose(); } catch { } } } }
/// <summary> /// Handler for every requested InspectionStream object's write callback. /// </summary> /// <param name="stream"> /// The originating stream. /// </param> /// <param name="buffer"> /// The data passed through the stream. /// </param> /// <param name="dropConnection"> /// Whether or not to immediately terminate the stream. /// </param> private void OnWrappedStreamWrite(InspectionStream stream, Memory <byte> buffer, out bool dropConnection) { dropConnection = false; _streamInpsectionCb?.Invoke(stream.MessageInfo, StreamOperation.Write, buffer, out dropConnection); }
public SslStreamExt(Stream innerStream, bool leaveInnerStreamOpen, RemoteCertificateValidationCallback userCertificateValidationCallback, LocalCertificateSelectionCallback userCertificateSelectionCallback, EncryptionPolicy encryptionPolicy) : base(new InspectionStream(innerStream, leaveInnerStreamOpen), leaveInnerStreamOpen, userCertificateValidationCallback, userCertificateSelectionCallback, encryptionPolicy) { _inspection = (InspectionStream)InnerStream; }
/// <summary> /// Invoked when this handler is determined to be the best suited to handle the supplied connection. /// </summary> /// <param name="context"> /// The HTTP context. /// </param> /// <returns> /// The handling task. /// </returns> public override async Task Handle(HttpContext context) { Diagnostics.DiagnosticsWebSession diagSession = new Diagnostics.DiagnosticsWebSession(); if (Diagnostics.Collector.IsDiagnosticsEnabled) { diagSession.DateStarted = DateTime.Now; } try { // Use helper to get the full, proper URL for the request. //var fullUrl = Microsoft.AspNetCore.Http.Extensions.UriHelper.GetDisplayUrl(context.Request); var fullUrl = Microsoft.AspNetCore.Http.Extensions.UriHelper.GetEncodedUrl(context.Request); // Next we need to try and parse the URL as a URI, because the websocket client // requires this for connecting upstream. if (!Uri.TryCreate(fullUrl, UriKind.RelativeOrAbsolute, out Uri reqUrl)) { LoggerProxy.Default.Error("Failed to parse HTTP URL."); return; } if (context.Connection.ClientCertificate != null) { // TODO - Handle client certificates. } bool requestHasZeroContentLength = false; foreach (var hdr in context.Request.Headers) { try { if (hdr.Key.Equals("Content-Length", StringComparison.OrdinalIgnoreCase) && hdr.Value.ToString().Equals("0")) { requestHasZeroContentLength = true; } } catch { } } HttpRequestMessage requestMsg; diagSession.ClientRequestUri = fullUrl; // Let's do our first call to message begin for the request side. var requestMessageNfo = new HttpMessageInfo { Url = reqUrl, Method = new HttpMethod(context.Request.Method), IsEncrypted = context.Request.IsHttps, Headers = context.Request.Headers.ToNameValueCollection(), MessageProtocol = MessageProtocol.Http, MessageType = MessageType.Request, RemoteAddress = context.Connection.RemoteIpAddress, RemotePort = (ushort)context.Connection.RemotePort, LocalAddress = context.Connection.LocalIpAddress, LocalPort = (ushort)context.Connection.LocalPort }; _newMessageCb?.Invoke(requestMessageNfo); if (Diagnostics.Collector.IsDiagnosticsEnabled) { diagSession.ClientRequestHeaders = context.Request.Headers.ToString(); } if (requestMessageNfo.ProxyNextAction == ProxyNextAction.DropConnection) { // Apply whatever the user did here and then quit. context.Response.ClearAllHeaders(); await context.Response.ApplyMessageInfo(requestMessageNfo, context.RequestAborted); return; } // Create the message AFTER we give the user a chance to alter things. requestMsg = new HttpRequestMessage(requestMessageNfo.Method, requestMessageNfo.Url); var initialFailedHeaders = requestMsg.PopulateHeaders(requestMessageNfo.Headers); // Check if we have a request body. if (context.Request.Body != null) { // Now check what the user wanted to do. switch (requestMessageNfo.ProxyNextAction) { // We have a body and the user previously instructed us to give them the // content, if any, for inspection. case ProxyNextAction.AllowButRequestContentInspection: { // Get the request body into memory. using (var ms = new MemoryStream()) { await Microsoft.AspNetCore.Http.Extensions.StreamCopyOperation.CopyToAsync(context.Request.Body, ms, s_maxInMemoryData, context.RequestAborted); var requestBody = ms.ToArray(); // If we don't have a body, there's no sense in calling the message end callback. if (requestBody.Length > 0) { diagSession.ClientRequestBody = requestBody; // We'll now call the message end function for the request side. requestMessageNfo = new HttpMessageInfo { Url = reqUrl, Method = new HttpMethod(context.Request.Method), IsEncrypted = context.Request.IsHttps, Headers = context.Request.Headers.ToNameValueCollection(), MessageProtocol = MessageProtocol.Http, MessageType = MessageType.Request, RemoteAddress = context.Connection.RemoteIpAddress, RemotePort = (ushort)context.Connection.RemotePort, LocalAddress = context.Connection.LocalIpAddress, LocalPort = (ushort)context.Connection.LocalPort, BodyInternal = requestBody }; _wholeBodyInspectionCb?.Invoke(requestMessageNfo); if (requestMessageNfo.ProxyNextAction == ProxyNextAction.DropConnection) { // User wants to block this request after inspecting the content. // Apply whatever the user did here and then quit. context.Response.ClearAllHeaders(); await context.Response.ApplyMessageInfo(requestMessageNfo, context.RequestAborted); return; } // Since the user may have modified things, we'll now re-create // the request no matter what. requestMsg = new HttpRequestMessage(requestMessageNfo.Method, requestMessageNfo.Url); initialFailedHeaders = requestMsg.PopulateHeaders(requestMessageNfo.Headers); // Set our content, even if it's empty. Don't worry about ByteArrayContent // and friends setting other headers, we're gonna blow relevant headers away // below and then set them properly. requestMsg.Content = new ByteArrayContent(requestBody); requestMsg.Content.Headers.Clear(); requestMsg.Content.Headers.TryAddWithoutValidation("Content-Length", requestBody.Length.ToString()); } else { if (requestHasZeroContentLength) { requestMsg.Content = new ByteArrayContent(requestBody); requestMsg.Content.Headers.Clear(); requestMsg.Content.Headers.TryAddWithoutValidation("Content-Length", "0"); } } } } break; case ProxyNextAction.AllowButRequestStreamedContentInspection: { requestMessageNfo = new HttpMessageInfo { Url = reqUrl, Method = new HttpMethod(context.Request.Method), IsEncrypted = context.Request.IsHttps, Headers = context.Request.Headers.ToNameValueCollection(), MessageProtocol = MessageProtocol.Http, MessageType = MessageType.Request, RemoteAddress = context.Connection.RemoteIpAddress, RemotePort = (ushort)context.Connection.RemotePort, LocalAddress = context.Connection.LocalIpAddress, LocalPort = (ushort)context.Connection.LocalPort }; // We have a body and the user wants to just stream-inspect it. var wrappedStream = new InspectionStream(requestMessageNfo, context.Request.Body) { StreamRead = OnWrappedStreamRead, StreamWrite = OnWrappedStreamWrite }; requestMsg.Content = new StreamContent(wrappedStream); } break; default: { if (context.Request.ContentLength.HasValue && context.Request.ContentLength.Value > 0) { // We have a body, but the user doesn't want to inspect it. // So, we'll just set our content to wrap the context's input // stream. requestMsg.Content = new StreamContent(context.Request.Body); } } break; } } // Ensure that content type is set properly because ByteArrayContent and friends will // modify these fields. // To explain these further, these headers almost always fail because // they apply to the .Content property only (content-specific headers), // so once we have a .Content property created, we'll go ahead and // pour over the failed headers and try to apply to them to the content. initialFailedHeaders = requestMsg.PopulateHeaders(initialFailedHeaders); #if VERBOSE_WARNINGS foreach (string key in initialFailedHeaders) { LoggerProxy.Default.Warn(string.Format("Failed to add HTTP header with key {0} and with value {1}.", key, initialFailedHeaders[key])); } #endif if (Diagnostics.Collector.IsDiagnosticsEnabled) { diagSession.ServerRequestHeaders = requestMsg.Headers.ToString(); } // Lets start sending the request upstream. We're going to ask the client to return // control to us when the headers are complete. This way we're not buffering entire // responses into memory, and if the user doesn't request to inspect the content, we // can just async stream the content transparently and Kestrel is so cool and sweet // and nice, it'll automatically stream as chunked content. HttpResponseMessage response = null; try { response = await s_client.SendAsync(requestMsg, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted); diagSession.StatusCode = (int)(response?.StatusCode ?? 0); } catch (HttpRequestException e) { LoggerProxy.Default.Error(e); if (e.InnerException is WebException && e.InnerException.InnerException is System.Security.Authentication.AuthenticationException) { requestMessageNfo = new HttpMessageInfo { Url = reqUrl, Method = new HttpMethod(context.Request.Method), IsEncrypted = context.Request.IsHttps, Headers = context.Request.Headers.ToNameValueCollection(), MessageProtocol = MessageProtocol.Http, MessageType = MessageType.Request, RemoteAddress = context.Connection.RemoteIpAddress, RemotePort = (ushort)context.Connection.RemotePort, LocalAddress = context.Connection.LocalIpAddress, LocalPort = (ushort)context.Connection.LocalPort, BodyInternal = null }; _badCertificateCb?.Invoke(requestMessageNfo); context.Response.ClearAllHeaders(); await context.Response.ApplyMessageInfo(requestMessageNfo, context.RequestAborted); return; } else if (e.InnerException is WebException) { var webException = e.InnerException as WebException; if (webException.Response != null) { diagSession.StatusCode = (int?)(webException.Response as HttpWebResponse)?.StatusCode ?? 0; } } } catch (Exception e) { LoggerProxy.Default.Error(e); } if (response == null) { return; } // Blow away all response headers. We wanna clone these now from our upstream request. context.Response.ClearAllHeaders(); // Ensure our client's response status code is set to match ours. context.Response.StatusCode = (int)response.StatusCode; var responseHeaders = response.ExportAllHeaders(); bool responseHasZeroContentLength = false; bool responseIsFixedLength = false; foreach (var kvp in responseHeaders.ToIHeaderDictionary()) { foreach (var value in kvp.Value) { if (kvp.Key.Equals("Content-Length", StringComparison.OrdinalIgnoreCase)) { responseIsFixedLength = true; if (value.Length <= 0 && value.Equals("0")) { responseHasZeroContentLength = true; } } } } if (Diagnostics.Collector.IsDiagnosticsEnabled) { diagSession.ServerResponseHeaders = responseHeaders.ToString(); } // Match the HTTP version of the client on the upstream request. We don't want to // transparently pass around headers that are wrong for the client's HTTP version. Version upstreamReqVersionMatch = null; Match match = s_httpVerRegex.Match(context.Request.Protocol); if (match != null && match.Success) { upstreamReqVersionMatch = Version.Parse(match.Value); requestMsg.Version = upstreamReqVersionMatch; } // For later reference... bool upstreamIsHttp1 = upstreamReqVersionMatch != null && upstreamReqVersionMatch.Major == 1 && upstreamReqVersionMatch.Minor == 0; // Let's call the message begin handler for the response. Unless of course, the user has asked us NOT to do this. if (requestMessageNfo.ProxyNextAction != ProxyNextAction.AllowAndIgnoreContentAndResponse) { var responseMessageNfo = new HttpMessageInfo { Url = reqUrl, IsEncrypted = context.Request.IsHttps, Headers = response.ExportAllHeaders(), MessageProtocol = MessageProtocol.Http, MessageType = MessageType.Response, RemoteAddress = context.Connection.RemoteIpAddress, RemotePort = (ushort)context.Connection.RemotePort, LocalAddress = context.Connection.LocalIpAddress, LocalPort = (ushort)context.Connection.LocalPort }; _newMessageCb?.Invoke(responseMessageNfo); if (responseMessageNfo.ProxyNextAction == ProxyNextAction.DropConnection) { // Apply whatever the user did here and then quit. context.Response.ClearAllHeaders(); await context.Response.ApplyMessageInfo(responseMessageNfo, context.RequestAborted); return; } context.Response.ClearAllHeaders(); context.Response.PopulateHeaders(responseMessageNfo.Headers); switch (responseMessageNfo.ProxyNextAction) { case ProxyNextAction.AllowButRequestContentInspection: { using (var upstreamResponseStream = await response.Content.ReadAsStreamAsync()) { using (var ms = new MemoryStream()) { await Microsoft.AspNetCore.Http.Extensions.StreamCopyOperation.CopyToAsync(upstreamResponseStream, ms, s_maxInMemoryData, context.RequestAborted); var responseBody = ms.ToArray(); diagSession.ServerResponseBody = responseBody; responseMessageNfo = new HttpMessageInfo { Url = reqUrl, IsEncrypted = context.Request.IsHttps, Headers = response.ExportAllHeaders(), MessageProtocol = MessageProtocol.Http, MessageType = MessageType.Response, RemoteAddress = context.Connection.RemoteIpAddress, RemotePort = (ushort)context.Connection.RemotePort, LocalAddress = context.Connection.LocalIpAddress, LocalPort = (ushort)context.Connection.LocalPort, BodyInternal = responseBody }; _wholeBodyInspectionCb?.Invoke(responseMessageNfo); if (responseMessageNfo.ProxyNextAction == ProxyNextAction.DropConnection) { // Apply whatever the user did here and then quit. context.Response.ClearAllHeaders(); await context.Response.ApplyMessageInfo(responseMessageNfo, context.RequestAborted); return; } context.Response.ClearAllHeaders(); context.Response.PopulateHeaders(responseMessageNfo.Headers); // User inspected but allowed the content. Just write to the response // body and then move on with your life fam. // // However, don't try to write a body if it's zero length. Also, do // not try to write a body, even if present, if the status is 204. // Kestrel will not let us do this, and so far I can't find a way to // remove this technically correct strict-compliance. if (!responseHasZeroContentLength && (responseBody.Length > 0 && context.Response.StatusCode != 204)) { // If the request is HTTP1.0, we need to pull all the data so we // can properly set the content-length by adding the header in. if (upstreamIsHttp1) { context.Response.Headers.Add("Content-Length", responseBody.Length.ToString()); } await context.Response.Body.WriteAsync(responseBody, 0, responseBody.Length); } else { if (responseHasZeroContentLength) { context.Response.Headers.Add("Content-Length", "0"); } } // Ensure we exit here, because if we fall past this scope then the // response is going to get mangled. return; } } } case ProxyNextAction.AllowButRequestStreamedContentInspection: { responseMessageNfo = new HttpMessageInfo { Url = reqUrl, IsEncrypted = context.Request.IsHttps, Headers = response.ExportAllHeaders(), MessageProtocol = MessageProtocol.Http, MessageType = MessageType.Response, RemoteAddress = context.Connection.RemoteIpAddress, RemotePort = (ushort)context.Connection.RemotePort, LocalAddress = context.Connection.LocalIpAddress, LocalPort = (ushort)context.Connection.LocalPort }; using (var responseStream = await response.Content.ReadAsStreamAsync()) { // We have a body and the user wants to just stream-inspect it. using (var wrappedStream = new InspectionStream(responseMessageNfo, responseStream)) { wrappedStream.StreamRead = OnWrappedStreamRead; wrappedStream.StreamWrite = OnWrappedStreamWrite; await Microsoft.AspNetCore.Http.Extensions.StreamCopyOperation.CopyToAsync(wrappedStream, context.Response.Body, null, context.RequestAborted); } } return; } } } // if (requestMessageNfo.ProxyNextAction != ProxyNextAction.AllowAndIgnoreContentAndResponse) // If we made it here, then the user just wants to let the response be streamed in // without any inspection etc, so do exactly that. using (var responseStream = await response.Content.ReadAsStreamAsync()) { context.Response.StatusCode = (int)response.StatusCode; context.Response.PopulateHeaders(response.ExportAllHeaders()); if (!responseHasZeroContentLength && (upstreamIsHttp1 || responseIsFixedLength)) { using (var ms = new MemoryStream()) { await Microsoft.AspNetCore.Http.Extensions.StreamCopyOperation.CopyToAsync(responseStream, ms, s_maxInMemoryData, context.RequestAborted); var responseBody = ms.ToArray(); context.Response.Headers.Remove("Content-Length"); context.Response.Headers.Add("Content-Length", responseBody.Length.ToString()); await context.Response.Body.WriteAsync(responseBody, 0, responseBody.Length); } } else { context.Response.Headers.Remove("Content-Length"); if (responseHasZeroContentLength) { context.Response.Headers.Add("Content-Length", "0"); } else { await Microsoft.AspNetCore.Http.Extensions.StreamCopyOperation.CopyToAsync(responseStream, context.Response.Body, null, context.RequestAborted); } } } } catch (Exception e) { if (!(e is TaskCanceledException) && !(e is OperationCanceledException)) { // Ignore task cancelled exceptions. LoggerProxy.Default.Error(e); } } finally { if (Diagnostics.Collector.IsDiagnosticsEnabled) { diagSession.DateEnded = DateTime.Now; Diagnostics.Collector.ReportSession(diagSession); } } }