From ad92f6f9dda847d5dd0cdd4993a69190bfdf8f3f Mon Sep 17 00:00:00 2001 From: andrewlalis Date: Fri, 24 Oct 2025 10:34:20 -0400 Subject: [PATCH] Add internal transfer label, convert to filters for request handling. --- finnow-api/source/api_mapping.d | 54 ++++++++++++--- web-app/src/pages/TransactionPage.vue | 96 +++++++-------------------- 2 files changed, 68 insertions(+), 82 deletions(-) diff --git a/finnow-api/source/api_mapping.d b/finnow-api/source/api_mapping.d index 02ce249..9f6301b 100644 --- a/finnow-api/source/api_mapping.d +++ b/finnow-api/source/api_mapping.d @@ -3,6 +3,7 @@ module api_mapping; import handy_http_primitives; import handy_http_handlers.path_handler; import handy_http_handlers.filtered_handler; +import slf4d; /// The base path to all API endpoints. private const API_PATH = "/api"; @@ -101,7 +102,15 @@ HttpRequestHandler mapApiHandlers(string webOrigin) { a )); - return new CorsHandler(h, webOrigin); + // Build the main handler into a filter chain: + return new FilteredHandler( + [ + cast(HttpRequestFilter) new CorsFilter(webOrigin), + cast(HttpRequestFilter) new ContentLengthFilter(), + cast(HttpRequestFilter) new ExceptionHandlingFilter() + ], + h + ); } private void getStatus(ref ServerHttpRequest request, ref ServerHttpResponse response) { @@ -136,26 +145,53 @@ private void map( handler.addMapping(method, API_PATH ~ subPath, HttpRequestHandler.of(fn)); } -private class CorsHandler : HttpRequestHandler { - private HttpRequestHandler handler; +private class CorsFilter : HttpRequestFilter { private string webOrigin; - this(HttpRequestHandler handler, string webOrigin) { - this.handler = handler; + this(string webOrigin) { this.webOrigin = webOrigin; } - - void handle(ref ServerHttpRequest request, ref ServerHttpResponse response) { + + void doFilter(ref ServerHttpRequest request, ref ServerHttpResponse response, FilterChain filterChain) { response.headers.add("Access-Control-Allow-Origin", webOrigin); response.headers.add("Access-Control-Allow-Methods", "*"); response.headers.add("Access-Control-Allow-Headers", "Authorization, Content-Type"); + filterChain.doFilter(request, response); + } +} + +private class ContentLengthFilter : HttpRequestFilter { + const MAX_LENGTH = 1024 * 1024 * 20; // 2MB limit + + void doFilter(ref ServerHttpRequest request, ref ServerHttpResponse response, FilterChain filterChain) { + if ("Content-Length" in request.headers) { + ulong contentLength = request.getHeaderAs!ulong("Content-Length"); + if (contentLength > MAX_LENGTH) { + warnF!"Received request with content length of %d, larger than max allowed %d bytes."( + contentLength, + MAX_LENGTH + ); + import std.conv; + response.status = HttpStatus.PAYLOAD_TOO_LARGE; + response.writeBodyString( + "Request body is too large. Must be at most " ~ MAX_LENGTH.to!string ~ " bytes." + ); + return; // Don't propagate the filter. + } + } + + filterChain.doFilter(request, response); + } +} + +private class ExceptionHandlingFilter : HttpRequestFilter { + void doFilter(ref ServerHttpRequest request, ref ServerHttpResponse response, FilterChain filterChain) { try { - this.handler.handle(request, response); + filterChain.doFilter(request, response); } catch (HttpStatusException e) { response.status = e.status; response.writeBodyString(e.message.idup); } catch (Exception e) { - import slf4d; error(e); response.status = HttpStatus.INTERNAL_SERVER_ERROR; response.writeBodyString("An error occurred: " ~ e.msg); diff --git a/web-app/src/pages/TransactionPage.vue b/web-app/src/pages/TransactionPage.vue index d8a1dd8..fd9cc22 100644 --- a/web-app/src/pages/TransactionPage.vue +++ b/web-app/src/pages/TransactionPage.vue @@ -87,58 +87,34 @@ async function deleteTransaction() { }