Created
May 10, 2022 23:31
-
-
Save jcolebrand/2d34fcf1a80ed6e2ca1d9e4e87ed0b25 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace Tooling.WebApi.Common.Utility | |
{ | |
using System; | |
using System.IO; | |
using System.Linq; | |
using System.Net; | |
using System.Net.Http; | |
using System.Text; | |
using System.Threading.Tasks; | |
using Microsoft.AspNetCore.Http; | |
using Microsoft.Extensions.Logging; | |
/// <summary> | |
/// Utility class for extensions specifically for HTTP needs | |
/// </summary> | |
public static class HttpExtensions | |
{ | |
/// <summary> | |
/// Track headers we don't want to log | |
/// </summary> | |
private static string[] ExcludedHeaderNames = new[] { "password", "authorize", "authorization", "x-api-key", "api-key" }; | |
/// <summary> | |
/// Use to log all DNS entries associated with a hostname | |
/// </summary> | |
/// <param name="logger">A logger element to go after</param> | |
/// <param name="hostname">The hostname to log at</param> | |
/// <returns><placeholder>A <see cref="Task"/> representing the asynchronous operation.</placeholder></returns> | |
public static async Task LogHostDns(this ILogger logger, string hostname) | |
{ | |
logger.LogDebug($"Resolve DNS for hostname"); | |
var hostEntry = await Dns.GetHostEntryAsync(hostname); | |
foreach (var address in hostEntry.AddressList) | |
{ | |
logger.LogDebug($"{hostname}: Found IpAddress: {address.AddressFamily} address: {address}"); | |
} | |
foreach (var alias in hostEntry.Aliases) | |
{ | |
logger.LogDebug($"{hostname}: Found Alias: {alias}"); | |
} | |
} | |
/// <summary> | |
/// Get the very least details of an HTTP request to a string | |
/// </summary> | |
/// <param name="request">A given HttpRequest</param> | |
/// <returns>A string version of the request</returns> | |
public static string GetScantDetails(this HttpRequest request) | |
{ | |
string baseUrl = $"{request.Scheme}://{request.Host}{request.Path}{request.QueryString.Value}"; | |
return $"{request.Protocol} {request.Method} {baseUrl}"; | |
} | |
/// <summary> | |
/// Log a request as best we can to a logger | |
/// </summary> | |
/// <typeparam name="T">The type of logger being used</typeparam> | |
/// <param name="request">Request instance to apply to</param> | |
/// <param name="logger">The logger instance to write to</param> | |
/// <param name="bodyText">The text being sent in this request</param> | |
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> | |
public static async Task<string> LogRequest<T>(this HttpWebRequest request, ILogger<T> logger, string bodyText) | |
{ | |
var sb = new StringBuilder(); | |
sb.AppendLine($"Http Request Information:"); | |
sb.AppendLine($"{request.Method} {request.RequestUri.AbsolutePath} {request.ProtocolVersion}"); | |
if (!string.IsNullOrWhiteSpace(request.RequestUri.Query)) | |
{ | |
sb.AppendLine($"QueryString: {request.RequestUri.Query} "); | |
} | |
var scannableHeaders = request | |
.Headers | |
.AllKeys | |
.Where(headerKey => | |
!ExcludedHeaderNames | |
.Any(excludedHeader => | |
excludedHeader.Equals(headerKey, StringComparison.InvariantCultureIgnoreCase))); | |
foreach (var header in scannableHeaders) | |
{ | |
sb.AppendLine($"{header}::{request.Headers[header.ToString()]}"); | |
} | |
// Add a blank line to make it look like an HTTP Request | |
sb.AppendLine(string.Empty); | |
sb.AppendLine(bodyText); | |
logger.LogInformation(sb.ToString()); | |
return bodyText; | |
} | |
/// <summary> | |
/// Log a request as best we can to a logger | |
/// </summary> | |
/// <typeparam name="T">The type of logger being used</typeparam> | |
/// <param name="request">Request instance to apply to</param> | |
/// <param name="logger">The logger instance to write to</param> | |
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> | |
public static async Task LogRequest<T>(this HttpRequestMessage request, ILogger<T> logger) | |
{ | |
var sb = new StringBuilder(); | |
sb.AppendLine($"Http Request Information:"); | |
sb.AppendLine($"{request.Method} {request.RequestUri.AbsoluteUri} HTTP/{request.Version}"); | |
if (!string.IsNullOrWhiteSpace(request.RequestUri.Query)) | |
{ | |
sb.AppendLine($"QueryString: {request.RequestUri.Query} "); | |
} | |
foreach (var header in request.Headers.Where(whereHeader => | |
!ExcludedHeaderNames | |
.Any(excludedHeader => | |
excludedHeader.Equals(whereHeader.Key, StringComparison.InvariantCultureIgnoreCase)))) | |
{ | |
sb.AppendLine($"{header.Key}::{header.Value}"); | |
} | |
// Add a blank line to make it look like an HTTP Request | |
sb.AppendLine(string.Empty); | |
sb.AppendLine(await request.Content.ReadAsStringAsync()); | |
logger.LogInformation(sb.ToString()); | |
return; | |
} | |
/// <summary> | |
/// Log a request as best we can to a logger | |
/// </summary> | |
/// <typeparam name="T">The type of logger being used</typeparam> | |
/// <param name="request">Request instance to apply to</param> | |
/// <param name="logger">The logger instance to write to</param> | |
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> | |
public static async Task<string> LogRequest<T>(this HttpRequest request, ILogger<T> logger) | |
{ | |
request.EnableBuffering(); | |
await using var requestStream = new MemoryStream(); | |
await request.Body.CopyToAsync(requestStream); | |
string bodyText = string.Empty; | |
try | |
{ | |
bodyText = ReadStreamInChunks(requestStream); | |
} | |
catch (Exception ex) | |
{ | |
// If we see this in a log we should update the type of exception here to catch on this read | |
// It's likely a streamexception of some sort. | |
logger.LogTrace(ex, "Could not parse the body object"); | |
bodyText = "Read was corrupted, could not parse the body"; | |
} | |
finally | |
{ | |
request.Body.Position = 0; | |
} | |
var sb = new StringBuilder(); | |
sb.AppendLine($"Http Request Information:"); | |
sb.AppendLine($"{request.Method} {request.Path.Value} {request.Protocol}"); | |
if (!string.IsNullOrWhiteSpace(request.QueryString.ToString())) | |
{ | |
sb.AppendLine($"QueryString: {request.QueryString} "); | |
} | |
foreach (var header in request.Headers.Where(whereHeader => | |
!ExcludedHeaderNames | |
.Any(excludedHeader => | |
excludedHeader.Equals(whereHeader.Key, StringComparison.InvariantCultureIgnoreCase)))) | |
{ | |
sb.AppendLine($"{header.Key}::{header.Value}"); | |
} | |
// Add a blank line to make it look like an HTTP Request | |
sb.AppendLine(string.Empty); | |
sb.AppendLine(bodyText); | |
logger.LogInformation(sb.ToString()); | |
return bodyText; | |
} | |
/// <summary> | |
/// Log a request as best we can to a logger | |
/// </summary> | |
/// <typeparam name="T">The type of logger being used</typeparam> | |
/// <param name="response">Request instance to apply to</param> | |
/// <param name="logger">The logger instance to write to</param> | |
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> | |
public static async Task<string> LogResponse<T>(this WebResponse response, ILogger<T> logger) | |
{ | |
await using var requestStream = new MemoryStream(); | |
await response.GetResponseStream().CopyToAsync(requestStream); | |
string bodyText = string.Empty; | |
try | |
{ | |
bodyText = ReadStreamInChunks(requestStream); | |
} | |
catch (Exception ex) | |
{ | |
// If we see this in a log we should update the type of exception here to catch on this read | |
// It's likely a streamexception of some sort. | |
logger.LogTrace(ex, "Could not parse the body object"); | |
bodyText = "Read was corrupted, could not parse the body"; | |
} | |
var sb = new StringBuilder(); | |
sb.AppendLine($"Http Response Information:"); | |
var scannableHeaders = response | |
.Headers | |
.AllKeys | |
.Where(headerKey => | |
!ExcludedHeaderNames | |
.Any(excludedHeader => | |
excludedHeader.Equals(headerKey, StringComparison.InvariantCultureIgnoreCase))); | |
foreach (var header in scannableHeaders) | |
{ | |
sb.AppendLine($"{header}::{response.Headers[header.ToString()]}"); | |
} | |
// Add a blank line to make it look like an HTTP Response | |
sb.AppendLine(string.Empty); | |
sb.AppendLine(bodyText); | |
logger.LogInformation(sb.ToString()); | |
return bodyText; | |
} | |
private static string ReadStreamInChunks(Stream stream) | |
{ | |
const int readChunkBufferLength = 4096; | |
stream.Seek(0, SeekOrigin.Begin); | |
using var textWriter = new StringWriter(); | |
using var reader = new StreamReader(stream); | |
var readChunk = new char[readChunkBufferLength]; | |
int readChunkLength; | |
do | |
{ | |
readChunkLength = reader.ReadBlock(readChunk, 0, readChunkBufferLength); | |
textWriter.Write(readChunk, 0, readChunkLength); | |
} | |
while (readChunkLength > 0); | |
return textWriter.ToString(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment