From 090d11711584fc5c01b243d44aee429ff48b0a8b Mon Sep 17 00:00:00 2001 From: Domingo Dirutigliano Date: Thu, 3 Sep 2026 18:42:16 +0200 Subject: [PATCH 1/3] Keep a dragged firewall rule at its measured width The Card carried Mantine's `w="100%"` prop, whose style props land in the style attribute after anything passed via `style`, so it overrode the pixel width dnd measures at lift time. On a `position: fixed` element `100%` resolves against the viewport rather than the list, so grabbing a rule snapped the card from 734px to 798px and pushed it past the right edge. Moving the width into `style`, ahead of the dnd spread, leaves normal flow untouched and lets the measured width win while dragging. --- frontend/src/pages/Firewall/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/Firewall/index.tsx b/frontend/src/pages/Firewall/index.tsx index 7815c0a..cfd2c9b 100644 --- a/frontend/src/pages/Firewall/index.tsx +++ b/frontend/src/pages/Firewall/index.tsx @@ -199,9 +199,11 @@ export const Firewall = () => { radius="md" p="md" mb="md" - w="100%" bg="transparent" style={{ + // not the `w` prop: Mantine's style props would beat the width dnd measures, and + // `100%` against a `position: fixed` card resolves to the viewport, not the list + width: '100%', transition: 'border-color 0.2s ease', // dnd puts the drag transform/transition in here: it has to win over our defaults ...provided.draggableProps.style, From ba2703d4bf384329e7eadc7bce538af906819b7e Mon Sep 17 00:00:00 2001 From: Domingo Dirutigliano Date: Thu, 3 Sep 2026 18:42:28 +0200 Subject: [PATCH 2/3] Stop a non-string filter code from crashing the nfproxy page `genericapi` ran every response body through `JSON.parse`, falling back to the raw text only when that threw. The nfproxy filter source is served as a PlainTextResponse, so a filter file that happens to be valid JSON came back as a number/array/object. Mantine's CodeHighlight then calls `code.trim()` on it, which throws and takes the whole page down with it. `42`, `"abc"`, `[1,2]` and `{"a":1}` are all both valid JSON and valid Python, so they pass the upload validation and leave the service page unreachable. Responses declaring text/plain are no longer parsed, and the render site type-checks the code before handing it to CodeHighlight. --- frontend/src/js/utils.tsx | 4 ++++ frontend/src/pages/NFProxy/ServiceDetails.tsx | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/js/utils.tsx b/frontend/src/js/utils.tsx index 0b25afc..396a577 100644 --- a/frontend/src/js/utils.tsx +++ b/frontend/src/js/utils.tsx @@ -111,7 +111,11 @@ export async function genericapi(method:string,path:string,data:any = undefined, const errorDefault = res.statusText return res.json().then( res => reject(getErrorMessageFromServerResponse(res, errorDefault)) ).catch( _err => reject(errorDefault)) } + // text/plain bodies (the nfproxy filter source) stay strings: a source file that + // happens to be valid JSON must not be parsed into a number/array/object + const isPlainText = (res.headers.get("content-type") ?? "").startsWith("text/plain") res.text().then(t => { + if (isPlainText) return resolve(t) try{ resolve(JSON.parse(t)) }catch(e){ diff --git a/frontend/src/pages/NFProxy/ServiceDetails.tsx b/frontend/src/pages/NFProxy/ServiceDetails.tsx index 2c3afe7..fb5f4f9 100644 --- a/frontend/src/pages/NFProxy/ServiceDetails.tsx +++ b/frontend/src/pages/NFProxy/ServiceDetails.tsx @@ -207,7 +207,7 @@ export default function ServiceDetailsNFProxy() { - {filterCode.data ? <> + {typeof filterCode.data === "string" && filterCode.data ? <> Filter code From 6140fcbe813c1faf7b43b0352b909cb4d4ee0339 Mon Sep 17 00:00:00 2001 From: Domingo Dirutigliano Date: Thu, 3 Sep 2026 18:42:37 +0200 Subject: [PATCH 3/3] Align the demo backend with the real nfproxy API The stubbed `GET .../code` answered with `{ code }` while the real endpoint is a PlainTextResponse returning the source, which is what crashed the demo service page, and the save route was registered as POST where the real API and the frontend both use PUT. The seeded sample also carried a `strip_debug_header` filter that mutated `HttpRequest.headers`. That silently does nothing: `headers` is a read-only property over the parsed message, and nothing re-serialises it onto the wire (a mangle forwards `current_pkt.raw_packet`, which the dict never touches). Dropped it, leaving the one filter that behaves as shown, and brought the filter counts and the packet counters back in line with it. --- frontend/src/js/demo.ts | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/frontend/src/js/demo.ts b/frontend/src/js/demo.ts index 118bab6..548e3be 100644 --- a/frontend/src/js/demo.ts +++ b/frontend/src/js/demo.ts @@ -27,13 +27,6 @@ def block_path_traversal(packet: HttpRequest): if ".." in packet.url: return REJECT return ACCEPT - - -@pyfilter -def strip_debug_header(packet: HttpRequest): - if "X-Debug" in packet.headers: - del packet.headers["X-Debug"] - return ACCEPT ` // ------------------------------------------------------------------ seed state @@ -65,14 +58,12 @@ const state = { }, nfproxy: { services: [ - { service_id: svcProxy, name: "shop-api", status: "active", port: 9000, proto: "tcp", ip_int: "10.60.3.1", n_filters: 2, edited_packets: 3401, blocked_packets: 762, fail_open: true, target_type: "internal", tls_stream_id: null }, - { service_id: svcQuiz, name: "quiz", status: "stop", port: 1337, proto: "tcp", ip_int: "10.60.3.1", n_filters: 2, edited_packets: 0, blocked_packets: 0, fail_open: false, target_type: "internal", tls_stream_id: null }, + { service_id: svcProxy, name: "shop-api", status: "active", port: 9000, proto: "tcp", ip_int: "10.60.3.1", n_filters: 1, edited_packets: 0, blocked_packets: 641, fail_open: true, target_type: "internal", tls_stream_id: null }, + { service_id: svcQuiz, name: "quiz", status: "stop", port: 1337, proto: "tcp", ip_int: "10.60.3.1", n_filters: 1, edited_packets: 0, blocked_packets: 0, fail_open: false, target_type: "internal", tls_stream_id: null }, ] as Json[], pyfilters: [ { name: "block_path_traversal", service_id: svcProxy, blocked_packets: 641, edited_packets: 0, active: true }, - { name: "strip_debug_header", service_id: svcProxy, blocked_packets: 0, edited_packets: 3401, active: true }, { name: "block_path_traversal", service_id: svcQuiz, blocked_packets: 0, edited_packets: 0, active: true }, - { name: "strip_debug_header", service_id: svcQuiz, blocked_packets: 0, edited_packets: 0, active: false }, ] as Json[], code: { [svcProxy]: SAMPLE_FILTER, [svcQuiz]: SAMPLE_FILTER } as Record, tls: {} as Record, @@ -125,7 +116,6 @@ const startTicker = () => { } for (const s of state.nfproxy.services) { if (s.status !== "active") continue - s.edited_packets += Math.floor(Math.random() * 12) s.blocked_packets += Math.random() < 0.4 ? 1 : 0 changed = true } @@ -252,8 +242,9 @@ const routes: [string, RegExp, Handler][] = [ ["PUT", /^nfproxy\/services\/([^/]+)\/tls-config$/, () => ok], ["GET", /^nfproxy\/services\/([^/]+)\/tls-config$/, () => ({ tls_enabled: false, tls_cert: null, tls_key: null })], ["GET", /^nfproxy\/services\/([^/]+)\/pyfilters$/, m => state.nfproxy.pyfilters.filter(f => f.service_id === m[1])], - ["GET", /^nfproxy\/services\/([^/]+)\/code$/, m => ({ code: state.nfproxy.code[m[1]] ?? "" })], - ["POST", /^nfproxy\/services\/([^/]+)\/code$/, (m, b) => { + // the real endpoint is a PlainTextResponse: it returns the source, not an object + ["GET", /^nfproxy\/services\/([^/]+)\/code$/, m => state.nfproxy.code[m[1]] ?? ""], + ["PUT", /^nfproxy\/services\/([^/]+)\/code$/, (m, b) => { state.nfproxy.code[m[1]] = b?.code ?? "" emit(["nfproxy"]); return ok }],