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>
This commit is contained in:
parent
77370eff19
commit
07f8673f2f
14 changed files with 237 additions and 85 deletions
|
|
@ -1,7 +1,10 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/nemunaire/repeater/internal/config"
|
||||
|
|
@ -28,12 +31,13 @@ func GetWiFiNetworks(c *gin.Context) {
|
|||
func ScanWiFi(c *gin.Context) {
|
||||
networks, err := wifi.ScanNetworks()
|
||||
if err != nil {
|
||||
logging.AddLog("WiFi", "Erreur lors du scan: "+err.Error())
|
||||
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é - "+string(rune(len(networks)))+" réseaux trouvés")
|
||||
logging.AddLog("WiFi", "Scan terminé - "+strconv.Itoa(len(networks))+" réseaux trouvés")
|
||||
c.JSON(http.StatusOK, networks)
|
||||
}
|
||||
|
||||
|
|
@ -45,16 +49,19 @@ func ConnectWiFi(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
logging.AddLog("WiFi", "Tentative de connexion à "+req.SSID)
|
||||
logging.AddLog("WiFi", "Tentative de connexion")
|
||||
|
||||
err := wifi.Connect(req.SSID, req.Password)
|
||||
if err != nil {
|
||||
logging.AddLog("WiFi", "Échec de connexion: "+err.Error())
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Échec de connexion: " + err.Error()})
|
||||
// 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 à "+req.SSID)
|
||||
logging.AddLog("WiFi", "Connexion réussie")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "success"})
|
||||
}
|
||||
|
||||
|
|
@ -64,8 +71,9 @@ func DisconnectWiFi(c *gin.Context) {
|
|||
|
||||
err := wifi.Disconnect()
|
||||
if err != nil {
|
||||
logging.AddLog("WiFi", "Erreur de déconnexion: "+err.Error())
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur de déconnexion: " + err.Error()})
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -83,19 +91,24 @@ func ConfigureHotspot(c *gin.Context) {
|
|||
|
||||
err := hotspot.Configure(config)
|
||||
if err != nil {
|
||||
logging.AddLog("Hotspot", "Erreur de configuration: "+err.Error())
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur de configuration: " + err.Error()})
|
||||
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: "+config.SSID)
|
||||
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) {
|
||||
// Determine current state
|
||||
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 {
|
||||
|
|
@ -107,13 +120,18 @@ func ToggleHotspot(c *gin.Context, status *models.SystemStatus) {
|
|||
}
|
||||
|
||||
if err != nil {
|
||||
logging.AddLog("Hotspot", "Erreur: "+err.Error())
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur: " + err.Error()})
|
||||
log.Printf("Hotspot toggle error: %v", err)
|
||||
logging.AddLog("Hotspot", "Erreur")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur"})
|
||||
return
|
||||
}
|
||||
|
||||
// Update status immediately
|
||||
status.HotspotStatus = hotspot.GetDetailedStatus()
|
||||
// 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})
|
||||
}
|
||||
|
|
@ -122,7 +140,8 @@ func ToggleHotspot(c *gin.Context, status *models.SystemStatus) {
|
|||
func GetDevices(c *gin.Context, cfg *config.Config) {
|
||||
devices, err := station.GetStations()
|
||||
if err != nil {
|
||||
logging.AddLog("Système", "Erreur lors de la récupération des appareils: "+err.Error())
|
||||
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
|
||||
}
|
||||
|
|
@ -130,9 +149,13 @@ func GetDevices(c *gin.Context, cfg *config.Config) {
|
|||
c.JSON(http.StatusOK, devices)
|
||||
}
|
||||
|
||||
// GetStatus returns system status
|
||||
func GetStatus(c *gin.Context, status *models.SystemStatus) {
|
||||
c.JSON(http.StatusOK, status)
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package handlers
|
|||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
|
|
@ -10,9 +11,23 @@ import (
|
|||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
CheckOrigin: checkSameOrigin,
|
||||
}
|
||||
|
||||
// checkSameOrigin only accepts WebSocket upgrades whose Origin header matches
|
||||
// the request's Host. Without this, any web page a LAN user visits could open
|
||||
// a WebSocket against the router's API.
|
||||
func checkSameOrigin(r *http.Request) bool {
|
||||
origin := r.Header.Get("Origin")
|
||||
if origin == "" {
|
||||
// Non-browser clients (curl, native apps) do not send Origin.
|
||||
return true
|
||||
},
|
||||
}
|
||||
u, err := url.Parse(origin)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return u.Host == r.Host
|
||||
}
|
||||
|
||||
// WebSocketLogs handles WebSocket connections for real-time logs
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"embed"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/nemunaire/repeater/internal/api/handlers"
|
||||
|
|
@ -12,7 +13,7 @@ import (
|
|||
)
|
||||
|
||||
// SetupRouter creates and configures the Gin router
|
||||
func SetupRouter(status *models.SystemStatus, cfg *config.Config, assets embed.FS) *gin.Engine {
|
||||
func SetupRouter(status *models.SystemStatus, statusMu *sync.RWMutex, cfg *config.Config, assets embed.FS) *gin.Engine {
|
||||
// Set Gin to release mode (can be overridden with GIN_MODE env var)
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
|
|
@ -35,7 +36,7 @@ func SetupRouter(status *models.SystemStatus, cfg *config.Config, assets embed.F
|
|||
{
|
||||
hotspot.POST("/config", handlers.ConfigureHotspot)
|
||||
hotspot.POST("/toggle", func(c *gin.Context) {
|
||||
handlers.ToggleHotspot(c, status)
|
||||
handlers.ToggleHotspot(c, status, statusMu)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -46,7 +47,7 @@ func SetupRouter(status *models.SystemStatus, cfg *config.Config, assets embed.F
|
|||
|
||||
// Status endpoint
|
||||
api.GET("/status", func(c *gin.Context) {
|
||||
handlers.GetStatus(c, status)
|
||||
handlers.GetStatus(c, status, statusMu)
|
||||
})
|
||||
|
||||
// Log endpoints
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue