router.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. """
  2. FastAPI router for WiFi management endpoints.
  3. """
  4. from fastapi import APIRouter, HTTPException, Request
  5. from fastapi.responses import HTMLResponse
  6. from pydantic import BaseModel
  7. from typing import Optional
  8. import logging
  9. from . import manager
  10. logger = logging.getLogger(__name__)
  11. router = APIRouter(prefix="/api/wifi", tags=["wifi"])
  12. # Separate router for the captive portal handler (no prefix)
  13. captive_portal_router = APIRouter(tags=["wifi"])
  14. class WiFiConnectRequest(BaseModel):
  15. ssid: str
  16. password: Optional[str] = ""
  17. class WiFiForgetRequest(BaseModel):
  18. ssid: str
  19. @router.get("/status")
  20. async def wifi_status():
  21. """Get current WiFi mode, connection, and IP."""
  22. return manager.get_wifi_status()
  23. @router.get("/networks")
  24. async def wifi_networks():
  25. """Scan for available WiFi networks."""
  26. return manager.scan_networks()
  27. @router.post("/connect")
  28. async def wifi_connect(req: WiFiConnectRequest):
  29. """Connect to a WiFi network and reboot."""
  30. if not req.ssid:
  31. raise HTTPException(status_code=400, detail="SSID is required")
  32. result = await manager.connect_to_network(req.ssid, req.password or "")
  33. if not result["success"]:
  34. raise HTTPException(status_code=400, detail=result["message"])
  35. return result
  36. @router.post("/forget")
  37. async def wifi_forget(req: WiFiForgetRequest):
  38. """Forget a saved WiFi network."""
  39. if not req.ssid:
  40. raise HTTPException(status_code=400, detail="SSID is required")
  41. result = manager.forget_network(req.ssid)
  42. if not result["success"]:
  43. raise HTTPException(status_code=400, detail=result["message"])
  44. return result
  45. @router.get("/saved")
  46. async def wifi_saved():
  47. """Get list of saved WiFi connections."""
  48. return manager.get_saved_connections()
  49. # --- Captive portal detection endpoints ---
  50. # These handle the well-known URLs that phones/tablets probe to detect captive portals.
  51. # In hotspot mode, DNS redirects all domains to the Pi, so these probes arrive here.
  52. # We serve a minimal HTML page that redirects to the WiFi setup page.
  53. CAPTIVE_PORTAL_HTML = """<!DOCTYPE html>
  54. <html>
  55. <head>
  56. <meta charset="utf-8">
  57. <meta name="viewport" content="width=device-width, initial-scale=1">
  58. <title>Dune Weaver - WiFi Setup</title>
  59. <style>
  60. * { margin: 0; padding: 0; box-sizing: border-box; }
  61. body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  62. display: flex; justify-content: center; align-items: center;
  63. min-height: 100vh; background: #0a0a0a; color: #fafafa; padding: 1rem; }
  64. .card { background: #1a1a1a; border-radius: 12px; padding: 2rem;
  65. max-width: 400px; width: 100%; text-align: center; }
  66. h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
  67. p { color: #888; margin-bottom: 1.5rem; font-size: 0.9rem; }
  68. a { display: inline-block; background: #3b82f6; color: white; padding: 0.75rem 2rem;
  69. border-radius: 8px; text-decoration: none; font-weight: 500; }
  70. a:hover { background: #2563eb; }
  71. </style>
  72. </head>
  73. <body>
  74. <div class="card">
  75. <h1>Welcome to Dune Weaver</h1>
  76. <p>Connect to your home WiFi to get started.</p>
  77. <a href="/wifi-setup">Set Up WiFi</a>
  78. </div>
  79. </body>
  80. </html>"""
  81. @captive_portal_router.get("/hotspot-detect.html", response_class=HTMLResponse)
  82. @captive_portal_router.get("/generate_204", response_class=HTMLResponse)
  83. @captive_portal_router.get("/connecttest.txt", response_class=HTMLResponse)
  84. @captive_portal_router.get("/ncsi.txt", response_class=HTMLResponse)
  85. @captive_portal_router.get("/redirect", response_class=HTMLResponse)
  86. @captive_portal_router.get("/canonical.html", response_class=HTMLResponse)
  87. async def captive_portal_detect():
  88. """Handle captive portal detection probes.
  89. Phones and tablets check these well-known URLs after connecting to WiFi.
  90. In hotspot mode, DNS resolves all domains to the Pi, so these probes
  91. arrive at our server. Returning anything other than the expected response
  92. triggers the OS to show a captive portal browser.
  93. """
  94. return HTMLResponse(content=CAPTIVE_PORTAL_HTML, status_code=200)