feat(api): Add external node management endpoints
This commit is contained in:
@ -14,6 +14,7 @@ CONFIG_FILE = '/etc/hysteria/config.json'
|
|||||||
CONFIG_ENV_FILE = '/etc/hysteria/.configs.env'
|
CONFIG_ENV_FILE = '/etc/hysteria/.configs.env'
|
||||||
WEBPANEL_ENV_FILE = '/etc/hysteria/core/scripts/webpanel/.env'
|
WEBPANEL_ENV_FILE = '/etc/hysteria/core/scripts/webpanel/.env'
|
||||||
NORMALSUB_ENV_FILE = '/etc/hysteria/core/scripts/normalsub/.env'
|
NORMALSUB_ENV_FILE = '/etc/hysteria/core/scripts/normalsub/.env'
|
||||||
|
NODES_JSON_PATH = "/etc/hysteria/nodes.json"
|
||||||
|
|
||||||
|
|
||||||
class Command(Enum):
|
class Command(Enum):
|
||||||
|
|||||||
@ -1,44 +1,43 @@
|
|||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
from ..schema.response import DetailResponse
|
from ..schema.response import DetailResponse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
from ..schema.config.ip import (
|
||||||
from ..schema.config.ip import EditInputBody, StatusResponse
|
EditInputBody,
|
||||||
|
StatusResponse,
|
||||||
|
AddNodeBody,
|
||||||
|
DeleteNodeBody,
|
||||||
|
NodeListResponse
|
||||||
|
)
|
||||||
import cli_api
|
import cli_api
|
||||||
|
|
||||||
router = APIRouter()
|
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():
|
async def get_ip_api():
|
||||||
"""
|
"""
|
||||||
Retrieves the current status of IP addresses.
|
Retrieves the current status of the main server's IP addresses.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
StatusResponse: A response model containing the current IP address details.
|
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:
|
try:
|
||||||
|
|
||||||
ipv4, ipv6 = cli_api.get_ip_address()
|
ipv4, ipv6 = cli_api.get_ip_address()
|
||||||
if ipv4 or ipv6:
|
return StatusResponse(ipv4=ipv4, ipv6=ipv6)
|
||||||
return StatusResponse(ipv4=ipv4, ipv6=ipv6) # type: ignore
|
|
||||||
raise HTTPException(status_code=404, detail='IP status not available.')
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=400, detail=f'Error: {str(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():
|
async def add_ip_api():
|
||||||
"""
|
"""
|
||||||
Adds the auto-detected IP addresses to the .configs.env file.
|
Adds the auto-detected IP addresses to the .configs.env file.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A DetailResponse with a message indicating the IP addresses were added successfully.
|
A DetailResponse with a message indicating the IP addresses were added successfully.
|
||||||
|
|
||||||
Raises:
|
|
||||||
HTTPException: if an error occurs while adding the IP addresses.
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
cli_api.add_ip_address()
|
cli_api.add_ip_address()
|
||||||
@ -47,19 +46,13 @@ async def add_ip_api():
|
|||||||
raise HTTPException(status_code=400, detail=f'Error: {str(e)}')
|
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):
|
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:
|
Args:
|
||||||
body: An instance of EditInputBody containing the new IPv4 and/or IPv6 addresses.
|
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:
|
try:
|
||||||
if not body.ipv4 and not body.ipv6:
|
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.')
|
return DetailResponse(detail='IP address edited successfully.')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=400, detail=f'Error: {str(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))
|
||||||
@ -21,4 +21,26 @@ class StatusResponse(BaseModel):
|
|||||||
raise ValueError(f"'{v}' is not a valid IPv4 or IPv6 address or domain name")
|
raise ValueError(f"'{v}' is not a valid IPv4 or IPv6 address or domain name")
|
||||||
|
|
||||||
class EditInputBody(StatusResponse):
|
class EditInputBody(StatusResponse):
|
||||||
pass
|
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]
|
||||||
@ -1,4 +1,4 @@
|
|||||||
from typing import Optional
|
from typing import Optional, List
|
||||||
from pydantic import BaseModel, RootModel
|
from pydantic import BaseModel, RootModel
|
||||||
|
|
||||||
|
|
||||||
@ -38,8 +38,14 @@ class EditUserInputBody(BaseModel):
|
|||||||
renew_creation_date: bool = False
|
renew_creation_date: bool = False
|
||||||
blocked: bool = False
|
blocked: bool = False
|
||||||
|
|
||||||
|
class NodeUri(BaseModel):
|
||||||
|
name: str
|
||||||
|
uri: str
|
||||||
|
|
||||||
class UserUriResponse(BaseModel):
|
class UserUriResponse(BaseModel):
|
||||||
username: str
|
username: str
|
||||||
ipv4: str | None = None
|
ipv4: Optional[str] = None
|
||||||
ipv6: str | None = None
|
ipv6: Optional[str] = None
|
||||||
normal_sub: str | None = None
|
nodes: Optional[List[NodeUri]] = []
|
||||||
|
normal_sub: Optional[str] = None
|
||||||
|
error: Optional[str] = None
|
||||||
@ -178,10 +178,12 @@ async def show_user_uri_api(username: str):
|
|||||||
uri_data_list = cli_api.show_user_uri_json([username])
|
uri_data_list = cli_api.show_user_uri_json([username])
|
||||||
if not uri_data_list:
|
if not uri_data_list:
|
||||||
raise HTTPException(status_code=404, detail=f'URI for user {username} not found.')
|
raise HTTPException(status_code=404, detail=f'URI for user {username} not found.')
|
||||||
|
|
||||||
uri_data = uri_data_list[0]
|
uri_data = uri_data_list[0]
|
||||||
if uri_data.get('error'):
|
if uri_data.get('error'):
|
||||||
raise HTTPException(status_code=404, detail=f"{uri_data['error']}")
|
raise HTTPException(status_code=404, detail=f"{uri_data['error']}")
|
||||||
return uri_data
|
|
||||||
|
return UserUriResponse(**uri_data)
|
||||||
except cli_api.ScriptNotFoundError as e:
|
except cli_api.ScriptNotFoundError as e:
|
||||||
raise HTTPException(status_code=500, detail=f'Server script error: {str(e)}')
|
raise HTTPException(status_code=500, detail=f'Server script error: {str(e)}')
|
||||||
except cli_api.CommandExecutionError as e:
|
except cli_api.CommandExecutionError as e:
|
||||||
|
|||||||
Reference in New Issue
Block a user