gitscope
← marzneshin / v0.7.1 / app/routes/node.py
https://dev.kronoz.win/r/marzneshin.git
app/routes/node.py · Python
Blame Raw
  1import asyncio
  2import logging
  3from typing import Annotated
  4
  5import sqlalchemy
  6from fastapi import APIRouter, Body, Query
  7from fastapi import HTTPException, WebSocket
  8from fastapi_pagination.ext.sqlalchemy import paginate
  9from fastapi_pagination.links import Page
 10from starlette.websockets import WebSocketDisconnect
 11
 12from app import marznode
 13from app.db import crud, get_tls_certificate
 14from app.db.models import Node
 15from app.dependencies import (
 16    DBDep,
 17    SudoAdminDep,
 18    EndDateDep,
 19    StartDateDep,
 20    get_admin,
 21)
 22from app.models.node import (
 23    NodeCreate,
 24    NodeModify,
 25    NodeResponse,
 26    NodeSettings,
 27    NodeStatus,
 28    BackendConfig,
 29    BackendStats,
 30)
 31from app.models.system import TrafficUsageSeries
 32
 33logger = logging.getLogger(__name__)
 34router = APIRouter(prefix="/nodes", tags=["Node"])
 35
 36
 37@router.get("", response_model=Page[NodeResponse])
 38def get_nodes(
 39    db: DBDep,
 40    admin: SudoAdminDep,
 41    status: list[NodeStatus] = Query(None),
 42    name: str = Query(None),
 43):
 44    query = db.query(Node)
 45
 46    if name:
 47        query = query.filter(Node.name.ilike(f"%{name}%"))
 48
 49    if status:
 50        query = query.filter(Node.status.in_(status))
 51
 52    return paginate(db, query)
 53
 54
 55@router.post("", response_model=NodeResponse)
 56async def add_node(new_node: NodeCreate, db: DBDep, admin: SudoAdminDep):
 57    try:
 58        db_node = crud.create_node(db, new_node)
 59    except sqlalchemy.exc.IntegrityError:
 60        db.rollback()
 61        raise HTTPException(
 62            status_code=409, detail=f'Node "{new_node.name}" already exists'
 63        )
 64    certificate = get_tls_certificate(db)
 65
 66    await marznode.operations.add_node(db_node, certificate)
 67
 68    logger.info("New node `%s` added", db_node.name)
 69    return db_node
 70
 71
 72@router.get("/settings", response_model=NodeSettings)
 73def get_node_settings(db: DBDep, admin: SudoAdminDep):
 74    tls = crud.get_tls_certificate(db)
 75
 76    return NodeSettings(certificate=tls.certificate)
 77
 78
 79@router.get("/{node_id}", response_model=NodeResponse)
 80def get_node(node_id: int, db: DBDep, admin: SudoAdminDep):
 81    db_node = crud.get_node_by_id(db, node_id)
 82    if not db_node:
 83        raise HTTPException(status_code=404, detail="Node not found")
 84
 85    return db_node
 86
 87
 88@router.websocket("/{node_id}/{backend}/logs")
 89async def node_logs(
 90    node_id: int,
 91    backend: str,
 92    websocket: WebSocket,
 93    db: DBDep,
 94    include_buffer: bool = True,
 95):
 96    token = websocket.query_params.get("token", "") or websocket.headers.get(
 97        "Authorization", ""
 98    ).removeprefix("Bearer ")
 99    admin = get_admin(db, token)
100
101    if not admin or not admin.is_sudo:
102        return await websocket.close(reason="You're not allowed", code=4403)
103
104    if not marznode.nodes.get(node_id):
105        return await websocket.close(reason="Node not found", code=4404)
106
107    await websocket.accept()
108    try:
109        async for line in marznode.nodes[node_id].get_logs(
110            name=backend, include_buffer=include_buffer
111        ):
112            try:
113                await websocket.send_text(line)
114            except WebSocketDisconnect:
115                break
116    finally:
117        await websocket.close()
118
119
120@router.put("/{node_id}", response_model=NodeResponse)
121async def modify_node(
122    node_id: int, modified_node: NodeModify, db: DBDep, admin: SudoAdminDep
123):
124    db_node = crud.get_node_by_id(db, node_id)
125    if not db_node:
126        raise HTTPException(status_code=404, detail="Node not found")
127
128    db_node = crud.update_node(db, db_node, modified_node)
129
130    await marznode.operations.remove_node(db_node.id)
131    if db_node.status != NodeStatus.disabled:
132        certificate = get_tls_certificate(db)
133        await marznode.operations.add_node(db_node, certificate)
134
135    logger.info("Node `%s` modified", db_node.name)
136    return db_node
137
138
139@router.delete("/{node_id}")
140async def remove_node(node_id: int, db: DBDep, admin: SudoAdminDep):
141    db_node = crud.get_node_by_id(db, node_id)
142    if not db_node:
143        raise HTTPException(status_code=404, detail="Node not found")
144
145    crud.remove_node(db, db_node)
146    await marznode.operations.remove_node(db_node.id)
147
148    logger.info(f"Node `%s` deleted", db_node.name)
149    return {}
150
151
152@router.post("/{node_id}/resync")
153async def reconnect_node(node_id: int, db: DBDep, admin: SudoAdminDep):
154    db_node = crud.get_node_by_id(db, node_id)
155    if not db_node:
156        raise HTTPException(status_code=404, detail="Node not found")
157
158    return {}
159
160
161@router.get("/{node_id}/usage", response_model=TrafficUsageSeries)
162def get_usage(
163    node_id: int,
164    db: DBDep,
165    admin: SudoAdminDep,
166    start_date: StartDateDep,
167    end_date: EndDateDep,
168):
169    """
170    Get nodes usage
171    """
172    node = crud.get_node_by_id(db, node_id)
173    if not node:
174        raise HTTPException(status_code=404, detail="Node not found")
175
176    return crud.get_node_usage(db, start_date, end_date, node)
177
178
179@router.get("/{node_id}/{backend}/stats", response_model=BackendStats)
180async def get_backend_stats(
181    node_id: int, backend: str, db: DBDep, admin: SudoAdminDep
182):
183    if not (node := marznode.nodes.get(node_id)):
184        raise HTTPException(status_code=404, detail="Node not found")
185
186    try:
187        stats = await node.get_backend_stats(backend)
188    except Exception:
189        raise HTTPException(502)
190    else:
191        return BackendStats(running=stats.running)
192
193
194@router.get("/{node_id}/{backend}/config", response_model=BackendConfig)
195async def get_node_xray_config(
196    node_id: int, backend: str, admin: SudoAdminDep
197):
198    if not (node := marznode.nodes.get(node_id)):
199        raise HTTPException(status_code=404, detail="Node not found")
200
201    try:
202        config, config_format = await node.get_backend_config(name=backend)
203    except Exception:
204        raise HTTPException(status_code=502, detail="Node isn't responsive")
205    else:
206        return {"config": config, "format": config_format}
207
208
209@router.put("/{node_id}/{backend}/config")
210async def alter_node_xray_config(
211    node_id: int,
212    backend: str,
213    admin: SudoAdminDep,
214    config: Annotated[BackendConfig, Body()],
215):
216    if not (node := marznode.nodes.get(node_id)):
217        raise HTTPException(status_code=404, detail="Node not found")
218
219    try:
220        await asyncio.wait_for(
221            node.restart_backend(
222                name=backend,
223                config=config.config,
224                config_format=config.format.value,
225            ),
226            5,
227        )
228    except:
229        raise HTTPException(
230            status_code=502, detail="No response from the node."
231        )
232    return {}