using System.Buffers; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using SeaHaven.Services.Interfaces; namespace Api.SeaHavenIndustries.Controllers { [ApiController] [AllowAnonymous] [Route("api/webhooks/work-orders")] public sealed class WorkOrderWebhookController : ControllerBase { private readonly IWorkOrderWebhookService _service; public WorkOrderWebhookController(IWorkOrderWebhookService service) { _service = service; } [HttpPost] [Consumes("application/json")] public async Task Receive(CancellationToken cancellationToken) { if (!HasSupportedContentType(Request.ContentType)) return StatusCode(StatusCodes.Status415UnsupportedMediaType); if (Request.Headers.ContentEncoding.Count > 0 && !string.Equals( Request.Headers.ContentEncoding.ToString(), "identity", StringComparison.OrdinalIgnoreCase)) return StatusCode(StatusCodes.Status415UnsupportedMediaType); var maxBytes = _service.MaximumBodyBytes; if (Request.ContentLength > maxBytes) return StatusCode(StatusCodes.Status413PayloadTooLarge); var body = await ReadBoundedBodyAsync(Request.Body, maxBytes, cancellationToken); if (body == null) return StatusCode(StatusCodes.Status413PayloadTooLarge); var result = await _service.ProcessAsync( new WorkOrderWebhookRequest( SingleHeader("X-SH-Timestamp"), SingleHeader("X-SH-Key-Id"), SingleHeader("X-SH-Signature"), body), cancellationToken); return result.Status switch { WorkOrderWebhookStatus.Applied => Ok(new { status = "accepted", state_mutation_skipped = result.StateMutationSkipped }), WorkOrderWebhookStatus.Duplicate => Ok(new { status = "duplicate" }), WorkOrderWebhookStatus.Unauthorized => Unauthorized(new { error = "unauthorized" }), WorkOrderWebhookStatus.Disabled => StatusCode( StatusCodes.Status503ServiceUnavailable, new { error = "webhook unavailable" }), WorkOrderWebhookStatus.InvalidEnvelope => BadRequest(new { error = "invalid envelope" }), WorkOrderWebhookStatus.HashConflict => Conflict(new { error = "delivery conflict" }), WorkOrderWebhookStatus.Unavailable => StatusCode( StatusCodes.Status503ServiceUnavailable, new { error = "webhook unavailable" }), _ => StatusCode(StatusCodes.Status503ServiceUnavailable) }; } private string? SingleHeader(string name) { var values = Request.Headers[name]; return values.Count == 1 ? values[0] : null; } private static bool HasSupportedContentType(string? contentType) { return MediaTypeHeaderValue.TryParse(contentType, out var parsed) && string.Equals( parsed.MediaType.Value, "application/json", StringComparison.OrdinalIgnoreCase); } private static async Task ReadBoundedBodyAsync( Stream source, int maxBytes, CancellationToken cancellationToken) { var rented = ArrayPool.Shared.Rent(81920); try { using var destination = new MemoryStream(Math.Min(maxBytes, 81920)); while (true) { var read = await source.ReadAsync(rented.AsMemory(0, rented.Length), cancellationToken); if (read == 0) return destination.ToArray(); if (destination.Length + read > maxBytes) return null; await destination.WriteAsync(rented.AsMemory(0, read), cancellationToken); } } finally { ArrayPool.Shared.Return(rented, clearArray: true); } } } }