using System.Text; using Microsoft.AspNetCore.Mvc; namespace DeepDrftPublic.Controllers; /// /// Proxies the anonymous telemetry write endpoints (POST api/event/play / api/event/share) /// to DeepDrftAPI so the WASM client never makes a cross-origin request (Phase 16 §2.2). Mirrors /// 's idiom — the named "DeepDrft.API" client forwards the /// request upstream — but for a POST write: the small JSON body is buffered and relayed verbatim, and /// the upstream status (202 on success, 4xx on a rejected payload, 429 on rate limit) passes back so the /// beacon's fire-and-forget contract is preserved end to end. SSR never posts these — they originate /// from the browser player/share surfaces only. /// // A sendBeacon POST cannot attach a Blazor antiforgery token, so the telemetry write routes opt out // explicitly. They are anonymous, idempotent-enough fire-and-forget logging — there is no // state-changing user action to protect with CSRF tokens, and the upstream rate-limits by IP. [ApiController] [Route("api/event")] [IgnoreAntiforgeryToken] public class EventProxyController : ControllerBase { private readonly HttpClient _upstream; private readonly ILogger _logger; public EventProxyController(IHttpClientFactory httpClientFactory, ILogger logger) { _upstream = httpClientFactory.CreateClient("DeepDrft.API"); _logger = logger; } /// Proxies a play event upstream. Body is opaque JSON — validated by DeepDrftAPI, not here. [HttpPost("play")] public Task ForwardPlay(CancellationToken ct = default) => Forward("api/event/play", ct); /// Proxies a share event upstream. [HttpPost("share")] public Task ForwardShare(CancellationToken ct = default) => Forward("api/event/share", ct); private async Task Forward(string upstreamPath, CancellationToken ct) { // Buffer the small JSON body and relay it verbatim. Reading the raw body keeps the proxy // transparent — it does not deserialize or re-shape the payload, just forwards it. string body; using (var reader = new StreamReader(Request.Body, Encoding.UTF8)) { body = await reader.ReadToEndAsync(ct); } using var request = new HttpRequestMessage(HttpMethod.Post, upstreamPath) { Content = new StringContent(body, Encoding.UTF8, "application/json") }; // Forward the real client IP so DeepDrftAPI's per-IP rate limiter (Program.cs "events" policy) // partitions on individual listeners rather than the proxy host. Standard XFF chaining: relay // any inbound X-Forwarded-For from an upstream proxy (nginx), then append the connection IP // of the current hop (the browser → public host connection). DeepDrftAPI calls // UseForwardedHeaders() in production, which resolves the leftmost untrusted value in the // chain into Connection.RemoteIpAddress — which the rate limiter then keys on. var clientIp = HttpContext.Connection.RemoteIpAddress?.ToString(); if (clientIp is not null) { var existing = Request.Headers["X-Forwarded-For"].ToString(); var xff = string.IsNullOrEmpty(existing) ? clientIp : $"{existing}, {clientIp}"; request.Headers.TryAddWithoutValidation("X-Forwarded-For", xff); } HttpResponseMessage upstream; try { upstream = await _upstream.SendAsync(request, ct); } catch (Exception ex) { _logger.LogError(ex, "Upstream call to DeepDrftAPI {Path} failed", upstreamPath); return StatusCode(502, "Upstream unavailable"); } // Relay the upstream status as-is. Telemetry is fire-and-forget; the beacon never reads the // body, so there is nothing to relay beyond the code (202 / 400 / 429 / 5xx). using (upstream) { return StatusCode((int)upstream.StatusCode); } } }