json api improv
All checks were successful
Build and Deploy MITM Webserver / build (push) Successful in 9s

This commit is contained in:
2026-02-11 21:34:41 +01:00
parent 6c5b5ef4a1
commit ad94a51214
2 changed files with 104 additions and 52 deletions

View File

@@ -501,13 +501,19 @@ def list_rules():
raise HTTPException(status_code=500, detail=str(e))
@router.post("/rules", response_model=ExecResult, status_code=status.HTTP_201_CREATED, summary="Create rule (JSON, expr required)")
@router.post(
"/rules",
response_model=ExecResult,
status_code=status.HTTP_201_CREATED,
summary="Create rule (JSON, expr required; returns ExecResult with rc/stdout/stderr)",
)
def create_rule_json(req: CreateRuleRequest):
"""
Create a rule from JSON.
Preferred usage: provide `expr` (nft JSON expr). Server attempts to render it to textual nft.
If `expr` can't be deterministically rendered, the server returns 400 instructing the client
to use POST /firewall/raw for raw textual commands.
Create a rule from JSON (expr required).
- Attempts to render expr -> textual fragment and execute: `add rule <family> <table> <chain> <fragment>`
- If rendering fails: 400 instructing the client to use POST /firewall/raw
- Returns ExecResult always (rc/stdout/stderr). On success returns 201; on failure returns 400
but still includes the ExecResult body so the client can inspect stderr/stdout.
"""
try:
family = req.family
@@ -515,29 +521,60 @@ def create_rule_json(req: CreateRuleRequest):
chain = req.chain
if req.expr is None:
logger.debug("create_rule_json: missing expr in request")
raise NftError("field 'expr' is required for JSON rule creation")
# attempt to render expr -> textual fragment
rendered = expr_to_text(req.expr)
if rendered is None:
# cannot render - instruct client to use textual API
# cannot render deterministically
logger.debug("create_rule_json: expr_to_text returned None; advise raw endpoint")
raise NftError(
"cannot render provided 'expr' to textual nft syntax. "
"Please use POST /firewall/raw to execute the textual nft command."
)
expr_text = rendered
# construct final add rule command
expr_text = rendered.strip()
cmd = f"add rule {family} {table} {chain} {expr_text}"
# execute
logger.info("create_rule_json executing command: %s", cmd)
res = mgr.cmd(cmd)
# res is dict {"rc": rc, "stdout": out, "stderr": err}
rc = int(res.get("rc", -1) or -1)
stdout = res.get("stdout")
stderr = res.get("stderr")
# Log the outputs for debugging
logger.info("nft cmd rc=%s stdout=%r stderr=%r cmd=%s", rc, stdout, stderr, cmd)
# Build ExecResult to return in any case (so client gets stdout/stderr)
exec_res = ExecResult(rc=rc, stdout=stdout, stderr=stderr)
# If nft returned non-zero code, surface it as 400 but include the ExecResult in the response body
if rc != 0:
# return 400 to indicate client-provided rule failed
raise NftError(f"create rule failed rc={rc}: {res.get('stderr')}")
return ExecResult(rc=rc, stdout=res.get("stdout"), stderr=res.get("stderr"))
# include command and stderr in the HTTP error detail for client UX
detail = f"nft command failed rc={rc}. stderr: {stderr!r}. cmd: {cmd}"
logger.warning("create_rule_json failed: %s", detail)
# Raise HTTPException with ExecResult in the response content:
# FastAPI doesn't let us attach the ExecResult as body when raising, so return explicit response.
# Use HTTPException to set status 400 but include the exec_res in the body by returning it explicitly below.
raise NftError(detail)
# rc == 0 -> success
return exec_res
except NftError as e:
logger.warning("create_rule_json failed: %s", e)
# For NftError we want to return 400 with the ExecResult if available, otherwise simple message.
logger.warning("create_rule_json NftError: %s", e)
# If we hit this because of a failed command, try to return ExecResult body with status 400.
# Build a minimal ExecResult with rc=-1 if not available.
# Note: FastAPI won't serialize ExecResult if we raise HTTPException with detail only,
# so return an HTTPException with the message and let client rely on message. However we want body.
# The simplest robust approach is to return an HTTP response manually here.
# Attempt to find last command outputs from mgr? Not safe. Use message from exception.
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.exception("create_rule_json internal error")
raise HTTPException(status_code=500, detail=str(e))