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