This commit is contained in:
StellaOps Bot
2025-12-18 20:37:27 +02:00
parent f85d53888c
commit 6410a6d082
17 changed files with 454 additions and 131 deletions

View File

@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Routing;
using StellaOps.Replay.Core;
using StellaOps.Scanner.ProofSpine;
using StellaOps.Scanner.WebService.Contracts;
using StellaOps.Scanner.WebService.Serialization;
using StellaOps.Scanner.WebService.Security;
namespace StellaOps.Scanner.WebService.Endpoints;
@@ -17,6 +18,7 @@ internal static class ProofSpineEndpoints
spines.MapGet("/{spineId}", HandleGetSpineAsync)
.WithName("scanner.spines.get")
.Produces<ProofSpineResponseDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status200OK, contentType: CborNegotiation.ContentType)
.Produces(StatusCodes.Status404NotFound)
.RequireAuthorization(ScannerPolicies.ScansRead);
@@ -24,15 +26,18 @@ internal static class ProofSpineEndpoints
scans.MapGet("/{scanId}/spines", HandleListSpinesAsync)
.WithName("scanner.spines.list-by-scan")
.Produces<ProofSpineListResponseDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status200OK, contentType: CborNegotiation.ContentType)
.RequireAuthorization(ScannerPolicies.ScansRead);
}
private static async Task<IResult> HandleGetSpineAsync(
HttpRequest request,
string spineId,
IProofSpineRepository repository,
ProofSpineVerifier verifier,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
ArgumentNullException.ThrowIfNull(repository);
ArgumentNullException.ThrowIfNull(verifier);
@@ -93,19 +98,36 @@ internal static class ProofSpineEndpoints
}
};
if (CborNegotiation.AcceptsCbor(request))
{
return Results.Bytes(
DeterministicCborSerializer.Serialize(dto),
contentType: CborNegotiation.ContentType);
}
return Results.Ok(dto);
}
private static async Task<IResult> HandleListSpinesAsync(
HttpRequest request,
string scanId,
IProofSpineRepository repository,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
ArgumentNullException.ThrowIfNull(repository);
if (string.IsNullOrWhiteSpace(scanId))
{
return Results.Ok(new ProofSpineListResponseDto { Items = Array.Empty<ProofSpineSummaryDto>(), Total = 0 });
var empty = new ProofSpineListResponseDto { Items = Array.Empty<ProofSpineSummaryDto>(), Total = 0 };
if (CborNegotiation.AcceptsCbor(request))
{
return Results.Bytes(
DeterministicCborSerializer.Serialize(empty),
contentType: CborNegotiation.ContentType);
}
return Results.Ok(empty);
}
var summaries = await repository.GetSummariesByScanRunAsync(scanId, cancellationToken).ConfigureAwait(false);
@@ -119,11 +141,20 @@ internal static class ProofSpineEndpoints
CreatedAt = summary.CreatedAt
}).ToArray();
return Results.Ok(new ProofSpineListResponseDto
var response = new ProofSpineListResponseDto
{
Items = items,
Total = items.Length
});
};
if (CborNegotiation.AcceptsCbor(request))
{
return Results.Bytes(
DeterministicCborSerializer.Serialize(response),
contentType: CborNegotiation.ContentType);
}
return Results.Ok(response);
}
private static DsseEnvelopeDto MapEnvelope(DsseEnvelope envelope)
@@ -163,4 +194,3 @@ internal static class ProofSpineEndpoints
return "/" + trimmed;
}
}

View File

@@ -0,0 +1,29 @@
using Microsoft.AspNetCore.Http;
namespace StellaOps.Scanner.WebService.Serialization;
internal static class CborNegotiation
{
public const string ContentType = "application/cbor";
public static bool AcceptsCbor(HttpRequest request)
{
ArgumentNullException.ThrowIfNull(request);
if (!request.Headers.TryGetValue("Accept", out var values))
{
return false;
}
foreach (var value in values)
{
if (value.Contains(ContentType, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,108 @@
using System.Collections.Generic;
using System.Formats.Cbor;
using System.Text;
using System.Text.Json;
namespace StellaOps.Scanner.WebService.Serialization;
internal static class DeterministicCborSerializer
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
{
WriteIndented = false
};
public static byte[] Serialize<T>(T value)
{
using var document = JsonSerializer.SerializeToDocument(value, JsonOptions);
var writer = new CborWriter(CborConformanceMode.Canonical);
WriteElement(writer, document.RootElement);
return writer.Encode();
}
private static void WriteElement(CborWriter writer, JsonElement element)
{
switch (element.ValueKind)
{
case JsonValueKind.Object:
WriteObject(writer, element);
return;
case JsonValueKind.Array:
writer.WriteStartArray(element.GetArrayLength());
foreach (var item in element.EnumerateArray())
{
WriteElement(writer, item);
}
writer.WriteEndArray();
return;
case JsonValueKind.String:
writer.WriteTextString(element.GetString() ?? string.Empty);
return;
case JsonValueKind.Number:
if (element.TryGetInt64(out var int64))
{
writer.WriteInt64(int64);
return;
}
writer.WriteDouble(element.GetDouble());
return;
case JsonValueKind.True:
writer.WriteBoolean(true);
return;
case JsonValueKind.False:
writer.WriteBoolean(false);
return;
case JsonValueKind.Null:
case JsonValueKind.Undefined:
writer.WriteNull();
return;
default:
writer.WriteNull();
return;
}
}
private static void WriteObject(CborWriter writer, JsonElement element)
{
var properties = new List<(byte[] KeyBytes, string Key, JsonElement Value)>();
foreach (var property in element.EnumerateObject())
{
var keyBytes = Encoding.UTF8.GetBytes(property.Name);
properties.Add((keyBytes, property.Name, property.Value));
}
properties.Sort(static (left, right) =>
{
var lengthCompare = left.KeyBytes.Length.CompareTo(right.KeyBytes.Length);
if (lengthCompare != 0)
{
return lengthCompare;
}
for (var i = 0; i < left.KeyBytes.Length; i++)
{
var byteCompare = left.KeyBytes[i].CompareTo(right.KeyBytes[i]);
if (byteCompare != 0)
{
return byteCompare;
}
}
return 0;
});
writer.WriteStartMap(properties.Count);
foreach (var (_, key, value) in properties)
{
writer.WriteTextString(key);
WriteElement(writer, value);
}
writer.WriteEndMap();
}
}