/// <summary> /// Creates a new replay object and enqueues it for requesting. /// </summary> /// <param name="messageInfo"> /// The message info. /// </param> /// <param name="cancellationToken"> /// The source stream cancellation token. /// </param> /// <returns> /// A newly constructed relay object. /// </returns> public ResponseReplay CreateReplay(HttpMessageInfo messageInfo, CancellationToken cancellationToken) { var replay = new ResponseReplay(V4HttpEndpoint, messageInfo, cancellationToken); // Fun fact I didn't know - the indexer is atomic and thread safe. So much easier than // AddOrUpdate w/ func! _replays[messageInfo.MessageId] = replay; return(replay); }
/// <summary> /// Force all bing-destined requests to go to yahoo.com. /// </summary> /// <param name="messageInfo"> /// The message info. /// </param> private static bool RedirectBingToYahoo(HttpMessageInfo messageInfo) { if (messageInfo.MessageType == MessageType.Request && messageInfo.Url.Host.Contains("bing.")) { messageInfo.MakeTemporaryRedirect("https://www.yahoo.com"); messageInfo.ProxyNextAction = ProxyNextAction.DropConnection; return(true); } return(false); }
/// <summary> /// Checks whether the host is MSNBC.com and if so, we will tell the proxy to let us fulfill /// the request ourselves. /// </summary> /// <param name="messageInfo"> /// The message info. /// </param> /// <returns> /// True if we should fulfill the request ourselves, false otherwise. /// </returns> private static bool ManuallyFulfill(HttpMessageInfo messageInfo) { if (messageInfo.MessageType == MessageType.Request) { if (messageInfo.Url.Host.Equals("msnbc.com", StringComparison.OrdinalIgnoreCase)) { messageInfo.ProxyNextAction = ProxyNextAction.AllowButDelegateHandler; return(true); } } return(false); }
/// <summary> /// Called whenever a new request or response message is intercepted. /// </summary> /// <param name="messageInfo"> /// The message info. /// </param> /// <remarks> /// In this callback we can do all kinds of crazy things, including fully modify the HTTP /// headers, the request target, etc etc. /// </remarks> private static void OnNewMessage(HttpMessageInfo messageInfo) { ForceGoogleSafeSearch(messageInfo); if (RedirectBingToYahoo(messageInfo)) { return; } // Block only this casino website. if (messageInfo.Url.Host.Equals("777.com", StringComparison.OrdinalIgnoreCase)) { messageInfo.MessageType = MessageType.Response; messageInfo.ProxyNextAction = ProxyNextAction.DropConnection; messageInfo.BodyContentType = "text/html"; messageInfo.Body = s_blockPageBytes; return; } // By default, allow and ignore content, but not any responses to this content. messageInfo.ProxyNextAction = ProxyNextAction.AllowAndIgnoreContent; // If the new message is a response, we want to inspect the payload if it is HTML. if (messageInfo.MessageType == MessageType.Response) { foreach (string headerName in messageInfo.Headers) { if (messageInfo.Headers[headerName].IndexOf("html") != -1) { Console.WriteLine("Requesting to inspect HTML response for request {0}.", messageInfo.Url); messageInfo.ProxyNextAction = ProxyNextAction.AllowButRequestContentInspection; return; } } // The other kind of filtering we want to do here is to monitor video // streams. So, if we find a video content type in a response, we'll subscribe // the very new, and extremely exciting streaming inspection callback!!!!! var contentTypeKey = "Content-Type"; var contentType = messageInfo.Headers[contentTypeKey]; if (contentType != null && (contentType.IndexOf("video/", StringComparison.OrdinalIgnoreCase) != -1 || contentType.IndexOf("mpeg", StringComparison.OrdinalIgnoreCase) != -1)) { // Means we have a video response coming. // We want to get the video stream too! Because we have the tools to tell // if video is naughty or nice! Console.WriteLine("Requesting to inspect streamed video response."); messageInfo.ProxyNextAction = ProxyNextAction.AllowButRequestStreamedContentInspection; } } }
/// <summary> /// Constructs a new ResponseReplay instance with the given parameters. /// </summary> /// <param name="serverHttpEndpoint"> /// The HTTP endpoint that the replay server is bound to. /// </param> /// <param name="messageInfo"> /// The message info. /// </param> /// <param name="cancellationToken"> /// The cancellation token from the original request handled by <seealso cref="FilterHttpResponseHandler" />. /// </param> /// <exception cref="ArgumentException"> /// If the message info object is null, or is not a response, this constructor will throw. /// </exception> /// <exception cref="ArgumentException"> /// If the cancellation token is null, this constructor will throw. /// </exception> public ResponseReplay(IPEndPoint serverHttpEndpoint, HttpMessageInfo messageInfo, CancellationToken cancellationToken) { if (messageInfo == null || messageInfo.MessageType != MessageType.Response) { throw new ArgumentException("The information object must not be null and must indicate that it is a response.", nameof(messageInfo)); } if (cancellationToken == null) { throw new ArgumentException("The cancellation token object must not be null.", nameof(cancellationToken)); } _serverHttpEndpoint = serverHttpEndpoint ?? throw new ArgumentException("The server endpoint cannot be null.", nameof(cancellationToken)); MessageInfo = messageInfo; _cancellationToken = cancellationToken; }
/// <summary> /// Called whenever we've subscribed to monitor a payload in a streaming fashion. This is /// useful for say, virus scanning without forcing the entire payload to be buffered into /// memory before it is streamed to the user, or to monitor and decode video on the fly /// without affecting the user. You can terminate the stream at any time while monitoring. /// </summary> /// <param name="messageInfo"> /// The originating http message item. /// </param> /// <param name="operation"> /// The operation kind. /// </param> /// <param name="buffer"> /// The data that passed through the stream. /// </param> /// <param name="dropConnection"> /// Whether or not to immediately terminate the connection. /// </param> private static void OnStreamedContentInspection(HttpMessageInfo messageInfo, StreamOperation operation, Memory <byte> buffer, out bool dropConnection) { var toFrom = operation == StreamOperation.Read ? "from" : "to"; Console.WriteLine($"Stream {operation} {buffer.Length} bytes {toFrom} {messageInfo.Url}"); dropConnection = false; // Drop googlevideo.com videos. if (messageInfo.Url.Host.IndexOf(".googlevideo.com") > -1) { // This basically means you can't watch anything on youtube. You can still load the // site, but you can't play any videos. // This is just to demonstrate that it's possible to have complete // control over unbuffered streams. // dropConnection = true; } }
/// <summary> /// Called whenever we've requested to inspect an entire message payload. /// </summary> /// <param name="messageInfo"> /// The message info. /// </param> private static void OnWholeBodyContentInspection(HttpMessageInfo messageInfo) { if (messageInfo.Body.Length > 0) { // We assume it's HTML because HTML is the only type we request // to inspect, but you can double-check if you'd like. // We should check Content-Type for charset=XXXX. var htmlResponse = Encoding.UTF8.GetString(messageInfo.Body.ToArray()); // Any HTML that has 777.com in it, we want to block. if (htmlResponse.IndexOf("777.com") != -1) { Console.WriteLine("Request {0} blocked by content inspection.", messageInfo.Url); messageInfo.ProxyNextAction = ProxyNextAction.DropConnection; } } }
/// <summary> /// Applies the data set in the supplied HttpMessageInfo object to the actual HTTP object. /// </summary> /// <param name="message"> /// The HTTP object. /// </param> /// <param name="messageInfo"> /// The message info. /// </param> /// <param name="cancelToken"> /// The cancellation token. /// </param> /// <returns> /// A boolean value indicating whether or not the operation was a success. Exceptions are /// handled and a value of false is returned in the event of an exception. /// </returns> public static bool ApplyMessageInfo(this HttpRequestMessage message, HttpMessageInfo messageInfo, CancellationToken cancelToken) { try { if (messageInfo.MessageType == MessageType.Request) { var failedHeaders = message.PopulateHeaders(messageInfo.Headers, messageInfo.ExemptedHeaders); message.Method = messageInfo.Method; message.RequestUri = messageInfo.Url; if (messageInfo.BodyIsUserCreated && messageInfo.Body.Length > 0) { message.Content = new ByteArrayContent(messageInfo.Body.ToArray()); failedHeaders = message.PopulateHeaders(messageInfo.Headers, messageInfo.ExemptedHeaders); #if VERBOSE_WARNINGS foreach (string key in failedHeaders) { LoggerProxy.Default.Warn(string.Format("Failed to add HTTP header with key {0} and with value {1}.", key, failedHeaders[key])); } #endif message.Content.Headers.TryAddWithoutValidation("Expires", TimeUtil.UnixEpochString); if (messageInfo.BodyContentType != null) { message.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(messageInfo.BodyContentType); } } else if (messageInfo.BodyIsUserCreated && messageInfo.Body.Length <= 0) { message.Content = null; } return(true); } } catch (Exception err) { LoggerProxy.Default.Error(err); } return(false); }
/// <summary> /// Applies the data set in the supplied HttpMessageInfo object to the actual HTTP object. /// </summary> /// <param name="message"> /// The HTTP object. /// </param> /// <param name="messageInfo"> /// The message info. /// </param> /// <param name="cancelToken"> /// The cancellation token. /// </param> /// <returns> /// A boolean value indicating whether or not the operation was a success. Exceptions are /// handled and a value of false is returned in the event of an exception. /// </returns> public static async Task <bool> ApplyMessageInfo(this HttpResponse message, HttpMessageInfo messageInfo, CancellationToken cancelToken) { try { if (messageInfo.MessageType == MessageType.Response) { var failedHeaders = message.PopulateHeaders(messageInfo.Headers); message.StatusCode = (int)messageInfo.StatusCode; #if VERBOSE_WARNINGS foreach (string key in failedHeaders) { LoggerProxy.Default.Warn(string.Format("Failed to add HTTP header with key {0} and with value {1}.", key, failedHeaders[key])); } #endif if (messageInfo.BodyIsUserCreated && messageInfo.Body.Length > 0) { using (var ms = new MemoryStream(messageInfo.Body.ToArray())) { ms.Position = 0; message.ContentType = messageInfo.BodyContentType; if (message.Headers.ContainsKey("Expires")) { message.Headers.Remove("Expires"); } message.Headers["Expires"] = new Microsoft.Extensions.Primitives.StringValues(TimeUtil.UnixEpochString); await ms.CopyToAsync(message.Body, 4096, cancelToken); } } return(true); } } catch (Exception err) { LoggerProxy.Default.Error(err); } return(false); }
/// <summary> /// Called whenever a requested replay is available for access. /// </summary> /// <param name="replayUrl"> /// The localhost URL to request the replay on. /// </param> /// <param name="cancellationCallback"> /// A callback that you can use to terminate the playback and, optionally, the source stream with. /// </param> private static void OnReplayInspection(HttpMessageInfo messageInfo, string replayUrl, HttpReplayTerminationCallback cancellationCallback) { // Just get the default browser to open the URL. Console.WriteLine(replayUrl); Process.Start(replayUrl); // Note - Once you access a replay, it's gone. Resources are flushed and it's not persisted anywhere. // Note - You must access a replay as soon as possible. There is a 65 megabyte internal memory limit // for buffering while waiting for a client to connect. // Note - A replay is a verbatum copy, headers and all, of a filtered transaction in progress. It is // a real-time duplicate of a filtered stream. The only exception is the transfer-encoding and // content-length headers. They will be changed and Kestrel most certainly will always chunk the // replay. // The original reason for the replay API was to duplicate video streams in real-time so they // the duplicate can be fed to Windows Media Foundation and image classification can be // performed on the video frames. If and when bad images are found in the video stream, // the cancellationCallback can be used to kill the original, source video stream. }
/// <summary> /// Applies the data set in the supplied HttpMessageInfo object to the actual HTTP object. /// </summary> /// <param name="message"> /// The HTTP object. /// </param> /// <param name="messageInfo"> /// The message info. /// </param> /// <param name="cancelToken"> /// The cancellation token. /// </param> /// <returns> /// A boolean value indicating whether or not the operation was a success. Exceptions are /// handled and a value of false is returned in the event of an exception. /// </returns> public static async Task <bool> ApplyMessageInfo(this HttpRequest message, HttpMessageInfo messageInfo, CancellationToken cancelToken) { try { if (messageInfo.MessageType == MessageType.Request) { var failedHeaders = message.PopulateHeaders(messageInfo.Headers, messageInfo.ExemptedHeaders); #if VERBOSE_WARNINGS foreach (string key in failedHeaders) { LoggerProxy.Default.Warn(string.Format("Failed to add HTTP header with key {0} and with value {1}.", key, failedHeaders[key])); } #endif message.Method = messageInfo.Method.Method; message.Host = new HostString(messageInfo.Url.Host, messageInfo.Url.Port); message.Path = new PathString(messageInfo.Url.GetComponents(UriComponents.Path, UriFormat.Unescaped)); message.QueryString = new QueryString(messageInfo.Url.Query ?? string.Empty); if (messageInfo.BodyIsUserCreated && messageInfo.Body.Length > 0) { using (var ms = new MemoryStream(messageInfo.Body.ToArray())) { ms.Position = 0; message.ContentType = messageInfo.BodyContentType; message.Headers.Add("Expires", new Microsoft.Extensions.Primitives.StringValues(TimeUtil.UnixEpochString)); await ms.CopyToAsync(message.Body, 4096, cancelToken); } } return(true); } } catch (Exception err) { LoggerProxy.Default.Error(err); } return(false); }
/// <summary> /// Rewrites the message URL to force safe search on if the host is a google.X domain. /// </summary> /// <param name="messageInfo"> /// The message info. /// </param> private static void ForceGoogleSafeSearch(HttpMessageInfo messageInfo) { // If the host has google in it, we'll append the safe search command. if (messageInfo.Url.Host.IndexOf("google.", StringComparison.OrdinalIgnoreCase) > -1) { // Take everything but query params. string newUri = messageInfo.Url.GetLeftPart(UriPartial.Path); // Parse the params. var queryParams = QueryHelpers.ParseQuery(messageInfo.Url.Query); // Iterate over all parsed params. foreach (var param in queryParams) { // Skip any param named "safe" because who knows, the user might // explicitly have &safe=inative, disabling safe search, so just // ignore anything named this. if (param.Key.Equals("safe", StringComparison.OrdinalIgnoreCase)) { continue; } // Anything not "safe" param, append to the new URI. foreach (var value in param.Value) { newUri = QueryHelpers.AddQueryString(newUri, param.Key, value); } } // When we're all done, append safe search enforcement. newUri = QueryHelpers.AddQueryString(newUri, "safe", "active"); // if we end up with a valid URI, overwrite it. if (Uri.TryCreate(newUri, UriKind.Absolute, out Uri result)) { messageInfo.Url = result; } } }
/// <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) { ClientWebSocket wsServer = null; System.Net.WebSockets.WebSocket wsClient = null; try { // First we need the URL for this connection, since it's been requested to be // upgraded to a websocket. var connFeature = context.Features.Get <IHttpRequestFeature>(); string fullUrl = string.Empty; if (connFeature != null && connFeature.RawTarget != null && !string.IsNullOrEmpty(connFeature.RawTarget) && !(string.IsNullOrWhiteSpace(connFeature.RawTarget))) { fullUrl = $"{context.Request.Scheme}://{context.Request.Host}{connFeature.RawTarget}"; } else { fullUrl = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}{context.Request.QueryString}"; } // Need to replate the scheme with appropriate websocket scheme. if (fullUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase)) { fullUrl = "ws://" + fullUrl.Substring(7); } else if (fullUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) { fullUrl = "wss://" + fullUrl.Substring(8); } // 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 wsUri)) { LoggerProxy.Default.Error("Failed to parse websocket URI."); return; } // Create the websocket that's going to connect to the remote server. wsServer = new ClientWebSocket(); wsServer.Options.Cookies = new System.Net.CookieContainer(); //wsServer.Options.SetBuffer((int)ushort.MaxValue * 16, (int)ushort.MaxValue * 16); foreach (var proto in context.WebSockets.WebSocketRequestedProtocols) { wsServer.Options.AddSubProtocol(proto); } foreach (var hdr in context.Request.Headers) { if (!ForbiddenWsHeaders.IsForbidden(hdr.Key)) { try { wsServer.Options.SetRequestHeader(hdr.Key, hdr.Value.ToString()); } catch (Exception hdrException) { LoggerProxy.Default.Error(hdrException); } } } foreach (var cookie in context.Request.Cookies) { try { wsServer.Options.Cookies.Add(new Uri(fullUrl, UriKind.Absolute), new System.Net.Cookie(cookie.Key, System.Net.WebUtility.UrlEncode(cookie.Value))); } catch (Exception e) { LoggerProxy.Default.Error("Error while attempting to add websocket cookie."); LoggerProxy.Default.Error(e); } } if (context.Connection.ClientCertificate != null) { wsServer.Options.ClientCertificates = new System.Security.Cryptography.X509Certificates.X509CertificateCollection(new[] { context.Connection.ClientCertificate.ToV2Certificate() }); } // Connect the server websocket to the upstream, remote webserver. await wsServer.ConnectAsync(wsUri, context.RequestAborted); foreach (string key in wsServer.ResponseHeaders) { if (!ForbiddenWsHeaders.IsForbidden(key)) { try { var value = wsServer.ResponseHeaders[key]; context.Response.Headers[key] = wsServer.ResponseHeaders[key]; } catch (Exception hdrException) { LoggerProxy.Default.Error(hdrException); } } } // Create, via acceptor, the client websocket. This is the local machine's websocket. wsClient = await context.WebSockets.AcceptWebSocketAsync(wsServer.SubProtocol ?? null); // 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); } var msgNfo = new HttpMessageInfo { Url = wsUri, Method = new HttpMethod(context.Request.Method), IsEncrypted = context.Request.IsHttps, Headers = context.Request.Headers.ToNameValueCollection(), HttpVersion = upstreamReqVersionMatch ?? new Version(1, 0), MessageProtocol = MessageProtocol.WebSocket, MessageType = MessageType.Request, RemoteAddress = context.Connection.RemoteIpAddress, RemotePort = (ushort)context.Connection.RemotePort, LocalAddress = context.Connection.LocalIpAddress, LocalPort = (ushort)context.Connection.LocalPort }; _configuration.NewHttpMessageHandler?.Invoke(msgNfo); switch (msgNfo.ProxyNextAction) { case ProxyNextAction.DropConnection: { await wsClient.CloseAsync(System.Net.WebSockets.WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); return; } } var serverMessageInfo = new HttpMessageInfo { Url = wsUri, MessageId = msgNfo.MessageId, Method = new HttpMethod(context.Request.Method), IsEncrypted = context.Request.IsHttps, Headers = context.Request.Headers.ToNameValueCollection(), HttpVersion = upstreamReqVersionMatch ?? new Version(1, 0), MessageProtocol = MessageProtocol.WebSocket, MessageType = MessageType.Response, RemoteAddress = context.Connection.RemoteIpAddress, RemotePort = (ushort)context.Connection.RemotePort, LocalAddress = context.Connection.LocalIpAddress, LocalPort = (ushort)context.Connection.LocalPort }; var clientMessageInfo = new HttpMessageInfo { Url = wsUri, MessageId = msgNfo.MessageId, IsEncrypted = context.Request.IsHttps, Headers = context.Request.Headers.ToNameValueCollection(), HttpVersion = upstreamReqVersionMatch ?? new Version(1, 0), MessageProtocol = MessageProtocol.WebSocket, MessageType = MessageType.Request, RemoteAddress = context.Connection.RemoteIpAddress, RemotePort = (ushort)context.Connection.RemotePort, LocalAddress = context.Connection.LocalIpAddress, LocalPort = (ushort)context.Connection.LocalPort }; bool inspect = true; switch (msgNfo.ProxyNextAction) { case ProxyNextAction.AllowAndIgnoreContent: case ProxyNextAction.AllowAndIgnoreContentAndResponse: { inspect = false; } break; } // Spawn an async task that will poll the remote server for data in a loop, and then // write any data it gets to the client websocket. var serverTask = Task.Run(async() => { System.Net.WebSockets.WebSocketReceiveResult serverResult = null; var serverBuffer = new byte[1024 * 4]; try { bool looping = true; serverResult = await wsServer.ReceiveAsync(new ArraySegment <byte>(serverBuffer), context.RequestAborted); while (looping && !serverResult.CloseStatus.HasValue && !context.RequestAborted.IsCancellationRequested) { if (inspect) { serverMessageInfo.Body = new Memory <byte>(serverBuffer, 0, serverResult.Count); switch (serverResult.MessageType) { case System.Net.WebSockets.WebSocketMessageType.Binary: { serverMessageInfo.BodyContentType = s_octetStreamContentType; } break; case System.Net.WebSockets.WebSocketMessageType.Text: { serverMessageInfo.BodyContentType = s_plainTextContentType; } break; } _configuration.HttpMessageWholeBodyInspectionHandler?.Invoke(serverMessageInfo); } switch (serverMessageInfo.ProxyNextAction) { case ProxyNextAction.DropConnection: { looping = false; } break; default: { await wsClient.SendAsync(new ArraySegment <byte>(serverBuffer, 0, serverResult.Count), serverResult.MessageType, serverResult.EndOfMessage, context.RequestAborted); if (!wsClient.CloseStatus.HasValue) { serverResult = await wsServer.ReceiveAsync(new ArraySegment <byte>(serverBuffer), context.RequestAborted); continue; } } break; } looping = false; } await wsClient.CloseAsync(serverResult.CloseStatus.Value, serverResult.CloseStatusDescription, context.RequestAborted); } catch (Exception err) { LoggerProxy.Default.Error(err); try { var closeStatus = serverResult?.CloseStatus ?? System.Net.WebSockets.WebSocketCloseStatus.NormalClosure; var closeMessage = serverResult?.CloseStatusDescription ?? string.Empty; await wsClient.CloseAsync(closeStatus, closeMessage, context.RequestAborted); } catch { } } }); // Spawn an async task that will poll the local client websocket, in a loop, and then // write any data it gets to the remote server websocket. var clientTask = Task.Run(async() => { System.Net.WebSockets.WebSocketReceiveResult clientResult = null; var clientBuffer = new byte[1024 * 4]; try { bool looping = true; clientResult = await wsClient.ReceiveAsync(new ArraySegment <byte>(clientBuffer), context.RequestAborted); while (looping && !clientResult.CloseStatus.HasValue && !context.RequestAborted.IsCancellationRequested) { if (inspect) { clientMessageInfo.Body = new Memory <byte>(clientBuffer, 0, clientResult.Count); switch (clientResult.MessageType) { case System.Net.WebSockets.WebSocketMessageType.Binary: { clientMessageInfo.BodyContentType = s_octetStreamContentType; } break; case System.Net.WebSockets.WebSocketMessageType.Text: { clientMessageInfo.BodyContentType = s_plainTextContentType; } break; } _configuration.HttpMessageWholeBodyInspectionHandler?.Invoke(clientMessageInfo); } switch (clientMessageInfo.ProxyNextAction) { case ProxyNextAction.DropConnection: { looping = false; } break; default: { await wsServer.SendAsync(new ArraySegment <byte>(clientBuffer, 0, clientResult.Count), clientResult.MessageType, clientResult.EndOfMessage, context.RequestAborted); if (!wsServer.CloseStatus.HasValue) { clientResult = await wsClient.ReceiveAsync(new ArraySegment <byte>(clientBuffer), context.RequestAborted); continue; } } break; } looping = false; } await wsServer.CloseAsync(clientResult.CloseStatus.Value, clientResult.CloseStatusDescription, context.RequestAborted); } catch (Exception err) { LoggerProxy.Default.Error(err); try { var closeStatus = clientResult?.CloseStatus ?? System.Net.WebSockets.WebSocketCloseStatus.NormalClosure; var closeMessage = clientResult?.CloseStatusDescription ?? string.Empty; await wsServer.CloseAsync(closeStatus, closeMessage, context.RequestAborted); } catch { } } }); // Above, we have created a bridge between the local and remote websocket. Wait for // both associated tasks to complete. await Task.WhenAll(serverTask, clientTask); } catch (Exception wshe) { LoggerProxy.Default.Error(wshe); } finally { if (wsClient != null) { wsClient.Dispose(); wsClient = null; } if (wsServer != null) { wsServer.Dispose(); wsServer = null; } } }
/// <summary> /// Called whenever we request to fulfill a request ourselves. /// </summary> /// <param name="messageInfo"> /// The message info. /// </param> /// <param name="context"> /// The http context to read and write to and from. /// </param> /// <returns> /// Completion task. /// </returns> private static async Task OnManualFulfillmentCallback(HttpMessageInfo messageInfo, HttpContext context) { // Create the message AFTER we give the user a chance to alter things. var requestMsg = new HttpRequestMessage(messageInfo.Method, messageInfo.Url); // Ignore failed headers. We don't really care. var initialFailedHeaders = requestMsg.PopulateHeaders(messageInfo.Headers, messageInfo.ExemptedHeaders); // Make sure we send the body. if (context.Request.Body != null) { 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); } } try { var response = await s_client.SendAsync(requestMsg, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted); // 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 upstreamResponseHeaders = response.ExportAllHeaders(); bool responseHasZeroContentLength = false; bool responseIsFixedLength = false; foreach (var kvp in upstreamResponseHeaders.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; } } } } // Copy over the upstream headers. context.Response.PopulateHeaders(upstreamResponseHeaders, new System.Collections.Generic.HashSet <string>()); // Copy over the upstream body. using (var responseStream = await response?.Content.ReadAsStreamAsync()) { context.Response.StatusCode = (int)response.StatusCode; context.Response.PopulateHeaders(response.ExportAllHeaders(), new System.Collections.Generic.HashSet <string>()); if (!responseHasZeroContentLength && 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) { while (e != null) { Console.WriteLine(e.Message); Console.WriteLine(e.StackTrace); } } }
/// <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) { ClientWebSocket wsServer = null; System.Net.WebSockets.WebSocket wsClient = null; DiagnosticsWebSession diagSession = new DiagnosticsWebSession(); try { // First we need the URL for this connection, since it's been requested to be upgraded to // a websocket. var fullUrl = Microsoft.AspNetCore.Http.Extensions.UriHelper.GetDisplayUrl(context.Request); // Need to replate the scheme with appropriate websocket scheme. if (fullUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase)) { fullUrl = "ws://" + fullUrl.Substring(7); } else if (fullUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) { fullUrl = "wss://" + fullUrl.Substring(8); } diagSession.ClientRequestUri = fullUrl; // 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 wsUri)) { LoggerProxy.Default.Error("Failed to parse websocket URI."); return; } // Create the websocket that's going to connect to the remote server. wsServer = new ClientWebSocket(); wsServer.Options.Cookies = new System.Net.CookieContainer(); wsServer.Options.SetBuffer((int)ushort.MaxValue * 16, (int)ushort.MaxValue * 16); foreach (var cookie in context.Request.Cookies) { try { wsServer.Options.Cookies.Add(new Uri(fullUrl, UriKind.Absolute), new System.Net.Cookie(cookie.Key, System.Net.WebUtility.UrlEncode(cookie.Value))); } catch (Exception e) { LoggerProxy.Default.Error("Error while attempting to add websocket cookie."); LoggerProxy.Default.Error(e); } } if (context.Connection.ClientCertificate != null) { wsServer.Options.ClientCertificates = new System.Security.Cryptography.X509Certificates.X509CertificateCollection(new[] { context.Connection.ClientCertificate.ToV2Certificate() }); } if (Collector.IsDiagnosticsEnabled) { var diagHeaderBuilder = new StringBuilder(); foreach (var hdr in context.Request.Headers) { diagHeaderBuilder.AppendFormat($"{hdr.Key}: {hdr.Value.ToString()}\r\n"); } diagSession.ClientRequestHeaders = diagHeaderBuilder.ToString(); } var reqHeaderBuilder = new StringBuilder(); foreach (var hdr in context.Request.Headers) { if (!ForbiddenWsHeaders.IsForbidden(hdr.Key)) { reqHeaderBuilder.AppendFormat("{0}: {1}\r\n", hdr.Key, hdr.Value.ToString()); try { wsServer.Options.SetRequestHeader(hdr.Key, hdr.Value.ToString()); } catch (Exception hdrException) { LoggerProxy.Default.Error(hdrException); } } } reqHeaderBuilder.Append("\r\n"); diagSession.ServerRequestHeaders = reqHeaderBuilder.ToString(); // Connect the server websocket to the upstream, remote webserver. await wsServer.ConnectAsync(wsUri, context.RequestAborted); // Create, via acceptor, the client websocket. This is the local machine's websocket. wsClient = await context.WebSockets.AcceptWebSocketAsync(wsServer.SubProtocol ?? null); var msgNfo = new HttpMessageInfo { Url = wsUri, IsEncrypted = context.Request.IsHttps, Headers = context.Request.Headers.ToNameValueCollection(), MessageProtocol = MessageProtocol.WebSocket, MessageType = MessageType.Request, RemoteAddress = context.Connection.RemoteIpAddress, RemotePort = (ushort)context.Connection.RemotePort, LocalAddress = context.Connection.LocalIpAddress, LocalPort = (ushort)context.Connection.LocalPort }; _newMessageCb?.Invoke(msgNfo); switch (msgNfo.ProxyNextAction) { case ProxyNextAction.DropConnection: { await wsClient.CloseAsync(System.Net.WebSockets.WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); return; } } // Spawn an async task that will poll the remote server for data in a loop, and then // write any data it gets to the client websocket. var serverTask = Task.Run(async() => { System.Net.WebSockets.WebSocketReceiveResult serverStatus = null; var serverBuffer = new byte[1024 * 4]; try { bool looping = true; serverStatus = await wsServer.ReceiveAsync(new ArraySegment <byte>(serverBuffer), context.RequestAborted); while (looping && !serverStatus.CloseStatus.HasValue && !context.RequestAborted.IsCancellationRequested) { await wsClient.SendAsync(new ArraySegment <byte>(serverBuffer, 0, serverStatus.Count), serverStatus.MessageType, serverStatus.EndOfMessage, context.RequestAborted); if (!wsClient.CloseStatus.HasValue) { serverStatus = await wsServer.ReceiveAsync(new ArraySegment <byte>(serverBuffer), context.RequestAborted); continue; } looping = false; } await wsClient.CloseAsync(serverStatus.CloseStatus.Value, serverStatus.CloseStatusDescription, context.RequestAborted); } catch { try { var closeStatus = serverStatus?.CloseStatus ?? System.Net.WebSockets.WebSocketCloseStatus.NormalClosure; var closeMessage = serverStatus?.CloseStatusDescription ?? string.Empty; await wsClient.CloseAsync(closeStatus, closeMessage, context.RequestAborted); } catch { } } }); // Spawn an async task that will poll the local client websocket, in a loop, and then // write any data it gets to the remote server websocket. var clientTask = Task.Run(async() => { System.Net.WebSockets.WebSocketReceiveResult clientResult = null; var clientBuffer = new byte[1024 * 4]; try { bool looping = true; clientResult = await wsClient.ReceiveAsync(new ArraySegment <byte>(clientBuffer), context.RequestAborted); while (looping && !clientResult.CloseStatus.HasValue && !context.RequestAborted.IsCancellationRequested) { await wsServer.SendAsync(new ArraySegment <byte>(clientBuffer, 0, clientResult.Count), clientResult.MessageType, clientResult.EndOfMessage, context.RequestAborted); if (!wsServer.CloseStatus.HasValue) { clientResult = await wsClient.ReceiveAsync(new ArraySegment <byte>(clientBuffer), context.RequestAborted); continue; } looping = false; } await wsServer.CloseAsync(clientResult.CloseStatus.Value, clientResult.CloseStatusDescription, context.RequestAborted); } catch { try { var closeStatus = clientResult?.CloseStatus ?? System.Net.WebSockets.WebSocketCloseStatus.NormalClosure; var closeMessage = clientResult?.CloseStatusDescription ?? string.Empty; await wsServer.CloseAsync(closeStatus, closeMessage, context.RequestAborted); } catch { } } }); // Above, we have created a bridge between the local and remote websocket. Wait for both // associated tasks to complete. await Task.WhenAll(serverTask, clientTask); } catch (Exception wshe) { LoggerProxy.Default.Error(wshe); } finally { Collector.ReportSession(diagSession); if (wsClient != null) { wsClient.Dispose(); wsClient = null; } if (wsServer != null) { wsServer.Dispose(); wsServer = null; } } }
/// <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); } } }
/// <summary> /// Constructs a new InspectionStream instance. /// </summary> /// <param name="messageInfo"> /// The message info. /// </param> /// <param name="innerStream"> /// The inner stream object. /// </param> public InspectionStream(HttpMessageInfo messageInfo, Stream innerStream) { MessageInfo = messageInfo; _innerStream = 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) { 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> /// Called whenever a new request or response message is intercepted. /// </summary> /// <param name="messageInfo"> /// The message info. /// </param> /// <remarks> /// In this callback we can do all kinds of crazy things, including fully modify the HTTP /// headers, the request target, etc etc. /// </remarks> private static void OnNewMessage(HttpMessageInfo messageInfo) { if (messageInfo.BodyContentType != string.Empty) { Console.WriteLine("New message with content of type: {0}\n\t{1}\n\t{2}", messageInfo.BodyContentType, messageInfo.Url, messageInfo.MessageProtocol); } else { Console.WriteLine("New message: {0}\n\t{1}", messageInfo.Url, messageInfo.MessageProtocol); } ForceGoogleSafeSearch(messageInfo); if (RedirectBingToYahoo(messageInfo)) { return; } if (ManuallyFulfill(messageInfo)) { return; } // Get Technikempire.com as a replay request. // Replay requests are only available on response message types. // This will cause us to receive a request URI on the IpV4 loopback adapter // that will enable us to "replay" the request. // // This "replay" is a mirroring of the data, allowing it to pass through // but being duplicated in real time. This means you can inspect the // stream in-parallel without interrupting the original stream. // // At any time, you can force the original, mirrored stream to abort and // close by invoking the callback provided in the relay inspection // callback handler. if (messageInfo.Url.Host.Equals("technikempire.com", StringComparison.OrdinalIgnoreCase)) { messageInfo.ProxyNextAction = ProxyNextAction.AllowButRequestResponseReplay; return; } // Block only this casino website. if (messageInfo.Url.Host.Equals("777.com", StringComparison.OrdinalIgnoreCase)) { messageInfo.MessageType = MessageType.Response; messageInfo.ProxyNextAction = ProxyNextAction.DropConnection; messageInfo.BodyContentType = "text/html"; messageInfo.Body = s_blockPageBytes; return; } // By default, allow and ignore content, but not any responses to this content. messageInfo.ProxyNextAction = ProxyNextAction.AllowAndIgnoreContent; // If the new message is a response, we want to inspect the payload if it is HTML. if (messageInfo.MessageType == MessageType.Response) { foreach (string headerName in messageInfo.Headers) { if (messageInfo.Headers[headerName].IndexOf("html") != -1) { Console.WriteLine("Requesting to inspect HTML response for request {0}.", messageInfo.Url); messageInfo.ProxyNextAction = ProxyNextAction.AllowButRequestContentInspection; return; } } // The other kind of filtering we want to do here is to monitor video // streams. So, if we find a video content type in a response, we'll subscribe // the very new, and extremely exciting streaming inspection callback!!!!! var contentTypeKey = "Content-Type"; var contentType = messageInfo.Headers[contentTypeKey]; if (contentType != null && (contentType.IndexOf("video/", StringComparison.OrdinalIgnoreCase) != -1 || contentType.IndexOf("mpeg", StringComparison.OrdinalIgnoreCase) != -1)) { // Means we have a video response coming. // We want to get the video stream too! Because we have the tools to tell // if video is naughty or nice! Console.WriteLine("Requesting to inspect streamed video response."); messageInfo.ProxyNextAction = ProxyNextAction.AllowButRequestStreamedContentInspection; } } }