Skip to content

Instantly share code, notes, and snippets.

@jcolebrand
Created May 10, 2022 23:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jcolebrand/2d34fcf1a80ed6e2ca1d9e4e87ed0b25 to your computer and use it in GitHub Desktop.
Save jcolebrand/2d34fcf1a80ed6e2ca1d9e4e87ed0b25 to your computer and use it in GitHub Desktop.
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