Example #1
0
        /// <summary>
        /// Exports all headers, including content headers, if any.
        /// </summary>
        /// <param name="message">
        /// The request message.
        /// </param>
        /// <returns>
        /// A NameValueCollection of all headers.
        /// </returns>
        public static NameValueCollection ExportAllHeaders(this HttpRequestMessage message)
        {
            var retVal = new NameValueCollection();

            foreach (var header in message.Headers)
            {
                if (ForbiddenHttpHeaders.IsForbidden(header.Key))
                {
                    continue;
                }

                foreach (var value in header.Value)
                {
                    retVal.Add(header.Key, value);
                }
            }

            if (message.Content != null && message.Content.Headers != null)
            {
                foreach (var header in message.Content.Headers)
                {
                    if (ForbiddenHttpHeaders.IsForbidden(header.Key))
                    {
                        continue;
                    }

                    foreach (var value in header.Value)
                    {
                        retVal.Add(header.Key, value);
                    }
                }
            }

            return(retVal);
        }
        /// <summary>
        /// Copies all possible headers from the given collection into this HttpResponseMessage
        /// instance and then returns the headers that failed to be added.
        /// </summary>
        /// <param name="message">
        /// The message.
        /// </param>
        /// <param name="headers">
        /// The headers.
        /// </param>
        /// <param name="exemptedHeaders">
        /// List of headers that are exempt from being removed if they are "forbidden" headers.
        /// </param>
        /// <returns>
        /// A collection of all headers that failed to be added.
        /// </returns>
        public static NameValueCollection PopulateHeaders(this HttpResponseMessage message, NameValueCollection headers, HashSet <string> exemptedHeaders)
        {
            // This will hold whatever headers we cannot successfully add here.
            var clonedCollection = new NameValueCollection(headers);

            foreach (string key in headers)
            {
                if (ForbiddenHttpHeaders.IsForbidden(key))
                {
                    continue;
                }

                if (message.Headers.TryAddWithoutValidation(key, headers.GetValues(key)))
                {
                    clonedCollection.Remove(key);
                }
                else
                {
                    if (message.Content != null)
                    {
                        if (message.Content.Headers.TryAddWithoutValidation(key, headers.GetValues(key)))
                        {
                            clonedCollection.Remove(key);
                        }
                    }
                }
            }

            return(clonedCollection);
        }
        /// <summary>
        /// Copies all possible headers from the given collection into this HttpResponse instance and
        /// then returns the headers that failed to be added.
        /// </summary>
        /// <param name="message">
        /// The message.
        /// </param>
        /// <param name="headers">
        /// The headers.
        /// </param>
        /// <param name="exemptedHeaders">
        /// List of headers that are exempt from being removed if they are "forbidden" headers.
        /// </param>
        /// <returns>
        /// A collection of all headers that failed to be added.
        /// </returns>
        public static NameValueCollection PopulateHeaders(this HttpRequest message, NameValueCollection headers, HashSet <string> exemptedHeaders)
        {
            // This will hold whatever headers we cannot successfully add here.
            var clonedCollection = new NameValueCollection(headers);

            foreach (string key in headers)
            {
                if (!exemptedHeaders.Contains(key) && ForbiddenHttpHeaders.IsForbidden(key))
                {
                    continue;
                }

                try
                {
                    if (message.Headers.ContainsKey(key))
                    {
                        message.Headers.Remove(key);
                    }

                    message.Headers.Add(key, headers.GetValues(key));
                    clonedCollection.Remove(key);
                }
                catch { }
            }

            return(clonedCollection);
        }
Example #4
0
        /// <summary>
        /// Copies all possible headers from the given collection into this HttpRequestMessage
        /// instance and then returns the headers that failed to be added.
        /// </summary>
        /// <param name="message">
        /// The message.
        /// </param>
        /// <param name="headers">
        /// The headers.
        /// </param>
        /// <param name="exemptedHeaders">
        /// List of headers that are exempt from being removed if they are "forbidden" headers.
        /// </param>
        /// <returns>
        /// A collection of all headers that failed to be added.
        /// </returns>
        public static NameValueCollection PopulateHeaders(this HttpRequestMessage message, NameValueCollection headers, HashSet <string> exemptedHeaders)
        {
            // This will hold whatever headers we cannot successfully add here.
            var clonedCollection = new NameValueCollection(headers);

            foreach (string key in headers)
            {
                if (!exemptedHeaders.Contains(key) && ForbiddenHttpHeaders.IsForbidden(key))
                {
                    continue;
                }

                if (message.Headers.TryAddWithoutValidation(key, headers.GetValues(key)))
                {
                    clonedCollection.Remove(key);
                }
                else
                {
                    if (message.Content != null)
                    {
                        if (message.Content.Headers.TryAddWithoutValidation(key, headers.GetValues(key)))
                        {
                            clonedCollection.Remove(key);
                        }
                    }
                }
            }

            // Apparently, host won't set unless we use the typed accessor!
            message.Headers.Host = headers["Host"] ?? message.Headers.Host;

            return(clonedCollection);
        }
Example #5
0
        /// <summary>
        /// Copies all possible headers from the given collection into this HttpResponse instance and
        /// then returns the headers that failed to be added.
        /// </summary>
        /// <param name="message">
        /// The message.
        /// </param>
        /// <param name="headers">
        /// The headers.
        /// </param>
        /// <returns>
        /// A collection of all headers that failed to be added.
        /// </returns>
        public static NameValueCollection PopulateHeaders(this HttpResponse message, NameValueCollection headers)
        {
            // This will hold whatever headers we cannot successfully add here.
            var clonedCollection = new NameValueCollection(headers);

            foreach (string key in headers)
            {
                if (ForbiddenHttpHeaders.IsForbidden(key))
                {
                    continue;
                }

                try
                {
                    message.Headers.Add(key, new Microsoft.Extensions.Primitives.StringValues(headers.GetValues(key)));
                    clonedCollection.Remove(key);
                }
                catch { }
            }

            return(clonedCollection);
        }
Example #6
0
        public override async Task Handle(HttpContext context)
        {
            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.
                Uri reqUrl = null;

                if (!Uri.TryCreate(fullUrl, UriKind.RelativeOrAbsolute, out reqUrl))
                {
                    LoggerProxy.Default.Error("Failed to parse HTTP URL.");
                    return;
                }

                // Create a new request to send out upstream.
                var requestMsg = new HttpRequestMessage(new HttpMethod(context.Request.Method), fullUrl);

                if (context.Connection.ClientCertificate != null)
                {
                    // TODO - Handle client certificates.
                }

                // Build request headers into this, so we can pass the result to message begin/end callbacks.
                var reqHeaderBuilder = new StringBuilder();

                var failedInitialHeaders = new List <Tuple <string, string> >();

                bool requestHasZeroContentLength = false;

                // Clone headers from the real client request to our upstream HTTP request.
                foreach (var hdr in context.Request.Headers)
                {
                    try
                    {
                        if (hdr.Key.Equals("Content-Length", StringComparison.OrdinalIgnoreCase) && hdr.Value.ToString().Equals("0"))
                        {
                            requestHasZeroContentLength = true;
                        }
                    }
                    catch { }

                    try
                    {
                        reqHeaderBuilder.AppendFormat("{0}: {1}\r\n", hdr.Key, hdr.Value.ToString());
                    }
                    catch { }

                    if (ForbiddenHttpHeaders.IsForbidden(hdr.Key))
                    {
                        continue;
                    }

                    if (!requestMsg.Headers.TryAddWithoutValidation(hdr.Key, hdr.Value.ToString()))
                    {
                        string hName  = hdr.Key != null ? hdr.Key : string.Empty;
                        string hValue = hdr.Value.ToString() != null?hdr.Value.ToString() : string.Empty;

                        if (hName.Length > 0 && hValue.Length > 0)
                        {
                            failedInitialHeaders.Add(new Tuple <string, string>(hName, hValue));
                        }
                    }
                }

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

                // Add trailing CRLF to the request headers string.
                reqHeaderBuilder.Append("\r\n");

                // Since headers are complete at this stage, let's do our first call to message begin
                // for the request side.
                ProxyNextAction requestNextAction = ProxyNextAction.AllowAndIgnoreContentAndResponse;
                string          requestBlockResponseContentType = string.Empty;
                byte[]          requestBlockResponse            = null;
                m_msgBeginCb?.Invoke(reqUrl, reqHeaderBuilder.ToString(), m_nullBody, context.Request.IsHttps ? MessageType.Https : MessageType.Http, MessageDirection.Request, out requestNextAction, out requestBlockResponseContentType, out requestBlockResponse);

                if (requestNextAction == ProxyNextAction.DropConnection)
                {
                    if (requestBlockResponse != null)
                    {
                        // User wants to block this request with a custom response.
                        await DoCustomResponse(context, requestBlockResponseContentType, requestBlockResponse);
                    }
                    else
                    {
                        // User wants to block this request with a generic 204 response.
                        Do204(context);
                    }

                    return;
                }

                // Get the request body into memory.
                using (var ms = new MemoryStream())
                {
                    await Microsoft.AspNetCore.Http.Extensions.StreamCopyOperation.CopyToAsync(context.Request.Body, ms, null, 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 have a body and the user previously instructed us to give them the
                        // content, if any, for inspection.
                        if (requestNextAction == ProxyNextAction.AllowButRequestContentInspection)
                        {
                            // We'll now call the message end function for the request side.
                            bool shouldBlockRequest = false;
                            requestBlockResponseContentType = string.Empty;
                            requestBlockResponse            = null;
                            m_msgEndCb?.Invoke(reqUrl, reqHeaderBuilder.ToString(), requestBody, context.Request.IsHttps ? MessageType.Https : MessageType.Http, MessageDirection.Request, out shouldBlockRequest, out requestBlockResponseContentType, out requestBlockResponse);

                            if (shouldBlockRequest)
                            {
                                // User wants to block this request after inspecting the content.

                                if (requestBlockResponse != null)
                                {
                                    // User wants to block this request with a custom response.
                                    await DoCustomResponse(context, requestBlockResponseContentType, requestBlockResponse);
                                }
                                else
                                {
                                    // User wants to block this request with a generic 204 response.
                                    Do204(context);
                                }

                                return;
                            }
                        }

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

                // Ensure that content type is set properly because ByteArrayContent and friends will
                // modify these fields.
                foreach (var et in failedInitialHeaders)
                {
                    if (!requestMsg.Headers.TryAddWithoutValidation(et.Item1, et.Item2))
                    {
                        if (requestMsg.Content != null)
                        {
                            if (!requestMsg.Content.Headers.TryAddWithoutValidation(et.Item1, et.Item2))
                            {
                                LoggerProxy.Default.Warn(string.Format("Failed to add HTTP header with key {0} and with value {1}.", et.Item1, et.Item2));
                            }
                        }
                    }
                }

                // Lets start sending the request upstream. We're going to as 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);
                }
                catch (HttpRequestException ex)
                {
                    LoggerProxy.Default.Error(ex);

                    if (ex.InnerException is WebException && ex.InnerException.InnerException is System.Security.Authentication.AuthenticationException)
                    {
                        if (m_onBadCertificate != null)
                        {
                            string customResponseContentType = null;
                            byte[] customResponse            = null;

                            m_onBadCertificate(reqUrl, ex, out customResponseContentType, out customResponse);

                            if (customResponse != null)
                            {
                                await DoCustomResponse(context, customResponseContentType, customResponse);

                                return;
                            }
                            else
                            {
                                Do204(context);
                            }
                        }
                    }
                }
                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.Headers.Clear();

                // Ensure our client's response status code is set to match ours.
                context.Response.StatusCode = (int)response.StatusCode;

                // Build response headers into this, so we can pass the result to message begin/end callbacks.
                var resHeaderBuilder = new StringBuilder();

                bool responseHasZeroContentLength = false;

                // Iterate over all upstream response headers. Note that response.Content.Headers is
                // not ALL headers. Headers are split up into different properties according to
                // logical grouping.
                foreach (var hdr in response.Content.Headers)
                {
                    try
                    {
                        if (hdr.Key.Equals("Content-Length", StringComparison.OrdinalIgnoreCase) && hdr.Value.ToString().Equals("0"))
                        {
                            responseHasZeroContentLength = true;
                        }
                    }
                    catch { }

                    try
                    {
                        resHeaderBuilder.AppendFormat("{0}: {1}\r\n", hdr.Key, string.Join(", ", hdr.Value));
                    }
                    catch { }

                    if (ForbiddenHttpHeaders.IsForbidden(hdr.Key))
                    {
                        continue;
                    }

                    try
                    {
                        context.Response.Headers.Add(hdr.Key, new Microsoft.Extensions.Primitives.StringValues(hdr.Value.ToArray()));
                    }
                    catch (Exception e)
                    {
                        LoggerProxy.Default.Error(e);
                    }
                }

                // As mentioned above, headers are split up into different properties. We need to now
                // clone over the generic headers.
                foreach (var hdr in response.Headers)
                {
                    try
                    {
                        if (hdr.Key.Equals("Content-Length", StringComparison.OrdinalIgnoreCase) && hdr.Value.ToString().Equals("0"))
                        {
                            responseHasZeroContentLength = true;
                        }
                    }
                    catch { }

                    try
                    {
                        resHeaderBuilder.AppendFormat("{0}: {1}\r\n", hdr.Key, string.Join(", ", hdr.Value));
                    }
                    catch { }

                    if (ForbiddenHttpHeaders.IsForbidden(hdr.Key))
                    {
                        continue;
                    }

                    try
                    {
                        context.Response.Headers.Add(hdr.Key, new Microsoft.Extensions.Primitives.StringValues(hdr.Value.ToArray()));
                    }
                    catch (Exception e)
                    {
                        LoggerProxy.Default.Error(e);
                    }
                }

                resHeaderBuilder.Append("\r\n");

                // Now that we have response headers, let's call the message begin handler for the
                // response. Unless of course, the user has asked us NOT to do this.
                if (requestNextAction != ProxyNextAction.AllowAndIgnoreContentAndResponse)
                {
                    ProxyNextAction responseNextAction = ProxyNextAction.AllowAndIgnoreContent;
                    string          responseBlockResponseContentType = string.Empty;
                    byte[]          responseBlockResponse            = null;

                    m_msgBeginCb?.Invoke(reqUrl, resHeaderBuilder.ToString(), m_nullBody, context.Request.IsHttps ? MessageType.Https : MessageType.Http, MessageDirection.Response, out responseNextAction, out responseBlockResponseContentType, out responseBlockResponse);

                    if (responseNextAction == ProxyNextAction.DropConnection)
                    {
                        if (responseBlockResponse != null)
                        {
                            // User wants to block this response with a custom response.
                            await DoCustomResponse(context, responseBlockResponseContentType, responseBlockResponse);
                        }
                        else
                        {
                            // User wants to block this response with a generic 204 response.
                            Do204(context);
                        }
                    }

                    if (responseNextAction == ProxyNextAction.AllowButRequestContentInspection)
                    {
                        using (var upstreamResponseStream = await response.Content.ReadAsStreamAsync())
                        {
                            using (var ms = new MemoryStream())
                            {
                                await Microsoft.AspNetCore.Http.Extensions.StreamCopyOperation.CopyToAsync(upstreamResponseStream, ms, null, context.RequestAborted);

                                var responseBody = ms.ToArray();

                                bool shouldBlockResponse = false;
                                responseBlockResponseContentType = string.Empty;
                                responseBlockResponse            = null;
                                m_msgEndCb?.Invoke(reqUrl, resHeaderBuilder.ToString(), responseBody, context.Request.IsHttps ? MessageType.Https : MessageType.Http, MessageDirection.Response, out shouldBlockResponse, out responseBlockResponseContentType, out responseBlockResponse);

                                if (shouldBlockResponse)
                                {
                                    if (responseBlockResponse != null)
                                    {
                                        // User wants to block this response with a custom response.
                                        await DoCustomResponse(context, responseBlockResponseContentType, responseBlockResponse);
                                    }
                                    else
                                    {
                                        // User wants to block this response with a generic 204 response.
                                        Do204(context);
                                    }

                                    return;
                                }

                                // 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 (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 (upstreamReqVersionMatch != null && upstreamReqVersionMatch.Major == 1 && upstreamReqVersionMatch.Minor == 0)
                                    {
                                        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;
                            }
                        }
                    }
                }

                // 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())
                {
                    if (upstreamReqVersionMatch != null && upstreamReqVersionMatch.Major == 1 && upstreamReqVersionMatch.Minor == 0)
                    {
                        using (var ms = new MemoryStream())
                        {
                            await Microsoft.AspNetCore.Http.Extensions.StreamCopyOperation.CopyToAsync(responseStream, ms, null, context.RequestAborted);

                            var responseBody = ms.ToArray();

                            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");
                        }
                        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);
                }
            }
        }
Example #7
0
        public override async Task Handle(HttpContext context)
        {
            // Use this to collect information about the HTTP request's server and client parts.
            Diagnostics.DiagnosticsWebSession diagSession = new Diagnostics.DiagnosticsWebSession();
            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.
                Uri reqUrl = null;

                if (!Uri.TryCreate(fullUrl, UriKind.RelativeOrAbsolute, out reqUrl))
                {
                    LoggerProxy.Default.Error("Failed to parse HTTP URL.");
                    return;
                }

                // Create a new request to send out upstream.
                var requestMsg = new HttpRequestMessage(new HttpMethod(context.Request.Method), fullUrl);
                diagSession.ClientRequestUri = fullUrl;

                if (context.Connection.ClientCertificate != null)
                {
                    // TODO - Handle client certificates.
                }

                // Build request headers into this, so we can pass the result to message begin/end callbacks.
                var reqHeaderBuilder = new StringBuilder();

                var failedInitialHeaders = new List <Tuple <string, string> >();

                bool   requestHasContentLengthHeader = false;
                bool   requestHasZeroContentLength   = false;
                string contentTypeValue = null;

                // Clone headers from the real client request to our upstream HTTP request.
                foreach (var hdr in context.Request.Headers)
                {
                    try
                    {
                        if (hdr.Key.Equals("Content-Length", StringComparison.OrdinalIgnoreCase))
                        {
                            requestHasZeroContentLength   = hdr.Value.ToString().Equals("0");
                            requestHasContentLengthHeader = true;
                        }
                    }
                    catch { }

                    try
                    {
                        if (hdr.Key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase))
                        {
                            contentTypeValue = hdr.Value.ToString();
                        }
                    }
                    catch { }

                    try
                    {
                        reqHeaderBuilder.AppendFormat("{0}: {1}\r\n", hdr.Key, hdr.Value.ToString());
                    }
                    catch { }

                    if (ForbiddenHttpHeaders.IsForbidden(hdr.Key))
                    {
                        continue;
                    }

                    // Content-Type is typically a header that's attached to a content body.. We have to add this manual check in here because
                    // we do some nasty reflection farther down which removes Content-Type from the global invalid headers list.
                    // In other words, we can't guarantee that Content-Type is going to be found as an invalid header according to .NET
                    // because we manipulate its internal invalid header list.
                    if (hdr.Key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase) || !requestMsg.Headers.TryAddWithoutValidation(hdr.Key, hdr.Value.ToString()))
                    {
                        string hName  = hdr.Key != null ? hdr.Key : string.Empty;
                        string hValue = hdr.Value.ToString() != null?hdr.Value.ToString() : string.Empty;

                        if (hName.Length > 0 && hValue.Length > 0)
                        {
                            failedInitialHeaders.Add(new Tuple <string, string>(hName, hValue));
                        }
                    }
                }

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

                // Add trailing CRLF to the request headers string.
                reqHeaderBuilder.Append("\r\n");

                diagSession.ClientRequestHeaders = reqHeaderBuilder.ToString();

                bool upstreamIsHttp1 = upstreamReqVersionMatch != null && upstreamReqVersionMatch.Major == 1 && upstreamReqVersionMatch.Minor == 0;

                // Since headers are complete at this stage, let's do our first call to message begin
                // for the request side.
                ProxyNextAction requestNextAction = ProxyNextAction.AllowAndIgnoreContentAndResponse;
                string          requestBlockResponseContentType = string.Empty;
                byte[]          requestBlockResponse            = null;
                m_msgBeginCb?.Invoke(reqUrl, reqHeaderBuilder.ToString(), m_nullBody, context.Request.IsHttps ? MessageType.Https : MessageType.Http, MessageDirection.Request, out requestNextAction, out requestBlockResponseContentType, out requestBlockResponse);

                if (requestNextAction == ProxyNextAction.DropConnection)
                {
                    if (requestBlockResponse != null)
                    {
                        // User wants to block this request with a custom response.
                        await DoCustomResponse(context, requestBlockResponseContentType, requestBlockResponse);
                    }
                    else
                    {
                        // User wants to block this request with a generic 204 response.
                        Do204(context);
                    }

                    return;
                }

                // Get the request body into memory.
                using (var ms = new MemoryStream())
                {
                    await Microsoft.AspNetCore.Http.Extensions.StreamCopyOperation.CopyToAsync(context.Request.Body, ms, null, 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 have a body and the user previously instructed us to give them the
                        // content, if any, for inspection.
                        if (requestNextAction == ProxyNextAction.AllowButRequestContentInspection)
                        {
                            // We'll now call the message end function for the request side.
                            bool shouldBlockRequest = false;
                            requestBlockResponseContentType = string.Empty;
                            requestBlockResponse            = null;
                            m_msgEndCb?.Invoke(reqUrl, reqHeaderBuilder.ToString(), requestBody, context.Request.IsHttps ? MessageType.Https : MessageType.Http, MessageDirection.Request, out shouldBlockRequest, out requestBlockResponseContentType, out requestBlockResponse);

                            if (shouldBlockRequest)
                            {
                                // User wants to block this request after inspecting the content.

                                if (requestBlockResponse != null)
                                {
                                    // User wants to block this request with a custom response.
                                    await DoCustomResponse(context, requestBlockResponseContentType, requestBlockResponse);
                                }
                                else
                                {
                                    // User wants to block this request with a generic 204 response.
                                    Do204(context);
                                }

                                return;
                            }
                        }

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

                if (contentTypeValue != null && requestMsg.Content == null)
                {
                    // FIXME: Parse out charset properly.
                    string[] contentTypeParts = contentTypeValue.Split(';');
                    for (int i = 1; i < contentTypeParts.Length; i++)
                    {
                        contentTypeParts[i] = contentTypeParts[i].Trim();
                    }

                    // This bit of reflection here is ugly-ugly-ugly. It fixes a bug where clients use and
                    // depend on the Content-Type header in a GET request to determine the return message
                    // from the server.
                    // Some alternate fixes.
                    // 1. Use .NET core (see farther down).
                    // 2. Re-implement in its entirety the HttpRequestMessage class, which is a bit overkill

                    // Note to the reader: NEVER EVER DO THIS IF THERE ARE OTHER OPTIONS.
                    var field = typeof(System.Net.Http.Headers.HttpRequestHeaders)
                                .GetField("invalidHeaders", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)
                                ?? typeof(System.Net.Http.Headers.HttpRequestHeaders)
                                .GetField("s_invalidHeaders", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);

                    if (field != null)
                    {
                        var invalidFields = (HashSet <string>)field.GetValue(null);

                        if (invalidFields.Contains("Content-Type"))
                        {
                            invalidFields.Remove("Content-Type");
                            LoggerProxy.Default.Info("Removing Content-Type from list of invalid headers.");
                        }
                    }
                    else
                    {
                        LoggerProxy.Default.Info("invalidHeaders fields not found.");
                    }

                    try
                    {
                        requestMsg.Headers.Add("Content-Type", contentTypeValue);
                    }
                    catch (Exception ex)
                    {
                        LoggerProxy.Default.Error(ex);
                    }

                    // This is an alternate fix, which works in .NET core 1.1 according to https://stackoverflow.com/a/44495081
                    //requestMsg.Content = new StringContent("", Encoding.UTF8, contentTypeParts[0]);
                }

                // Ensure that content type is set properly because ByteArrayContent and friends will
                // modify these fields.
                foreach (var et in failedInitialHeaders)
                {
                    if (et.Item1.Equals("Content-Type", StringComparison.OrdinalIgnoreCase) || !requestMsg.Headers.TryAddWithoutValidation(et.Item1, et.Item2))
                    {
                        if (requestMsg.Content != null)
                        {
                            if (!requestMsg.Content.Headers.TryAddWithoutValidation(et.Item1, et.Item2))
                            {
                                LoggerProxy.Default.Warn(string.Format("Failed to add HTTP header with key {0} and with value {1}.", et.Item1, et.Item2));
                            }
                        }
                    }
                }

                if (Diagnostics.Collector.IsDiagnosticsEnabled)
                {
                    diagSession.ServerRequestHeaders = requestMsg.Headers.ToString();
                }

                // Lets start sending the request upstream. We're going to as 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 ex)
                {
                    LoggerProxy.Default.Error(ex.GetType().Name);
                    LoggerProxy.Default.Error(ex); // Getting an error here on portal.worldpay.us

                    if (ex.InnerException is WebException && ex.InnerException.InnerException is System.Security.Authentication.AuthenticationException)
                    {
                        if (m_onBadCertificate != null)
                        {
                            string customResponseContentType = null;
                            byte[] customResponse            = null;

                            m_onBadCertificate(reqUrl, ex, out customResponseContentType, out customResponse);

                            if (customResponse != null)
                            {
                                await DoCustomResponse(context, customResponseContentType, customResponse);

                                return;
                            }
                            else
                            {
                                Do204(context);
                            }
                        }
                    }
                    else if (ex.InnerException is WebException)
                    {
                        var webException = ex.InnerException as WebException;

                        if (webException.Response != null)
                        {
                            diagSession.StatusCode = (int?)(webException.Response as HttpWebResponse)?.StatusCode ?? 0;
                        }
                    }
                }
                catch (TaskCanceledException e)
                {
                    // Just swallow these exceptions. There doesn't seem to be any ill effects coming from these anyway.
                }
                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.Headers.Clear();

                // Ensure our client's response status code is set to match ours.
                context.Response.StatusCode = (int)response.StatusCode;

                // Build response headers into this, so we can pass the result to message begin/end callbacks.
                var resHeaderBuilder = new StringBuilder();

                bool responseHasZeroContentLength = false;
                bool responseIsFixedLength        = false;

                // Iterate over all upstream response headers. Note that response.Content.Headers is
                // not ALL headers. Headers are split up into different properties according to
                // logical grouping.
                foreach (var hdr in response.Content.Headers)
                {
                    try
                    {
                        if (hdr.Key.Equals("Content-Length", StringComparison.OrdinalIgnoreCase) && hdr.Value.ToString().Equals("0"))
                        {
                            responseIsFixedLength = true;

                            if (hdr.Value.ToString().Equals("0"))
                            {
                                responseHasZeroContentLength = true;
                            }
                        }
                    }
                    catch { }

                    try
                    {
                        resHeaderBuilder.AppendFormat("{0}: {1}\r\n", hdr.Key, string.Join(", ", hdr.Value));
                    }
                    catch { }

                    if (ForbiddenHttpHeaders.IsForbidden(hdr.Key))
                    {
                        continue;
                    }

                    try
                    {
                        /*IEnumerable<string> valueEnumerable = response.Content.Headers.GetValues(hdr.Key);
                         * StringBuilder strBuilder = new StringBuilder();
                         * foreach(var value in valueEnumerable)
                         * {
                         *  strBuilder.Append($"'{value}',");
                         * }
                         *
                         * LoggerProxy.Default.Info($"{hdr.Key} ::: {strBuilder.ToString()}");*/

                        context.Response.Headers.Add(hdr.Key, new Microsoft.Extensions.Primitives.StringValues(hdr.Value.ToArray()));
                    }
                    catch (Exception e)
                    {
                        LoggerProxy.Default.Error(e);
                    }
                }

                // As mentioned above, headers are split up into different properties. We need to now
                // clone over the generic headers.
                foreach (var hdr in response.Headers)
                {
                    try
                    {
                        if (hdr.Key.Equals("Content-Length", StringComparison.OrdinalIgnoreCase) && hdr.Value.ToString().Equals("0"))
                        {
                            responseIsFixedLength = true;

                            if (hdr.Value.ToString().Equals("0"))
                            {
                                responseHasZeroContentLength = true;
                            }
                        }
                    }
                    catch { }

                    try
                    {
                        resHeaderBuilder.AppendFormat("{0}: {1}\r\n", hdr.Key, string.Join(", ", hdr.Value));
                    }
                    catch { }

                    if (ForbiddenHttpHeaders.IsForbidden(hdr.Key))
                    {
                        continue;
                    }

                    try
                    {
                        context.Response.Headers.Add(hdr.Key, new Microsoft.Extensions.Primitives.StringValues(hdr.Value.ToArray()));
                    }
                    catch (Exception e)
                    {
                        LoggerProxy.Default.Error(e);
                    }
                }

                resHeaderBuilder.Append("\r\n");

                diagSession.ServerResponseHeaders = resHeaderBuilder.ToString();
                // FIXME: Sadly this proxy doesn't have access to raw server headers?

                // Now that we have response headers, let's call the message begin handler for the
                // response. Unless of course, the user has asked us NOT to do this.
                if (requestNextAction != ProxyNextAction.AllowAndIgnoreContentAndResponse)
                {
                    ProxyNextAction responseNextAction = ProxyNextAction.AllowAndIgnoreContent;
                    string          responseBlockResponseContentType = string.Empty;
                    byte[]          responseBlockResponse            = null;

                    m_msgBeginCb?.Invoke(reqUrl, resHeaderBuilder.ToString(), m_nullBody, context.Request.IsHttps ? MessageType.Https : MessageType.Http, MessageDirection.Response, out responseNextAction, out responseBlockResponseContentType, out responseBlockResponse);

                    if (responseNextAction == ProxyNextAction.DropConnection)
                    {
                        if (responseBlockResponse != null)
                        {
                            // User wants to block this response with a custom response.
                            await DoCustomResponse(context, responseBlockResponseContentType, responseBlockResponse);
                        }
                        else
                        {
                            // User wants to block this response with a generic 204 response.
                            Do204(context);
                        }
                    }

                    if (responseNextAction == ProxyNextAction.AllowButRequestContentInspection)
                    {
                        using (var upstreamResponseStream = await response.Content.ReadAsStreamAsync())
                        {
                            using (var ms = new MemoryStream())
                            {
                                await Microsoft.AspNetCore.Http.Extensions.StreamCopyOperation.CopyToAsync(upstreamResponseStream, ms, null, context.RequestAborted);

                                var responseBody = ms.ToArray();
                                diagSession.ServerResponseBody = responseBody;

                                bool shouldBlockResponse = false;
                                responseBlockResponseContentType = string.Empty;
                                responseBlockResponse            = null;
                                m_msgEndCb?.Invoke(reqUrl, resHeaderBuilder.ToString(), responseBody, context.Request.IsHttps ? MessageType.Https : MessageType.Http, MessageDirection.Response, out shouldBlockResponse, out responseBlockResponseContentType, out responseBlockResponse);

                                if (shouldBlockResponse)
                                {
                                    if (responseBlockResponse != null)
                                    {
                                        // User wants to block this response with a custom response.
                                        await DoCustomResponse(context, responseBlockResponseContentType, responseBlockResponse);
                                    }
                                    else
                                    {
                                        // User wants to block this response with a generic 204 response.
                                        Do204(context);
                                    }

                                    return;
                                }

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

                // 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())
                {
                    if (!responseHasZeroContentLength && (upstreamIsHttp1 || responseIsFixedLength))
                    {
                        using (var ms = new MemoryStream())
                        {
                            await Microsoft.AspNetCore.Http.Extensions.StreamCopyOperation.CopyToAsync(responseStream, ms, null, context.RequestAborted);

                            var responseBody = ms.ToArray();
                            diagSession.ServerResponseBody = responseBody;

                            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");
                        }
                        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
            {
                diagSession.DateEnded = DateTime.Now;
                Diagnostics.Collector.ReportSession(diagSession);
            }
        }
        public override async Task Handle(HttpContext context)
        {
            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://"))
                {
                    fullUrl = "ws://" + fullUrl.Substring(7);
                }
                else if (fullUrl.StartsWith("https://"))
                {
                    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.
                Uri wsUri = null;

                if (!Uri.TryCreate(fullUrl, UriKind.RelativeOrAbsolute, out wsUri))
                {
                    LoggerProxy.Default.Error("Failed to parse websocket URI.");
                    return;
                }

                string subProtocol = context.Request.Headers[Constants.Headers.SecWebSocketProtocol];
                var    wsServer    = new ClientWebSocket();

                if (subProtocol != null && subProtocol.Length > 0)
                {
                    wsServer.Options.AddSubProtocol(subProtocol);
                }

                wsServer.Options.Cookies = new System.Net.CookieContainer();

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


                LoggerProxy.Default.Info(string.Format("Connecting websocket to {0}", wsUri.AbsoluteUri));

                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 (!ForbiddenHttpHeaders.IsForbidden(hdr.Key))
                    {
                        reqHeaderBuilder.AppendFormat("{0}: {1}\r\n", hdr.Key, hdr.Value.ToString());

                        try
                        {
                            if (!ForbiddenWsHeaders.IsForbidden(hdr.Key))
                            {
                                wsServer.Options.SetRequestHeader(hdr.Key, hdr.Value.ToString());
                                Console.WriteLine("Set Header: {0} ::: {1}", hdr.Key, hdr.Value.ToString());
                            }
                        }
                        catch (Exception hdrException)
                        {
                            Console.WriteLine("Failed Header: {0} ::: {1}", hdr.Key, hdr.Value.ToString());
                            LoggerProxy.Default.Error(hdrException);
                        }
                    }
                }

                reqHeaderBuilder.Append("\r\n");

                diagSession.ServerRequestHeaders = reqHeaderBuilder.ToString();

                string serverSubProtocol = null;

                await wsServer.ConnectAsync(wsUri, context.RequestAborted);

                if (wsServer.State == System.Net.WebSockets.WebSocketState.Open)
                {
                    serverSubProtocol = wsServer.SubProtocol;
                }
                else
                {
                }

                // Create, via acceptor, the client websocket. This is the local machine's websocket.
                var wsClient = await context.WebSockets.AcceptWebSocketAsync(serverSubProtocol);

                /*
                 * TODO - Much of this is presently lost to us because the socket
                 * we get from AcceptWebSocketAsync is a mostly internal implementation
                 * that is NOT a ClientWebSocket.
                 *
                 * Ideally we would xfer all such properties from the client to our proxy
                 * client socket.
                 *
                 * wsServer.Options.ClientCertificates = wsClient.Options.ClientCertificates;
                 * wsServer.Options.Cookies = wsClient.Options.Cookies;
                 * wsServer.Options.Credentials = wsClient.Options.Credentials;
                 * wsServer.Options.KeepAliveInterval = wsClient.Options.KeepAliveInterval;
                 * wsServer.Options.Proxy = wsClient.Options.Proxy;
                 * wsServer.Options.UseDefaultCredentials = wsClient.Options.UseDefaultCredentials;
                 */

                LoggerProxy.Default.Info(string.Format("Connecting websocket to {0}", wsUri.AbsoluteUri));

                ProxyNextAction nxtAction = ProxyNextAction.AllowAndIgnoreContentAndResponse;
                string          customResponseContentType = string.Empty;
                byte[]          customResponse            = null;
                m_msgBeginCb?.Invoke(wsUri, reqHeaderBuilder.ToString(), null, context.Request.IsHttps ? MessageType.SecureWebSocket : MessageType.WebSocket, MessageDirection.Request, out nxtAction, out customResponseContentType, out customResponse);

                switch (nxtAction)
                {
                case ProxyNextAction.DropConnection:
                {
                    if (customResponse != null)
                    {
                    }

                    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() =>
                {
                    var serverBuffer = new byte[1024 * 4];
                    var serverStatus = await wsServer.ReceiveAsync(new ArraySegment <byte>(serverBuffer), context.RequestAborted);

                    while (!serverStatus.CloseStatus.HasValue && !wsClient.CloseStatus.HasValue && !context.RequestAborted.IsCancellationRequested)
                    {
                        await wsClient.SendAsync(new ArraySegment <byte>(serverBuffer, 0, serverStatus.Count), serverStatus.MessageType, serverStatus.EndOfMessage, context.RequestAborted);

                        serverStatus = await wsServer.ReceiveAsync(new ArraySegment <byte>(serverBuffer), context.RequestAborted);
                    }

                    await wsServer.CloseAsync(serverStatus.CloseStatus.Value, serverStatus.CloseStatusDescription, context.RequestAborted);
                });

                // 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() =>
                {
                    var clientBuffer = new byte[1024 * 4];
                    var clientResult = await wsClient.ReceiveAsync(new ArraySegment <byte>(clientBuffer), context.RequestAborted);

                    while (!clientResult.CloseStatus.HasValue && !wsServer.CloseStatus.HasValue && !context.RequestAborted.IsCancellationRequested)
                    {
                        await wsServer.SendAsync(new ArraySegment <byte>(clientBuffer, 0, clientResult.Count), clientResult.MessageType, clientResult.EndOfMessage, context.RequestAborted);

                        clientResult = await wsClient.ReceiveAsync(new ArraySegment <byte>(clientBuffer), context.RequestAborted);
                    }

                    await wsClient.CloseAsync(clientResult.CloseStatus.Value, clientResult.CloseStatusDescription, context.RequestAborted);
                });

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