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>
317 lines
8.1 KiB
Go
317 lines
8.1 KiB
Go
package app
|
|
|
|
import (
|
|
"embed"
|
|
"log"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/nemunaire/repeater/internal/api"
|
|
"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/station/backend"
|
|
"github.com/nemunaire/repeater/internal/syslog"
|
|
"github.com/nemunaire/repeater/internal/wifi"
|
|
)
|
|
|
|
// App represents the application
|
|
type App struct {
|
|
Status models.SystemStatus
|
|
StatusMutex sync.RWMutex
|
|
StartTime time.Time
|
|
Assets embed.FS
|
|
Config *config.Config
|
|
SyslogTailer *syslog.SyslogTailer
|
|
|
|
stopCh chan struct{}
|
|
stopOnce sync.Once
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
// New creates a new application instance
|
|
func New(assets embed.FS) *App {
|
|
return &App{
|
|
Status: models.SystemStatus{
|
|
Connected: false,
|
|
ConnectionState: "disconnected",
|
|
ConnectedSSID: "",
|
|
HotspotStatus: nil,
|
|
ConnectedCount: 0,
|
|
DataUsage: 0.0,
|
|
Uptime: 0,
|
|
},
|
|
StartTime: time.Now(),
|
|
Assets: assets,
|
|
stopCh: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// Initialize initializes the application
|
|
func (a *App) Initialize(cfg *config.Config) error {
|
|
// Store config reference
|
|
a.Config = cfg
|
|
|
|
// Initialize WiFi backend
|
|
if err := wifi.Initialize(cfg.WifiInterface, cfg.WifiBackend); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Start WiFi event monitoring
|
|
if err := wifi.StartEventMonitoring(); err != nil {
|
|
log.Printf("Warning: WiFi event monitoring failed: %v", err)
|
|
// Don't fail - polling fallback still works
|
|
}
|
|
|
|
// Initialize station backend
|
|
stationConfig := backend.BackendConfig{
|
|
InterfaceName: cfg.HotspotInterface,
|
|
ARPTablePath: cfg.ARPTablePath,
|
|
DHCPLeasesPath: cfg.DHCPLeasesPath,
|
|
HostapdInterface: cfg.HotspotInterface,
|
|
}
|
|
if err := station.Initialize(cfg.StationBackend, stationConfig); err != nil {
|
|
log.Printf("Warning: Station backend initialization failed: %v", err)
|
|
// Don't fail - will continue without station discovery
|
|
} else {
|
|
// Start event monitoring for station events
|
|
if err := station.StartEventMonitoring(backend.EventCallbacks{
|
|
OnStationConnected: a.handleStationConnected,
|
|
OnStationDisconnected: a.handleStationDisconnected,
|
|
OnStationUpdated: a.handleStationUpdated,
|
|
}); err != nil {
|
|
log.Printf("Warning: Station event monitoring failed: %v", err)
|
|
// Don't fail - polling fallback still works
|
|
}
|
|
}
|
|
|
|
// Start syslog tailing if enabled
|
|
if cfg.SyslogEnabled {
|
|
a.SyslogTailer = syslog.NewSyslogTailer(
|
|
cfg.SyslogPath,
|
|
cfg.SyslogFilter,
|
|
cfg.SyslogSource,
|
|
)
|
|
if err := a.SyslogTailer.Start(); err != nil {
|
|
log.Printf("Warning: Failed to start syslog tailing: %v", err)
|
|
// Don't fail - app continues without syslog
|
|
}
|
|
}
|
|
|
|
// Start periodic tasks
|
|
a.wg.Add(2)
|
|
go a.periodicStatusUpdate()
|
|
go a.periodicDeviceUpdate()
|
|
|
|
logging.AddLog("Système", "Application initialisée")
|
|
return nil
|
|
}
|
|
|
|
// Run starts the HTTP server
|
|
func (a *App) Run(addr string) error {
|
|
router := api.SetupRouter(&a.Status, &a.StatusMutex, a.Config, a.Assets)
|
|
|
|
logging.AddLog("Système", "Serveur API démarré sur "+addr)
|
|
return router.Run(addr)
|
|
}
|
|
|
|
// Shutdown gracefully shuts down the application. Idempotent — main wires
|
|
// it both to a signal handler and a defer, and the second call must be a
|
|
// no-op (channel close panics otherwise; backend Close paths assume single
|
|
// invocation).
|
|
func (a *App) Shutdown() {
|
|
a.stopOnce.Do(func() {
|
|
// Signal periodic loops to exit, then wait for them so we don't
|
|
// race with backends being closed below.
|
|
close(a.stopCh)
|
|
a.wg.Wait()
|
|
|
|
// Stop syslog tailing if running
|
|
if a.SyslogTailer != nil {
|
|
a.SyslogTailer.Stop()
|
|
}
|
|
|
|
// Stop station monitoring and close backend
|
|
station.StopEventMonitoring()
|
|
station.Close()
|
|
|
|
wifi.StopEventMonitoring()
|
|
wifi.Close()
|
|
logging.AddLog("Système", "Application arrêtée")
|
|
})
|
|
}
|
|
|
|
// getSystemUptime reads system uptime from /proc/uptime
|
|
func getSystemUptime() int64 {
|
|
data, err := os.ReadFile("/proc/uptime")
|
|
if err != nil {
|
|
log.Printf("Error reading /proc/uptime: %v", err)
|
|
return 0
|
|
}
|
|
|
|
fields := strings.Fields(string(data))
|
|
if len(fields) == 0 {
|
|
return 0
|
|
}
|
|
|
|
uptime, err := strconv.ParseFloat(fields[0], 64)
|
|
if err != nil {
|
|
log.Printf("Error parsing uptime: %v", err)
|
|
return 0
|
|
}
|
|
|
|
return int64(uptime)
|
|
}
|
|
|
|
// getInterfaceBytes reads rx and tx bytes for a network interface
|
|
func getInterfaceBytes(interfaceName string) (rxBytes, txBytes int64) {
|
|
rxPath := "/sys/class/net/" + interfaceName + "/statistics/rx_bytes"
|
|
txPath := "/sys/class/net/" + interfaceName + "/statistics/tx_bytes"
|
|
|
|
// Read RX bytes
|
|
rxData, err := os.ReadFile(rxPath)
|
|
if err != nil {
|
|
log.Printf("Error reading rx_bytes for %s: %v", interfaceName, err)
|
|
} else {
|
|
rxBytes, _ = strconv.ParseInt(strings.TrimSpace(string(rxData)), 10, 64)
|
|
}
|
|
|
|
// Read TX bytes
|
|
txData, err := os.ReadFile(txPath)
|
|
if err != nil {
|
|
log.Printf("Error reading tx_bytes for %s: %v", interfaceName, err)
|
|
} else {
|
|
txBytes, _ = strconv.ParseInt(strings.TrimSpace(string(txData)), 10, 64)
|
|
}
|
|
|
|
return rxBytes, txBytes
|
|
}
|
|
|
|
// periodicStatusUpdate updates WiFi connection status periodically
|
|
func (a *App) periodicStatusUpdate() {
|
|
defer a.wg.Done()
|
|
ticker := time.NewTicker(5 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-a.stopCh:
|
|
return
|
|
case <-ticker.C:
|
|
}
|
|
|
|
a.StatusMutex.Lock()
|
|
a.Status.Connected = wifi.IsConnected()
|
|
a.Status.ConnectionState = wifi.GetConnectionState()
|
|
a.Status.ConnectedSSID = wifi.GetConnectedSSID()
|
|
a.Status.Uptime = getSystemUptime()
|
|
|
|
// Get detailed hotspot status
|
|
a.Status.HotspotStatus = hotspot.GetDetailedStatus()
|
|
|
|
// Get network data usage for WiFi interface
|
|
if a.Config != nil {
|
|
rxBytes, txBytes := getInterfaceBytes(a.Config.WifiInterface)
|
|
// Convert to MB and sum rx + tx
|
|
a.Status.DataUsage = float64(rxBytes+txBytes) / (1024 * 1024)
|
|
}
|
|
|
|
a.StatusMutex.Unlock()
|
|
}
|
|
}
|
|
|
|
// periodicDeviceUpdate updates connected devices list periodically
|
|
func (a *App) periodicDeviceUpdate() {
|
|
defer a.wg.Done()
|
|
ticker := time.NewTicker(10 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-a.stopCh:
|
|
return
|
|
case <-ticker.C:
|
|
}
|
|
|
|
devices, err := station.GetStations()
|
|
if err != nil {
|
|
log.Printf("Error getting connected devices: %v", err)
|
|
}
|
|
|
|
a.StatusMutex.Lock()
|
|
a.Status.ConnectedDevices = devices
|
|
a.Status.ConnectedCount = len(devices)
|
|
a.StatusMutex.Unlock()
|
|
}
|
|
}
|
|
|
|
// handleStationConnected handles station connection events
|
|
func (a *App) handleStationConnected(st backend.Station) {
|
|
a.StatusMutex.Lock()
|
|
defer a.StatusMutex.Unlock()
|
|
|
|
// Convert backend.Station to models.ConnectedDevice
|
|
device := models.ConnectedDevice{
|
|
Name: st.Hostname,
|
|
Type: st.Type,
|
|
MAC: st.MAC,
|
|
IP: st.IP,
|
|
}
|
|
|
|
// Check if device already exists
|
|
found := false
|
|
for i, d := range a.Status.ConnectedDevices {
|
|
if d.MAC == device.MAC {
|
|
a.Status.ConnectedDevices[i] = device
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
// Add new device if not found
|
|
if !found {
|
|
a.Status.ConnectedDevices = append(a.Status.ConnectedDevices, device)
|
|
a.Status.ConnectedCount = len(a.Status.ConnectedDevices)
|
|
logging.AddLog("Stations", "Device connected: "+device.MAC+" ("+device.IP+")")
|
|
}
|
|
}
|
|
|
|
// handleStationDisconnected handles station disconnection events
|
|
func (a *App) handleStationDisconnected(mac string) {
|
|
a.StatusMutex.Lock()
|
|
defer a.StatusMutex.Unlock()
|
|
|
|
// Remove device from list
|
|
for i, d := range a.Status.ConnectedDevices {
|
|
if d.MAC == mac {
|
|
a.Status.ConnectedDevices = append(a.Status.ConnectedDevices[:i], a.Status.ConnectedDevices[i+1:]...)
|
|
a.Status.ConnectedCount = len(a.Status.ConnectedDevices)
|
|
logging.AddLog("Stations", "Device disconnected: "+mac)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleStationUpdated handles station update events
|
|
func (a *App) handleStationUpdated(st backend.Station) {
|
|
a.StatusMutex.Lock()
|
|
defer a.StatusMutex.Unlock()
|
|
|
|
// Update existing device
|
|
for i, d := range a.Status.ConnectedDevices {
|
|
if d.MAC == st.MAC {
|
|
a.Status.ConnectedDevices[i] = models.ConnectedDevice{
|
|
Name: st.Hostname,
|
|
Type: st.Type,
|
|
MAC: st.MAC,
|
|
IP: st.IP,
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|