/// <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);
        }
예제 #2
0
        /// <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);
        }
예제 #3
0
        /// <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);
        }
예제 #4
0
        /// <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;
                }
            }
        }
예제 #5
0
        /// <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;
        }
예제 #6
0
        /// <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;
            }
        }
예제 #7
0
        /// <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;
                }
            }
        }
예제 #8
0
        /// <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);
        }
예제 #9
0
        /// <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);
        }
예제 #10
0
        /// <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);
        }
예제 #12
0
        /// <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;
                }
            }
        }
예제 #13
0
        /// <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;
                }
            }
        }
예제 #14
0
        /// <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;
                }
            }
        }
예제 #16
0
        /// <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);
                }
            }
        }
예제 #17
0
 /// <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 { }
                }
            }
        }
예제 #19
0
        /// <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;
                }
            }
        }