Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ tr:hover {
background: #f1f1f1;
}

.clickable-row {
cursor: pointer;
}

/* =========================
Section Titles
========================= */
Expand Down Expand Up @@ -274,6 +278,10 @@ pre {
color: #ddd;
}

.payload-hidden {
display: none;
}

.diff-badge {
background: #4a1717;
color: #ff8a8a;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

const text = await res.text();

setCompareResult(`<b style="color:red">${text || 'Compare failed'}</b>`);
setCompareResult(`<b style="color:red">${escapeHtml(text || 'Compare failed')}</b>`);

return;
}
Expand All @@ -33,10 +33,10 @@

setCompareResult(renderCompare(result));

} catch {
} catch (error) {

setCompareResult(
`<b style="color:red">${error}</b>`
`<b style="color:red">${escapeHtml(error.message || 'Compare failed')}</b>`
);
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,9 @@ if (clearBtn) {
location.reload();
});
}

document.querySelectorAll(".clickable-row[data-url]").forEach(row => {
row.addEventListener("click", () => {
window.location.assign(row.dataset.url);
});
});
15 changes: 7 additions & 8 deletions DebugProbe.AspNetCore/DebugProbe.AspNetCore.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,15 @@
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="Resources\**\*.html" />
<EmbeddedResource Include="Resources\**\*.css" />
<EmbeddedResource Include="Resources\js\*.js" />
<EmbeddedResource Include="Assets\debugprobe_logo_white_transparent.png" />
<EmbeddedResource Include="Assets\debugprobe_favicon.ico" />
<EmbeddedResource Include="Assets\**\*.html" />
<EmbeddedResource Include="Assets\**\*.css" />
<EmbeddedResource Include="Assets\**\*.js" />
<EmbeddedResource Include="Assets\**\*.png" />
<EmbeddedResource Include="Assets\**\*.ico" />
</ItemGroup>

<ItemGroup>
<None Remove="Resources\**\*.html" />
<None Remove="Resources\**\*.css" />
<None Remove="Resources\js\*.js" />
<None Remove="Assets\**\*" />
</ItemGroup>

<ItemGroup>
Expand All @@ -57,4 +55,5 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

</Project>
49 changes: 26 additions & 23 deletions DebugProbe.AspNetCore/Extensions/DebugProbeExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System.Net.Http.Json;
using DebugProbe.AspNetCore.Internal;
using DebugProbe.AspNetCore.Internal.Compare;
using DebugProbe.AspNetCore.Internal.Rendering;
using DebugProbe.AspNetCore.Internal.Resources;
using DebugProbe.AspNetCore.Internal.Utils;
using DebugProbe.AspNetCore.Middleware;
using DebugProbe.AspNetCore.Models;
using DebugProbe.AspNetCore.Options;
Expand All @@ -12,7 +15,10 @@ namespace DebugProbe.AspNetCore.Extensions;

public static class DebugProbeExtensions
{
private static readonly HttpClient Http = new();
private static readonly HttpClient Http = new()
{
Timeout = TimeSpan.FromSeconds(5)
};

public static IServiceCollection AddDebugProbe(
this IServiceCollection services,
Expand Down Expand Up @@ -66,7 +72,9 @@ public static IApplicationBuilder UseDebugProbe(this IApplicationBuilder app)
await ctx.Response.WriteAsync(html);
}).ExcludeFromDescription();

webApp.MapGet("/debug/compare/{id}", async (string id, string baseUrl, string remoteTraceId, DebugEntryStore store) =>
webApp.MapGet("/debug/compare/{id}", async (string id, string baseUrl, string remoteTraceId,
DebugEntryStore store,
DebugProbeOptions options) =>
{
var localEnvironment = store.Environment;
var localEntry = store.Get(id);
Expand All @@ -75,14 +83,23 @@ public static IApplicationBuilder UseDebugProbe(this IApplicationBuilder app)
return Results.NotFound("Local trace not found");
}

if (!Guid.TryParse(remoteTraceId, out _))
{
return Results.BadRequest("Invalid remote trace id");
}

var normalizedBaseUrl = baseUrl.TrimEnd('/');
var validation = await CompareUrlValidator.ValidateCompareBaseUrlAsync(baseUrl, options);

if (!validation.IsValid)
{
return Results.BadRequest(validation.Error);
}

var remoteEnvironmentUrl =
$"{normalizedBaseUrl}/debug/environment";
new Uri(validation.BaseUri!, "/debug/environment");

var remoteEntryUrl =
$"{normalizedBaseUrl}/debug/json/{remoteTraceId}";
new Uri(validation.BaseUri!, $"/debug/json/{remoteTraceId}");

DebugEntry? remoteEntry;
DebugEnvironment? remoteEnvironment;
Expand Down Expand Up @@ -161,30 +178,16 @@ public static IApplicationBuilder UseDebugProbe(this IApplicationBuilder app)
}).ExcludeFromDescription();

webApp.Map("/debug/logo.png", ctx =>
WriteEmbeddedAsset(ctx, "DebugProbe.AspNetCore.Assets.debugprobe_logo_white_transparent.png", "image/png")
EmbeddedAssetWriter.WriteEmbeddedAsset(ctx, "DebugProbe.AspNetCore.Assets.images.debugprobe_logo_white_transparent.png", "image/png")
).ExcludeFromDescription();

webApp.Map("/debug/favicon.ico", ctx =>
WriteEmbeddedAsset(ctx, "DebugProbe.AspNetCore.Assets.debugprobe_favicon.ico", "image/x-icon")
EmbeddedAssetWriter.WriteEmbeddedAsset(ctx, "DebugProbe.AspNetCore.Assets.images.debugprobe_favicon.ico", "image/x-icon")
).ExcludeFromDescription();
}

return app;
}

private static async Task WriteEmbeddedAsset(HttpContext ctx, string resourceName, string contentType)
{
ctx.Response.ContentType = contentType;

var assembly = typeof(DebugProbeMiddleware).Assembly;
using var stream = assembly.GetManifestResourceStream(resourceName);

if (stream is null)
{
ctx.Response.StatusCode = 404;
return;
}

await stream.CopyToAsync(ctx.Response.Body);
}

}
102 changes: 102 additions & 0 deletions DebugProbe.AspNetCore/Internal/Compare/CompareUrlValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using DebugProbe.AspNetCore.Options;

namespace DebugProbe.AspNetCore.Internal.Compare;

internal static class CompareUrlValidator
{
public static async Task<(bool IsValid, Uri? BaseUri, string Error)> ValidateCompareBaseUrlAsync(string baseUrl, DebugProbeOptions options)
{
if (!Uri.TryCreate(baseUrl, UriKind.Absolute, out var parsed))
{
return (false, null, "Invalid compare server URL");
}

if (parsed.Scheme != Uri.UriSchemeHttp && parsed.Scheme != Uri.UriSchemeHttps)
{
return (false, null, "Compare server URL must use http or https");
}

if (!string.IsNullOrEmpty(parsed.UserInfo))
{
return (false, null, "Compare server URL cannot include credentials");
}

System.Net.IPAddress[] addresses;

try
{
addresses = System.Net.IPAddress.TryParse(parsed.Host, out var ipAddress)
? [ipAddress]
: await System.Net.Dns.GetHostAddressesAsync(parsed.DnsSafeHost);
}
catch
{
return (false, null, "Failed to resolve compare server host");
}

if (!options.AllowLocalCompareTargets)
{
if (IsLocalHostName(parsed.Host))
{
return (false, null, "Compare server URL cannot target localhost");
}

if (addresses.Length == 0 || addresses.Any(IsPrivateOrLocalAddress))
{
return (false, null, "Compare server URL cannot target local or private network addresses");
}
}

return (true, new Uri(parsed.GetLeftPart(UriPartial.Authority)), string.Empty);
}

private static bool IsLocalHostName(string host)
{
return string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) ||
string.Equals(host, Environment.MachineName, StringComparison.OrdinalIgnoreCase);
}

private static bool IsPrivateOrLocalAddress(System.Net.IPAddress address)
{
if (System.Net.IPAddress.IsLoopback(address) ||
System.Net.IPAddress.Any.Equals(address) ||
System.Net.IPAddress.None.Equals(address) ||
System.Net.IPAddress.Broadcast.Equals(address))
{
return true;
}

if (address.IsIPv4MappedToIPv6)
{
address = address.MapToIPv4();
}

if (address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
{
var bytes = address.GetAddressBytes();

return bytes[0] == 10 ||
bytes[0] == 127 ||
bytes[0] == 0 ||
bytes[0] == 169 && bytes[1] == 254 ||
bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31 ||
bytes[0] == 192 && bytes[1] == 168 ||
bytes[0] == 100 && bytes[1] >= 64 && bytes[1] <= 127 ||
bytes[0] == 198 && (bytes[1] == 18 || bytes[1] == 19);
}

if (address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6)
{
var bytes = address.GetAddressBytes();

return address.IsIPv6LinkLocal ||
address.IsIPv6SiteLocal ||
address.IsIPv6Multicast ||
System.Net.IPAddress.IPv6Any.Equals(address) ||
System.Net.IPAddress.IPv6None.Equals(address) ||
bytes[0] is >= 0xfc and <= 0xfd;
}

return true;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System.Text.Json;
using DebugProbe.AspNetCore.Models;

namespace DebugProbe.AspNetCore.Internal;
namespace DebugProbe.AspNetCore.Internal.Compare;

/// <summary>
/// Compares two DebugEntry instances and produces a list of differences,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System.Net;
using DebugProbe.AspNetCore.Internal.Resources;
using DebugProbe.AspNetCore.Internal.Utils;
using DebugProbe.AspNetCore.Models;

namespace DebugProbe.AspNetCore.Internal;
namespace DebugProbe.AspNetCore.Internal.Rendering;

/// <summary>
/// Renders DebugProbe UI pages (layout, index, details) using embedded HTML templates.
Expand All @@ -23,7 +25,7 @@ public static string BuildLayout(string content)
public static string RenderIndexPage(List<DebugEntry> items)
{
var rows = string.Join("", items.Select(x => $@"
<tr onclick=""window.location='/debug/{x.Id}'"" style=""cursor:pointer"">
<tr data-url=""/debug/{Encode(x.Id)}"" class=""clickable-row"">
<td>{x.Timestamp:HH:mm:ss}</td>
<td>{Encode(x.Method)}</td>
<td>{Encode(x.Path)}</td>
Expand Down Expand Up @@ -95,7 +97,7 @@ private static string Encode(string? value)

private static string GetStatusText(int statusCode)
{
return $"{statusCode} {((HttpStatusCode)statusCode)}";
return $"{statusCode} {(HttpStatusCode)statusCode}";
}

private static string GetStatusClass(int statusCode)
Expand All @@ -112,6 +114,11 @@ private static string GetStatusClass(int statusCode)

private static string GetPayloadType(string value)
{
if (IsCapturePlaceholder(value))
{
return string.Empty;
}

if (string.IsNullOrWhiteSpace(value))
{
return "Empty";
Expand All @@ -127,6 +134,11 @@ private static string GetPayloadType(string value)

private static string GetPayloadTypeClass(string value)
{
if (IsCapturePlaceholder(value))
{
return "payload-hidden";
}

if (string.IsNullOrWhiteSpace(value))
{
return "payload-empty";
Expand All @@ -140,6 +152,11 @@ private static string GetPayloadTypeClass(string value)
return LooksLikeJson(value) ? "payload-invalid-json" : "payload-text";
}

private static bool IsCapturePlaceholder(string value)
{
return string.Equals(value, "[Body too large]", StringComparison.OrdinalIgnoreCase);
}

private static bool LooksLikeJson(string value)
{
var trimmed = value.TrimStart();
Expand Down
23 changes: 23 additions & 0 deletions DebugProbe.AspNetCore/Internal/Resources/EmbeddedAssetWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using DebugProbe.AspNetCore.Middleware;
using Microsoft.AspNetCore.Http;

namespace DebugProbe.AspNetCore.Internal.Resources;

internal static class EmbeddedAssetWriter
{
public static async Task WriteEmbeddedAsset(HttpContext ctx, string resourceName, string contentType)
{
ctx.Response.ContentType = contentType;

var assembly = typeof(DebugProbeMiddleware).Assembly;
using var stream = assembly.GetManifestResourceStream(resourceName);

if (stream is null)
{
ctx.Response.StatusCode = 404;
return;
}

await stream.CopyToAsync(ctx.Response.Body);
}
}
Loading