From 70fab7169ef1cd413aa699398869b9bb8fe318e0 Mon Sep 17 00:00:00 2001 From: ReturnFI <151555003+ReturnFI@users.noreply.github.com> Date: Thu, 16 Oct 2025 20:33:22 +0000 Subject: [PATCH] feat(api): Add full node configuration to web panel API --- .../webpanel/routers/api/v1/config/ip.py | 10 +++- .../routers/api/v1/schema/config/ip.py | 48 ++++++++++++++++++- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/core/scripts/webpanel/routers/api/v1/config/ip.py b/core/scripts/webpanel/routers/api/v1/config/ip.py index bcd8398..f981673 100644 --- a/core/scripts/webpanel/routers/api/v1/config/ip.py +++ b/core/scripts/webpanel/routers/api/v1/config/ip.py @@ -93,7 +93,14 @@ async def add_node(body: AddNodeBody): body: Request body containing the name and IP of the node. """ try: - cli_api.add_node(body.name, body.ip) + cli_api.add_node( + name=body.name, + ip=body.ip, + port=body.port, + sni=body.sni, + pinSHA256=body.pinSHA256, + obfs=body.obfs + ) return DetailResponse(detail=f"Node '{body.name}' added successfully.") except Exception as e: raise HTTPException(status_code=400, detail=str(e)) @@ -102,7 +109,6 @@ async def add_node(body: AddNodeBody): @router.post('/nodes/delete', response_model=DetailResponse, summary='Delete External Node') async def delete_node(body: DeleteNodeBody): """ - Deletes an external node from the configuration by its name. Args: diff --git a/core/scripts/webpanel/routers/api/v1/schema/config/ip.py b/core/scripts/webpanel/routers/api/v1/schema/config/ip.py index 538d2e7..73f293c 100644 --- a/core/scripts/webpanel/routers/api/v1/schema/config/ip.py +++ b/core/scripts/webpanel/routers/api/v1/schema/config/ip.py @@ -1,6 +1,7 @@ -from pydantic import BaseModel, field_validator, ValidationInfo -from ipaddress import IPv4Address, IPv6Address, ip_address +from pydantic import BaseModel, field_validator, Field +from ipaddress import ip_address import re +from typing import Optional def validate_ip_or_domain(v: str) -> str | None: if v is None or v.strip() in ['', 'None']: @@ -34,6 +35,10 @@ class EditInputBody(StatusResponse): class Node(BaseModel): name: str ip: str + port: Optional[int] = Field(default=None, ge=1, le=65535) + sni: Optional[str] = None + pinSHA256: Optional[str] = None + obfs: Optional[str] = None @field_validator('ip', mode='before') def check_node_ip(cls, v: str | None): @@ -41,6 +46,45 @@ class Node(BaseModel): raise ValueError("IP or Domain field cannot be empty.") return validate_ip_or_domain(v) + @field_validator('sni', mode='before') + def validate_sni_format(cls, v: str | None): + if v is None or not v.strip(): + return None + + v_stripped = v.strip() + + if "://" in v_stripped: + raise ValueError("SNI must not contain a protocol (e.g., http://).") + + try: + ip_address(v_stripped) + raise ValueError("SNI cannot be an IP address.") + except ValueError as e: + if "SNI cannot be an IP address" in str(e): + raise e + + domain_regex = re.compile( + r'^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$', + re.IGNORECASE + ) + if not domain_regex.match(v_stripped): + raise ValueError(f"'{v_stripped}' is not a valid domain name for SNI.") + + return v_stripped + + @field_validator('pinSHA256', mode='before') + def validate_pin_format(cls, v: str | None): + if v is None or not v.strip(): + return None + + v_stripped = v.strip().upper() + pin_regex = re.compile(r'^([0-9A-F]{2}:){31}[0-9A-F]{2}$') + + if not pin_regex.match(v_stripped): + raise ValueError("Invalid SHA256 pin format.") + + return v_stripped + class AddNodeBody(Node): pass