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>
171 lines
5.1 KiB
Go
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"})
|
|
}
|