Files
qbit-printing/dev/native-auth/client/FixtureHttp.cs
james.maesandCodex 1a0964ef23 chore: enforce owned source copyright notices (#25)
* chore(standards): preserve owned-source copyright notices

Add accurate repository copyright and license comments to 50 owned source/build/configuration files. Preserve existing holders, upstream wrappers, legal payloads and schema/data exemptions. Exact inverse-byte reconstruction and applicable parser/token/AST checks prove comment-only changes; existing relevant regressions pass.

Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>

* chore: enforce owned source copyright notices

Check real leading comments against explicit repository provenance and retain existing native style gates.

Co-Authored-By: Codex GPT-6 <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>

---------

Signed-off-by: James Maes <james@kof22.com>
Co-authored-by: Codex (GPT-6) <noreply@openai.com>
2026-11-01 12:41:00 -06:00

88 lines
4.6 KiB
C#

// Copyright 2026 Kingsrook, LLC
// SPDX-License-Identifier: Apache-2.0
using System.Net;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json.Nodes;
namespace Printing.NativeAuth;
/// <summary>Small bounded fixture transport; no redirects, proxy or response-body diagnostics.</summary>
internal sealed class FixtureHttp : IDisposable
{
internal sealed record Reply(HttpStatusCode Status, JsonObject? Json, Uri? Location);
internal sealed record TraceEntry(string Kind, int Status, bool HasQuery, string? LocationKind, bool LocationHasQuery);
internal static List<TraceEntry> Trace { get; } = [];
internal static string? LastRequestKind { get; private set; }
internal static int? LastStatus { get; private set; }
private readonly HttpClient _client;
private readonly CookieContainer _cookies = new();
private readonly Uri _origin;
private readonly string? _adminToken;
internal FixtureHttp(Uri origin, bool cookies = false, string? adminToken = null)
{
_origin = origin;
_adminToken = adminToken;
_client = new HttpClient(new HttpClientHandler { AllowAutoRedirect = false, UseProxy = false, UseCookies = cookies, CookieContainer = _cookies })
{
Timeout = TimeSpan.FromSeconds(15),
MaxResponseContentBufferSize = 524288,
};
}
internal async Task<Reply> SendAsync(string method, Uri uri, JsonObject? body, CancellationToken cancellationToken)
{
if(uri.Scheme != "https" || uri.GetLeftPart(UriPartial.Authority) != _origin.GetLeftPart(UriPartial.Authority))
{
throw new InvalidOperationException("A fixture request left its configured HTTPS origin.");
}
using var request = new HttpRequestMessage(new HttpMethod(method), uri);
// Fixed classifications only: never retain a query, callback, identifier or body.
LastRequestKind = Classify(uri, method);
LastStatus = null;
if(_adminToken is not null)
{
if(!uri.AbsolutePath.StartsWith("/api/v3/", StringComparison.Ordinal)) { throw new InvalidOperationException("Administrative credential scope refused."); }
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _adminToken);
}
if(body is not null)
{
request.Content = new StringContent(body.ToJsonString(), Encoding.UTF8, "application/json");
request.Headers.Add("Origin", _origin.GetLeftPart(UriPartial.Authority));
request.Headers.Referrer = _origin;
Cookie? csrf = _cookies.GetCookies(_origin).Cast<Cookie>().SingleOrDefault(cookie => cookie.Name == "authentik_csrf");
csrf ??= _cookies.GetCookies(_origin).Cast<Cookie>().SingleOrDefault(cookie => cookie.Name == "csrftoken");
if(csrf is not null) { request.Headers.Add("X-authentik-CSRF", csrf.Value); }
}
using HttpResponseMessage response = await _client.SendAsync(request, cancellationToken);
LastStatus = (int)response.StatusCode;
Uri? location = response.Headers.Location is null ? null : new Uri(_origin, response.Headers.Location);
if(Trace.Count == 32) { Trace.RemoveAt(0); }
Trace.Add(new TraceEntry(LastRequestKind, (int)response.StatusCode, uri.Query.Length != 0,
location is null ? null : Classify(location, "GET"), location?.Query.Length > 0));
string content = await response.Content.ReadAsStringAsync(cancellationToken);
JsonObject? json = response.Content.Headers.ContentType?.MediaType == "application/json" && content.Length != 0
? JsonNode.Parse(content)?.AsObject() : null;
return new Reply(response.StatusCode, json, response.Headers.Location);
}
internal Task<Reply> SendAsync(string method, string path, JsonObject? body, CancellationToken cancellationToken)
=> SendAsync(method, new Uri(_origin, path), body, cancellationToken);
private static string Classify(Uri uri, string method) => uri.Host == "callback.printing.internal" ? "callback"
: uri.AbsolutePath.StartsWith("/application/o/authorize", StringComparison.Ordinal) ? "provider-authorization"
: uri.AbsolutePath.StartsWith("/if/flow/", StringComparison.Ordinal) ? "provider-flow-page"
: uri.AbsolutePath.StartsWith("/api/v3/flows/executor/", StringComparison.Ordinal) ? "provider-flow-executor-" + method
: "fixture-api";
internal static JsonObject RequireJson(Reply reply, HttpStatusCode expected)
{
if(reply.Status != expected || reply.Json is null) { throw new InvalidOperationException("Unexpected fixture API status or document shape."); }
return reply.Json;
}
public void Dispose() => _client.Dispose();
}