fix json encoding
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 11s
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 11s
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
Unrestricted nftables FastAPI service using textual nft commands only.
|
Unrestricted nftables FastAPI service using textual nft commands only.
|
||||||
|
|
||||||
Endpoints (high level):
|
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)
|
POST /firewall/rules -> execute a textual nft command (convenience)
|
||||||
DELETE /firewall/rules/{handle} -> delete rule by handle via textual nft command
|
DELETE /firewall/rules/{handle} -> delete rule by handle via textual nft command
|
||||||
POST /firewall/raw -> execute arbitrary textual nft command (executes)
|
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
|
from pydantic import BaseModel, Field
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import json
|
||||||
|
|
||||||
# libnftables (we call textual commands through its .cmd() method)
|
# libnftables (we call textual commands through its .cmd() method)
|
||||||
from nftables import Nftables # type: ignore
|
from nftables import Nftables # type: ignore
|
||||||
@@ -47,7 +48,7 @@ class NftManager:
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.nft = Nftables()
|
self.nft = Nftables()
|
||||||
try:
|
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)
|
self.nft.set_json_output(True)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.debug("set_json_output not available or ignored")
|
logger.debug("set_json_output not available or ignored")
|
||||||
@@ -98,20 +99,67 @@ class RawCmdRequest(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class RulesetOut(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 ----------
|
# ---------- 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)
|
@router.get("/rules", response_model=RulesetOut)
|
||||||
def list_rules():
|
def list_rules():
|
||||||
"""
|
"""
|
||||||
Returns the textual nft ruleset output (as a single string).
|
Returns the textual nft ruleset output (as a string) or native JSON if nft returned JSON.
|
||||||
Use clients to parse as needed.
|
Clients should handle both cases.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
text = mgr.list_rules_text()
|
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:
|
except NftError as e:
|
||||||
logger.exception("list_rules failed")
|
logger.exception("list_rules failed")
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|||||||
Reference in New Issue
Block a user