gitscope
← marzneshin / v0.5.1 / 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    NodesUsageResponse,
 28    BackendConfig,
 29    BackendStats,
 30)
 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("/usage", response_model=NodesUsageResponse)
 79def get_usage(
 80    db: DBDep,
 81    admin: SudoAdminDep,
 82    start_date: StartDateDep,
 83    end_date: EndDateDep,
 84):
 85    """
 86    Get nodes usage
 87    """
 88    usages = crud.get_nodes_usage(db, start_date, end_date)
 89
 90    return {"usages": usages}
 91
 92
 93@router.get("/{node_id}", response_model=NodeResponse)
 94def get_node(node_id: int, db: DBDep, admin: SudoAdminDep):
 95    db_node = crud.get_node_by_id(db, node_id)
 96    if not db_node:
 97        raise HTTPException(status_code=404, detail="Node not found")
 98
 99    return db_node
100
101
102@router.websocket("/{node_id}/{backend}/logs")
103async def node_logs(
104    node_id: int,
105    backend: str,
106    websocket: WebSocket,
107    db: DBDep,
108    include_buffer: bool = True,
109):
110    token = websocket.query_params.get("token") or websocket.headers.get(
111        "Authorization", ""
112    ).removeprefix("Bearer ")
113    admin = get_admin(db, token)
114
115    if not admin or not admin.is_sudo:
116        return await websocket.close(reason="You're not allowed", code=4403)
117
118    if not marznode.nodes.get(node_id):
119        return await websocket.close(reason="Node not found", code=4404)
120
121    await websocket.accept()
122    try:
123        async for line in marznode.nodes[node_id].get_logs(
124            name=backend, include_buffer=include_buffer
125        ):
126            try:
127                await websocket.send_text(line)
128            except WebSocketDisconnect:
129                break
130    except:
131        await websocket.close()
132
133
134@router.put("/{node_id}", response_model=NodeResponse)
135async def modify_node(
136    node_id: int, modified_node: NodeModify, db: DBDep, admin: SudoAdminDep
137):
138    db_node = crud.get_node_by_id(db, node_id)
139    if not db_node:
140        raise HTTPException(status_code=404, detail="Node not found")
141
142    db_node = crud.update_node(db, db_node, modified_node)
143
144    await marznode.operations.remove_node(db_node.id)
145    if db_node.status != NodeStatus.disabled:
146        certificate = get_tls_certificate(db)
147        await marznode.operations.add_node(db_node, certificate)
148
149    logger.info("Node `%s` modified", db_node.name)
150    return db_node
151
152
153@router.delete("/{node_id}")
154async def remove_node(node_id: int, db: DBDep, admin: SudoAdminDep):
155    db_node = crud.get_node_by_id(db, node_id)
156    if not db_node:
157        raise HTTPException(status_code=404, detail="Node not found")
158
159    crud.remove_node(db, db_node)
160    await marznode.operations.remove_node(db_node.id)
161
162    logger.info(f"Node `%s` deleted", db_node.name)
163    return {}
164
165
166@router.post("/{node_id}/resync")
167async def reconnect_node(node_id: int, db: DBDep, admin: SudoAdminDep):
168    db_node = crud.get_node_by_id(db, node_id)
169    if not db_node:
170        raise HTTPException(status_code=404, detail="Node not found")
171
172    return {}
173
174
175@router.get("/{node_id}/{backend}/stats", response_model=BackendStats)
176async def get_backend_stats(
177    node_id: int, backend: str, db: DBDep, admin: SudoAdminDep
178):
179    if not (node := marznode.nodes.get(node_id)):
180        raise HTTPException(status_code=404, detail="Node not found")
181
182    try:
183        stats = await node.get_backend_stats(backend)
184    except Exception:
185        raise HTTPException(502)
186    else:
187        return BackendStats(running=stats.running)
188
189
190@router.get("/{node_id}/{backend}/config", response_model=BackendConfig)
191async def get_node_xray_config(
192    node_id: int, backend: str, db: DBDep, admin: SudoAdminDep
193):
194    if not (node := marznode.nodes.get(node_id)):
195        raise HTTPException(status_code=404, detail="Node not found")
196
197    try:
198        config, config_format = await node.get_backend_config(name=backend)
199    except Exception:
200        raise HTTPException(status_code=502, detail="Node isn't responsive")
201    else:
202        return {"config": config, "format": config_format}
203
204
205@router.put("/{node_id}/{backend}/config")
206async def alter_node_xray_config(
207    node_id: int,
208    backend: str,
209    db: DBDep,
210    admin: SudoAdminDep,
211    config: Annotated[BackendConfig, Body()],
212):
213    if not (node := marznode.nodes.get(node_id)):
214        raise HTTPException(status_code=404, detail="Node not found")
215    try:
216        await node.restart_backend(
217            name=backend,
218            config=config.config,
219            config_format=config.format.value,
220        )
221    except:
222        raise HTTPException(status_code=502, detail="Node isn't responsive")
223    return {}