repeater/internal/api/handlers/handlers.go
Pierre-Olivier Mercier 07f8673f2f Harden API surface and station/wifi backends
Bind to localhost by default and stop echoing backend errors (which can
embed credentials or low-level details) back over the API and log
broadcast. Validate hotspot SSID/passphrase/channel before writing
hostapd.conf and tighten its mode to 0600 since it stores the WPA PSK.
Restrict WebSocket upgrades to same-origin so a LAN browser can't be
turned into a proxy for the API.

Guard shared state: status reads/writes go through StatusMutex (the
periodic updater races with the toggle and status handlers otherwise),
broadcastToWebSockets no longer mutates the client map under RLock, and
station-event callbacks now run under SafeGo so a panic in app code can't
take down the daemon. Stop channels in hostapd, dhcp, and iwd signal
monitors are now closed under sync.Once to survive concurrent Stop calls.

App.Shutdown is idempotent and waits for the periodic loops before
closing backends, so signal-driven and deferred shutdowns no longer race.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 21:56:50 +08:00

171 lines
5.1 KiB
Go

package handlers
import (
"log"
"net/http"
"strconv"
"sync"
"github.com/gin-gonic/gin"
"github.com/nemunaire/repeater/internal/config"
"github.com/nemunaire/repeater/internal/hotspot"
"github.com/nemunaire/repeater/internal/logging"
"github.com/nemunaire/repeater/internal/models"
"github.com/nemunaire/repeater/internal/station"
"github.com/nemunaire/repeater/internal/wifi"
)
// GetWiFiNetworks returns cached WiFi networks without scanning
func GetWiFiNetworks(c *gin.Context) {
networks, err := wifi.GetCachedNetworks()
if err != nil {
logging.AddLog("WiFi", "Erreur lors de la récupération des réseaux: "+err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur lors de la récupération des réseaux"})
return
}
c.JSON(http.StatusOK, networks)
}
// ScanWiFi handles WiFi network scanning
func ScanWiFi(c *gin.Context) {
networks, err := wifi.ScanNetworks()
if err != nil {
log.Printf("WiFi scan error: %v", err)
logging.AddLog("WiFi", "Erreur lors du scan")
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur lors du scan WiFi"})
return
}
logging.AddLog("WiFi", "Scan terminé - "+strconv.Itoa(len(networks))+" réseaux trouvés")
c.JSON(http.StatusOK, networks)
}
// ConnectWiFi handles WiFi connection requests
func ConnectWiFi(c *gin.Context) {
var req models.WiFiConnectRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
return
}
logging.AddLog("WiFi", "Tentative de connexion")
err := wifi.Connect(req.SSID, req.Password)
if err != nil {
// Backend errors may include credentials or low-level details
// (dbus paths, kernel messages); keep them in the server log only.
log.Printf("WiFi connect error: %v", err)
logging.AddLog("WiFi", "Échec de connexion")
c.JSON(http.StatusInternalServerError, gin.H{"error": "Échec de connexion"})
return
}
logging.AddLog("WiFi", "Connexion réussie")
c.JSON(http.StatusOK, gin.H{"status": "success"})
}
// DisconnectWiFi handles WiFi disconnection
func DisconnectWiFi(c *gin.Context) {
logging.AddLog("WiFi", "Tentative de déconnexion")
err := wifi.Disconnect()
if err != nil {
log.Printf("WiFi disconnect error: %v", err)
logging.AddLog("WiFi", "Erreur de déconnexion")
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur de déconnexion"})
return
}
logging.AddLog("WiFi", "Déconnexion réussie")
c.JSON(http.StatusOK, gin.H{"status": "success"})
}
// ConfigureHotspot handles hotspot configuration
func ConfigureHotspot(c *gin.Context) {
var config models.HotspotConfig
if err := c.ShouldBindJSON(&config); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
return
}
err := hotspot.Configure(config)
if err != nil {
log.Printf("Hotspot configure error: %v", err)
logging.AddLog("Hotspot", "Erreur de configuration")
// Validation errors are user-actionable; keep them on the wire.
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
logging.AddLog("Hotspot", "Configuration mise à jour")
c.JSON(http.StatusOK, gin.H{"status": "success"})
}
// ToggleHotspot handles hotspot enable/disable
func ToggleHotspot(c *gin.Context, status *models.SystemStatus, statusMu *sync.RWMutex) {
// Determine current state under read lock to avoid racing with the
// periodic status updater that mutates HotspotStatus.
statusMu.RLock()
isEnabled := status.HotspotStatus != nil && status.HotspotStatus.State == "ENABLED"
statusMu.RUnlock()
var err error
if !isEnabled {
err = hotspot.Start()
logging.AddLog("Hotspot", "Hotspot activé")
} else {
err = hotspot.Stop()
logging.AddLog("Hotspot", "Hotspot désactivé")
}
if err != nil {
log.Printf("Hotspot toggle error: %v", err)
logging.AddLog("Hotspot", "Erreur")
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur"})
return
}
// Refresh status under the write lock; the periodic updater touches
// the same field on a 5s ticker.
newStatus := hotspot.GetDetailedStatus()
statusMu.Lock()
status.HotspotStatus = newStatus
statusMu.Unlock()
c.JSON(http.StatusOK, gin.H{"enabled": !isEnabled})
}
// GetDevices returns connected devices
func GetDevices(c *gin.Context, cfg *config.Config) {
devices, err := station.GetStations()
if err != nil {
log.Printf("GetDevices error: %v", err)
logging.AddLog("Système", "Erreur lors de la récupération des appareils")
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur lors de la récupération des appareils"})
return
}
c.JSON(http.StatusOK, devices)
}
// GetStatus returns system status. Reads under the shared lock so the JSON
// encoder doesn't observe a torn ConnectedDevices slice mid-update.
func GetStatus(c *gin.Context, status *models.SystemStatus, statusMu *sync.RWMutex) {
statusMu.RLock()
snapshot := *status
statusMu.RUnlock()
c.JSON(http.StatusOK, snapshot)
}
// GetLogs returns system logs
func GetLogs(c *gin.Context) {
logs := logging.GetLogs()
c.JSON(http.StatusOK, logs)
}
// ClearLogs clears system logs
func ClearLogs(c *gin.Context) {
logging.ClearLogs()
c.JSON(http.StatusOK, gin.H{"status": "success"})
}