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