BIG BIG BIG BIG BIG UPDATES

Co-authored-by: Copilot <[email protected]>
This commit is contained in:
gabdevele
2026-05-04 23:59:48 +02:00
co-authored by Copilot
Unverified
parent 46f7cd6a5f
commit 93ba8e9ac2
88 changed files with 7591 additions and 447 deletions
View File
+3
View File
@@ -0,0 +1,3 @@
from api.auth.router import router
__all__ = ["router"]
+41
View File
@@ -0,0 +1,41 @@
from datetime import UTC, datetime
from typing import Annotated
from core.security import (
AuthContext,
create_access_token,
get_auth_context,
verify_password,
)
from fastapi import APIRouter, Depends, HTTPException, Response, status
from models.auth import LoginInput, TokenResponse
from repositories.auth import get_user_by_username, revoke_token
router = APIRouter(
prefix="/auth",
tags=["Auth"],
)
@router.post("/login", response_model=TokenResponse)
async def login_handler(payload: LoginInput):
user = await get_user_by_username(payload.username)
if user is None or not verify_password(payload.password, user.password_hash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid username or password",
headers={"WWW-Authenticate": "Bearer"},
)
token = create_access_token(subject=user.username)
return TokenResponse(access_token=token)
@router.post("/logout", status_code=status.HTTP_204_NO_CONTENT)
async def logout_handler(
auth_context: Annotated[AuthContext, Depends(get_auth_context)],
) -> Response:
expires_at = datetime.fromtimestamp(auth_context.payload.exp, tz=UTC)
if expires_at > datetime.now(UTC):
await revoke_token(jti=auth_context.payload.jti, expires_at=expires_at)
return Response(status_code=status.HTTP_204_NO_CONTENT)
+62
View File
@@ -0,0 +1,62 @@
from core.security import (
InvalidTokenError,
authenticate_token,
require_authenticated_user,
)
from fastapi import APIRouter, Depends, HTTPException, WebSocket, status
from services.editor import python_lsp
from starlette.websockets import WebSocketDisconnect
router = APIRouter(prefix="/proxy/editor/python", tags=["Editor"])
@router.get("/session", dependencies=[Depends(require_authenticated_user)])
async def python_editor_session():
if python_lsp.lsp_binary() is None:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="basedpyright-langserver is not installed",
)
session = python_lsp.get_session()
return {
"root_uri": session.root_uri,
"file_uri": session.file_uri,
}
async def authenticate_editor_socket(websocket: WebSocket) -> None:
token = websocket.query_params.get("token")
if token is None:
authorization = websocket.headers.get("authorization", "")
scheme, _, value = authorization.partition(" ")
if scheme.lower() == "bearer" and value:
token = value
if not token:
raise InvalidTokenError
await authenticate_token(token)
@router.websocket("/ws")
async def python_editor_socket(websocket: WebSocket):
try:
await authenticate_editor_socket(websocket)
except InvalidTokenError:
await websocket.close(code=4401)
return
if python_lsp.lsp_binary() is None:
await websocket.close(code=1011, reason="basedpyright unavailable")
return
session = python_lsp.get_session()
await websocket.accept()
try:
await python_lsp.bridge(websocket, session)
except WebSocketDisconnect:
return
except RuntimeError:
await websocket.close(code=1011, reason="language server failed")
View File
+300
View File
@@ -0,0 +1,300 @@
from pathlib import Path
from core.security import require_authenticated_user
from fastapi import APIRouter, Depends, HTTPException, Request, status
from models.example import AddonExampleCreateInput
from models.proxy import ProxyCreateInput, ProxyUpdateInput
from pymongo.errors import DuplicateKeyError
from repositories.examples import (
create_custom_example,
delete_custom_example,
list_custom_examples,
)
from repositories.proxy import (
create_proxy,
delete_proxy,
get_proxy_by_port,
update_proxy,
update_proxy_runtime,
)
from repositories.proxy import (
list_proxies as list_proxy_documents,
)
from services.mitmproxy.examples import BUILTIN_EXAMPLES
from services.mitmproxy.health import check_proxy
from services.mitmproxy.manager import MitmdumpError, manager
from services.mitmproxy.options import (
MitmOptionError,
load_options,
public_options,
validate_options,
)
from services.mitmproxy.validator import ProxyCodeValidationError, validate_proxy_code
SUPPORTED_MODES = {
"regular",
"local",
"wireguard",
"reverse",
"transparent",
"tun",
"upstream",
"socks5",
"dns",
}
SUPPORTED_PROTOCOLS = {
"http",
"https",
"tcp",
"tls",
"udp",
"dtls",
"dns",
"http3",
"quic",
}
router = APIRouter(
prefix="/proxy",
tags=["Proxy"],
dependencies=[Depends(require_authenticated_user)],
)
async def _validate_code_or_raise(code: str) -> None:
try:
await validate_proxy_code(code)
except ProxyCodeValidationError as error:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail=error.to_http_detail(),
) from error
def validate_mode(mode: str) -> None:
if mode not in SUPPORTED_MODES:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail=f"Unsupported proxy mode: {mode}",
)
def validate_mode_spec(mode_spec: str | None) -> None:
if mode_spec is None:
return
mode = mode_spec.split(":", 1)[0].split("@", 1)[0]
validate_mode(mode)
def validate_protocol(protocol: str) -> None:
if protocol not in SUPPORTED_PROTOCOLS:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail=f"Unsupported reverse proxy protocol: {protocol}",
)
async def validate_mitm_options(options: dict[str, object]) -> None:
try:
await validate_options(options)
except MitmOptionError as error:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail=str(error),
) from error
async def get_proxy_or_404(proxy_port: int):
proxy = await get_proxy_by_port(proxy_port)
if proxy is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Proxy not found",
)
return proxy
@router.get("")
async def list_proxies_handler():
return await list_proxy_documents()
@router.get("/services")
async def list_services(request: Request):
return getattr(request.app.state, "services", [])
@router.get("/mitmproxy/options")
async def list_mitmproxy_options():
return public_options(await load_options())
@router.get("/mitmproxy/examples")
async def list_mitmproxy_examples():
return [*BUILTIN_EXAMPLES, *await list_custom_examples()]
@router.post("/mitmproxy/examples", status_code=status.HTTP_201_CREATED)
async def create_mitmproxy_example(payload: AddonExampleCreateInput):
await _validate_code_or_raise(payload.code)
try:
return await create_custom_example(payload)
except DuplicateKeyError as error:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"Example already exists: {payload.id}",
) from error
@router.delete(
"/mitmproxy/examples/{example_id}",
status_code=status.HTTP_204_NO_CONTENT,
)
async def delete_mitmproxy_example(example_id: str):
if any(example.id == example_id for example in BUILTIN_EXAMPLES):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Built-in examples cannot be deleted",
)
deleted = await delete_custom_example(example_id)
if not deleted:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Example not found",
)
@router.get("/{proxy_port}")
async def get_proxy(proxy_port: int):
return await get_proxy_or_404(proxy_port)
@router.post("", status_code=status.HTTP_201_CREATED)
async def create_proxy_handler(payload: ProxyCreateInput):
existing_proxy = await get_proxy_by_port(payload.proxy_port)
if existing_proxy is not None:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"Proxy already exists on port {payload.proxy_port}",
)
await _validate_code_or_raise(payload.code)
validate_mode(payload.mode)
validate_mode_spec(payload.mode_spec)
validate_protocol(payload.protocol)
await validate_mitm_options(payload.mitm_options)
return await create_proxy(payload)
@router.put("/{proxy_port}")
async def edit_proxy(proxy_port: int, payload: ProxyUpdateInput):
if payload.code is not None:
await _validate_code_or_raise(payload.code)
if payload.mode is not None:
validate_mode(payload.mode)
if "mode_spec" in payload.model_fields_set:
validate_mode_spec(payload.mode_spec)
if payload.protocol is not None:
validate_protocol(payload.protocol)
if payload.mitm_options is not None:
await validate_mitm_options(payload.mitm_options)
updated_proxy = await update_proxy(proxy_port, payload)
if updated_proxy is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Proxy not found",
)
return updated_proxy
@router.post("/{proxy_port}/start")
async def start_proxy(proxy_port: int):
proxy = await get_proxy_or_404(proxy_port)
try:
runtime = await manager.start(proxy)
except MitmdumpError as error:
updated = await update_proxy_runtime(
proxy_port,
{"status": "error", "last_error": str(error)},
)
return updated or proxy
return await update_proxy_runtime(proxy_port, runtime)
@router.post("/{proxy_port}/stop")
async def stop_proxy(proxy_port: int):
proxy = await get_proxy_or_404(proxy_port)
runtime = await manager.stop(proxy)
return await update_proxy_runtime(proxy_port, runtime)
@router.post("/{proxy_port}/restart")
async def restart_proxy(proxy_port: int):
proxy = await get_proxy_or_404(proxy_port)
stopped = await manager.stop(proxy)
proxy = await update_proxy_runtime(proxy_port, stopped) or proxy
try:
runtime = await manager.start(proxy)
except MitmdumpError as error:
updated = await update_proxy_runtime(
proxy_port,
{"status": "error", "last_error": str(error)},
)
return updated or proxy
return await update_proxy_runtime(proxy_port, runtime)
@router.get("/{proxy_port}/status")
async def proxy_status(proxy_port: int):
proxy = await get_proxy_or_404(proxy_port)
runtime_status = await manager.status(proxy)
if runtime_status != proxy.status:
values: dict[str, object] = {"status": runtime_status}
if runtime_status != "running":
values["pid"] = None
proxy = await update_proxy_runtime(proxy_port, values) or proxy
return {"proxy_port": proxy.proxy_port, "status": runtime_status, "pid": proxy.pid}
@router.get("/{proxy_port}/health")
async def proxy_health(proxy_port: int):
proxy = await get_proxy_or_404(proxy_port)
return await check_proxy(proxy)
@router.get("/{proxy_port}/logs")
async def proxy_logs(proxy_port: int):
proxy = await get_proxy_or_404(proxy_port)
if proxy.log_path is None:
return {"proxy_port": proxy.proxy_port, "log": ""}
path = Path(proxy.log_path)
if not path.is_file():
return {"proxy_port": proxy.proxy_port, "log": ""}
return {
"proxy_port": proxy.proxy_port,
"log": path.read_text(encoding="utf-8", errors="replace")[-8000:],
}
@router.delete("/{proxy_port}", status_code=status.HTTP_204_NO_CONTENT)
async def remove_proxy(proxy_port: int):
proxy = await get_proxy_by_port(proxy_port)
if proxy is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Proxy not found",
)
await manager.stop(proxy)
deleted = await delete_proxy(proxy_port)
if not deleted:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Proxy not found",
)