tunnel_port.py 2.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. """
  2. Device API endpoint for reporting tunnel port allocation.
  3. """
  4. from typing import Annotated, Optional
  5. from fastapi import APIRouter, Depends, Header, HTTPException, status
  6. from pydantic import BaseModel
  7. from sqlalchemy import select
  8. from sqlalchemy.ext.asyncio import AsyncSession
  9. from app.core.database import get_db
  10. from app.models.device import Device
  11. from app.services.tunnel_service import tunnel_service
  12. router = APIRouter(tags=["device-api"])
  13. class TunnelPortRequest(BaseModel):
  14. """Device reports tunnel port"""
  15. tunnel_type: str # "ssh" | "dashboard"
  16. port: Optional[int] = None
  17. status: str # "connected" | "disconnected"
  18. async def _auth_device_token(
  19. authorization: str | None, db: AsyncSession
  20. ) -> Device:
  21. """Authenticate device by token from Authorization header."""
  22. if not authorization or not authorization.lower().startswith("bearer "):
  23. raise HTTPException(
  24. status_code=status.HTTP_401_UNAUTHORIZED,
  25. detail="Missing token",
  26. )
  27. token = authorization.split(None, 1)[1]
  28. result = await db.execute(select(Device).where(Device.device_token == token))
  29. device = result.scalar_one_or_none()
  30. if not device:
  31. raise HTTPException(
  32. status_code=status.HTTP_401_UNAUTHORIZED,
  33. detail="Invalid token",
  34. )
  35. return device
  36. @router.post("/tunnel-port")
  37. async def report_tunnel_port(
  38. req: TunnelPortRequest,
  39. db: Annotated[AsyncSession, Depends(get_db)],
  40. authorization: Annotated[str | None, Header()] = None,
  41. ):
  42. """
  43. Device reports tunnel port allocation after establishing reverse SSH tunnel.
  44. Args:
  45. tunnel_type: "ssh" or "dashboard"
  46. port: Allocated port number (if connected)
  47. status: "connected" or "disconnected"
  48. Returns:
  49. success: True
  50. """
  51. device = await _auth_device_token(authorization, db)
  52. if req.tunnel_type not in ["ssh", "dashboard"]:
  53. raise HTTPException(
  54. status_code=status.HTTP_400_BAD_REQUEST,
  55. detail="tunnel_type must be 'ssh' or 'dashboard'"
  56. )
  57. # Update tunnel status
  58. tunnel_service.report_device_port(
  59. device_id=device.mac_address,
  60. tunnel_type=req.tunnel_type,
  61. port=req.port,
  62. status=req.status
  63. )
  64. print(f"[tunnel] Device {device.mac_address} reported {req.tunnel_type} "
  65. f"tunnel {req.status}" + (f" on port {req.port}" if req.port else ""))
  66. return {"success": True}