From 69abb95140ed7883ddd35d83128a3e93940b34ed Mon Sep 17 00:00:00 2001 From: malmert Date: Mon, 9 Feb 2026 20:36:49 +0100 Subject: [PATCH] fix json encoding --- backend/src/api/nft_manager.py | 62 ++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 7 deletions(-) diff --git a/backend/src/api/nft_manager.py b/backend/src/api/nft_manager.py index 366ca32..5056bc5 100644 --- a/backend/src/api/nft_manager.py +++ b/backend/src/api/nft_manager.py @@ -3,7 +3,7 @@ Unrestricted nftables FastAPI service using textual nft commands only. Endpoints (high level): - GET /firewall/rules -> return textual ruleset (raw nft output) + GET /firewall/rules -> return textual ruleset (raw nft output or parsed JSON when nft returned JSON) POST /firewall/rules -> execute a textual nft command (convenience) DELETE /firewall/rules/{handle} -> delete rule by handle via textual nft command POST /firewall/raw -> execute arbitrary textual nft command (executes) @@ -22,6 +22,7 @@ from fastapi import FastAPI, APIRouter, HTTPException, status from pydantic import BaseModel, Field import logging import re +import json # libnftables (we call textual commands through its .cmd() method) from nftables import Nftables # type: ignore @@ -47,7 +48,7 @@ class NftManager: def __init__(self) -> None: self.nft = Nftables() try: - # prefer seeing JSON when python-nftables prints it, but we only use .cmd() below + # Prefer JSON output where python-nftables may produce it, but we still treat stdout textually. self.nft.set_json_output(True) except Exception: logger.debug("set_json_output not available or ignored") @@ -98,20 +99,67 @@ class RawCmdRequest(BaseModel): class RulesetOut(BaseModel): - ruleset: str + # ruleset can be either a parsed JSON object (dict/list) or a string + ruleset: Any + + +# ---------- PREVIEW helpers (unchanged) ---------- +def parse_text_cmd_summary(cmd: str) -> Dict[str, Any]: + summary: Dict[str, Any] = {"operation": None, "family": None, "table": None, "chain": None, "remainder": cmd} + s = cmd.strip() + tokens = s.split() + if len(tokens) >= 1: + summary["operation"] = tokens[0].lower() + m = re.match(r'^(add|insert|delete|replace)\s+rule\s+(\S+)\s+(\S+)\s+(\S+)\b', s, flags=re.I) + if m: + summary["operation"] = m.group(1).lower() + summary["family"] = m.group(2) + summary["table"] = m.group(3) + summary["chain"] = m.group(4) + summary["remainder"] = s[m.end():].strip() + return summary + m2 = re.match(r'^(delete)\s+rule\s+(\S+)\s+(\S+)\s+(\S+)\s+handle\s+(\d+)', s, flags=re.I) + if m2: + summary["operation"] = m2.group(1).lower() + summary["family"] = m2.group(2) + summary["table"] = m2.group(3) + summary["chain"] = m2.group(4) + summary["remainder"] = f"handle {m2.group(5)}" + return summary + try: + idx = next(i for i,t in enumerate(tokens) if t.lower() == "rule") + if len(tokens) > idx + 3: + summary["family"] = tokens[idx+1] + summary["table"] = tokens[idx+2] + summary["chain"] = tokens[idx+3] + summary["remainder"] = " ".join(tokens[idx+4:]) if len(tokens) > idx+4 else "" + except StopIteration: + pass + return summary + # ---------- Routes ---------- -# GET /firewall/rules -> textual nft ruleset output +# GET /firewall/rules -> textual nft ruleset output or parsed JSON when applicable @router.get("/rules", response_model=RulesetOut) def list_rules(): """ - Returns the textual nft ruleset output (as a single string). - Use clients to parse as needed. + Returns the textual nft ruleset output (as a string) or native JSON if nft returned JSON. + Clients should handle both cases. """ try: text = mgr.list_rules_text() - return {"ruleset": text} + if text is None: + return {"ruleset": None} + s = text.strip() + # Try to interpret as JSON. nft will produce JSON when set_json_output(True) is active. + try: + parsed = json.loads(s) + # Return parsed JSON (will be serialized as JSON by FastAPI) + return {"ruleset": parsed} + except json.JSONDecodeError: + # Not JSON — return raw text + return {"ruleset": text} except NftError as e: logger.exception("list_rules failed") raise HTTPException(status_code=500, detail=str(e))