feat(api): Add external node management endpoints

This commit is contained in:
Whispering Wind
2025-08-06 14:45:35 +03:30
committed by GitHub
parent 1ae8efa5f9
commit 05993d013d
5 changed files with 103 additions and 28 deletions

View File

@ -14,6 +14,7 @@ CONFIG_FILE = '/etc/hysteria/config.json'
CONFIG_ENV_FILE = '/etc/hysteria/.configs.env'
WEBPANEL_ENV_FILE = '/etc/hysteria/core/scripts/webpanel/.env'
NORMALSUB_ENV_FILE = '/etc/hysteria/core/scripts/normalsub/.env'
NODES_JSON_PATH = "/etc/hysteria/nodes.json"
class Command(Enum):

View File

@ -1,44 +1,43 @@
from fastapi import APIRouter, HTTPException
from ..schema.response import DetailResponse
import json
import os
from ..schema.config.ip import EditInputBody, StatusResponse
from ..schema.config.ip import (
EditInputBody,
StatusResponse,
AddNodeBody,
DeleteNodeBody,
NodeListResponse
)
import cli_api
router = APIRouter()
@router.get('/get', response_model=StatusResponse, summary='Get IP Status')
@router.get('/get', response_model=StatusResponse, summary='Get Local Server IP Status')
async def get_ip_api():
"""
Retrieves the current status of IP addresses.
Retrieves the current status of the main server's IP addresses.
Returns:
StatusResponse: A response model containing the current IP address details.
Raises:
HTTPException: If the IP status is not available (404) or if there is an error processing the request (400).
"""
try:
ipv4, ipv6 = cli_api.get_ip_address()
if ipv4 or ipv6:
return StatusResponse(ipv4=ipv4, ipv6=ipv6) # type: ignore
raise HTTPException(status_code=404, detail='IP status not available.')
return StatusResponse(ipv4=ipv4, ipv6=ipv6)
except Exception as e:
raise HTTPException(status_code=400, detail=f'Error: {str(e)}')
@router.get('/add', response_model=DetailResponse, summary='Add IP')
@router.get('/add', response_model=DetailResponse, summary='Detect and Add Local Server IP')
async def add_ip_api():
"""
Adds the auto-detected IP addresses to the .configs.env file.
Returns:
A DetailResponse with a message indicating the IP addresses were added successfully.
Raises:
HTTPException: if an error occurs while adding the IP addresses.
"""
try:
cli_api.add_ip_address()
@ -47,19 +46,13 @@ async def add_ip_api():
raise HTTPException(status_code=400, detail=f'Error: {str(e)}')
@router.post('/edit', response_model=DetailResponse, summary='Edit IP')
@router.post('/edit', response_model=DetailResponse, summary='Edit Local Server IP')
async def edit_ip_api(body: EditInputBody):
"""
Edits the IP addresses in the .configs.env file.
Edits the main server's IP addresses in the .configs.env file.
Args:
body: An instance of EditInputBody containing the new IPv4 and/or IPv6 addresses.
Returns:
A DetailResponse with a message indicating the IP addresses were edited successfully.
Raises:
HTTPException: if an error occurs while editing the IP addresses.
"""
try:
if not body.ipv4 and not body.ipv6:
@ -69,3 +62,54 @@ async def edit_ip_api(body: EditInputBody):
return DetailResponse(detail='IP address edited successfully.')
except Exception as e:
raise HTTPException(status_code=400, detail=f'Error: {str(e)}')
@router.get('/nodes', response_model=NodeListResponse, summary='Get All External Nodes')
async def get_all_nodes():
"""
Retrieves the list of all configured external nodes.
Returns:
A list of node objects, each containing a name and an IP.
"""
if not os.path.exists(cli_api.NODES_JSON_PATH):
return []
try:
with open(cli_api.NODES_JSON_PATH, 'r') as f:
content = f.read()
if not content:
return []
return json.loads(content)
except (json.JSONDecodeError, IOError) as e:
raise HTTPException(status_code=500, detail=f"Failed to read or parse nodes file: {e}")
@router.post('/nodes/add', response_model=DetailResponse, summary='Add External Node')
async def add_node(body: AddNodeBody):
"""
Adds a new external node to the configuration.
Args:
body: Request body containing the name and IP of the node.
"""
try:
cli_api.add_node(body.name, body.ip)
return DetailResponse(detail=f"Node '{body.name}' added successfully.")
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@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:
body: Request body containing the name of the node to delete.
"""
try:
cli_api.delete_node(body.name)
return DetailResponse(detail=f"Node '{body.name}' deleted successfully.")
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))

View File

@ -22,3 +22,25 @@ class StatusResponse(BaseModel):
class EditInputBody(StatusResponse):
pass
class Node(BaseModel):
name: str
ip: str
@field_validator('ip', mode='before')
def check_ip(cls, v: str, info: ValidationInfo):
if v is None:
raise ValueError("IP cannot be None")
try:
ip_address(v)
return v
except ValueError:
raise ValueError(f"'{v}' is not a valid IPv4 or IPv6 address")
class AddNodeBody(Node):
pass
class DeleteNodeBody(BaseModel):
name: str
NodeListResponse = list[Node]

View File

@ -1,4 +1,4 @@
from typing import Optional
from typing import Optional, List
from pydantic import BaseModel, RootModel
@ -38,8 +38,14 @@ class EditUserInputBody(BaseModel):
renew_creation_date: bool = False
blocked: bool = False
class NodeUri(BaseModel):
name: str
uri: str
class UserUriResponse(BaseModel):
username: str
ipv4: str | None = None
ipv6: str | None = None
normal_sub: str | None = None
ipv4: Optional[str] = None
ipv6: Optional[str] = None
nodes: Optional[List[NodeUri]] = []
normal_sub: Optional[str] = None
error: Optional[str] = None

View File

@ -178,10 +178,12 @@ async def show_user_uri_api(username: str):
uri_data_list = cli_api.show_user_uri_json([username])
if not uri_data_list:
raise HTTPException(status_code=404, detail=f'URI for user {username} not found.')
uri_data = uri_data_list[0]
if uri_data.get('error'):
raise HTTPException(status_code=404, detail=f"{uri_data['error']}")
return uri_data
return UserUriResponse(**uri_data)
except cli_api.ScriptNotFoundError as e:
raise HTTPException(status_code=500, detail=f'Server script error: {str(e)}')
except cli_api.CommandExecutionError as e: