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>
224 lines
4.9 KiB
Go
224 lines
4.9 KiB
Go
package iwd
|
|
|
|
import (
|
|
"log"
|
|
"sync"
|
|
|
|
"github.com/godbus/dbus/v5"
|
|
)
|
|
|
|
// SignalMonitor monitors D-Bus signals from iwd
|
|
type SignalMonitor struct {
|
|
conn *dbus.Conn
|
|
station *Station
|
|
|
|
// Signal channel
|
|
signalChan chan *dbus.Signal
|
|
|
|
// Callbacks
|
|
onStateChange func(state StationState, ssid string)
|
|
onScanComplete func()
|
|
|
|
// Control
|
|
stopChan chan struct{}
|
|
stopOnce sync.Once
|
|
mu sync.RWMutex
|
|
running bool
|
|
|
|
// State tracking
|
|
lastScanning bool
|
|
}
|
|
|
|
// NewSignalMonitor creates a new signal monitor
|
|
func NewSignalMonitor(conn *dbus.Conn, station *Station) *SignalMonitor {
|
|
return &SignalMonitor{
|
|
conn: conn,
|
|
station: station,
|
|
signalChan: make(chan *dbus.Signal, 100),
|
|
stopChan: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// OnStateChange registers a callback for state changes
|
|
func (sm *SignalMonitor) OnStateChange(callback func(state StationState, ssid string)) {
|
|
sm.mu.Lock()
|
|
defer sm.mu.Unlock()
|
|
sm.onStateChange = callback
|
|
}
|
|
|
|
// OnScanComplete registers a callback for scan completion
|
|
func (sm *SignalMonitor) OnScanComplete(callback func()) {
|
|
sm.mu.Lock()
|
|
defer sm.mu.Unlock()
|
|
sm.onScanComplete = callback
|
|
}
|
|
|
|
// Start begins monitoring D-Bus signals
|
|
func (sm *SignalMonitor) Start() error {
|
|
sm.mu.Lock()
|
|
if sm.running {
|
|
sm.mu.Unlock()
|
|
return nil
|
|
}
|
|
sm.running = true
|
|
sm.mu.Unlock()
|
|
|
|
// Subscribe to PropertiesChanged signals for Station interface
|
|
stationPath := sm.station.GetPath()
|
|
|
|
// Add signal match for PropertiesChanged on Station interface
|
|
matchOptions := []dbus.MatchOption{
|
|
dbus.WithMatchObjectPath(stationPath),
|
|
dbus.WithMatchInterface("org.freedesktop.DBus.Properties"),
|
|
dbus.WithMatchMember("PropertiesChanged"),
|
|
}
|
|
|
|
if err := sm.conn.AddMatchSignal(matchOptions...); err != nil {
|
|
sm.mu.Lock()
|
|
sm.running = false
|
|
sm.mu.Unlock()
|
|
return err
|
|
}
|
|
|
|
// Register signal channel
|
|
sm.conn.Signal(sm.signalChan)
|
|
|
|
// Get initial scanning state
|
|
scanning, err := sm.station.IsScanning()
|
|
if err == nil {
|
|
sm.lastScanning = scanning
|
|
}
|
|
|
|
// Start monitoring goroutine
|
|
go sm.monitor()
|
|
|
|
log.Printf("D-Bus signal monitoring started for station %s", stationPath)
|
|
return nil
|
|
}
|
|
|
|
// Stop stops monitoring D-Bus signals. Idempotent: stopOnce guards the
|
|
// channel close so concurrent Stop() callers do not panic on double-close.
|
|
func (sm *SignalMonitor) Stop() {
|
|
sm.mu.Lock()
|
|
if !sm.running {
|
|
sm.mu.Unlock()
|
|
return
|
|
}
|
|
sm.running = false
|
|
sm.mu.Unlock()
|
|
|
|
sm.stopOnce.Do(func() { close(sm.stopChan) })
|
|
|
|
// Remove signal channel
|
|
sm.conn.RemoveSignal(sm.signalChan)
|
|
|
|
log.Printf("D-Bus signal monitoring stopped")
|
|
}
|
|
|
|
// monitor is the main signal processing loop
|
|
func (sm *SignalMonitor) monitor() {
|
|
for {
|
|
select {
|
|
case sig := <-sm.signalChan:
|
|
sm.handleSignal(sig)
|
|
case <-sm.stopChan:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleSignal processes a D-Bus signal
|
|
func (sm *SignalMonitor) handleSignal(sig *dbus.Signal) {
|
|
// Only process PropertiesChanged signals
|
|
if sig.Name != "org.freedesktop.DBus.Properties.PropertiesChanged" {
|
|
return
|
|
}
|
|
|
|
// Verify signal is from Station interface
|
|
if len(sig.Body) < 2 {
|
|
return
|
|
}
|
|
|
|
interfaceName, ok := sig.Body[0].(string)
|
|
if !ok || interfaceName != StationInterface {
|
|
return
|
|
}
|
|
|
|
// Parse changed properties
|
|
changedProps, ok := sig.Body[1].(map[string]dbus.Variant)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
// Check for State property change
|
|
if stateVariant, ok := changedProps["State"]; ok {
|
|
if state, ok := stateVariant.Value().(string); ok {
|
|
sm.handleStateChange(StationState(state))
|
|
}
|
|
}
|
|
|
|
// Check for Scanning property change
|
|
if scanningVariant, ok := changedProps["Scanning"]; ok {
|
|
if scanning, ok := scanningVariant.Value().(bool); ok {
|
|
sm.handleScanningChange(scanning)
|
|
}
|
|
}
|
|
|
|
// Check for ConnectedNetwork property change
|
|
if _, ok := changedProps["ConnectedNetwork"]; ok {
|
|
// Network connection changed, trigger state update
|
|
sm.handleConnectionChange()
|
|
}
|
|
}
|
|
|
|
// handleStateChange processes a state change
|
|
func (sm *SignalMonitor) handleStateChange(state StationState) {
|
|
sm.mu.RLock()
|
|
callback := sm.onStateChange
|
|
sm.mu.RUnlock()
|
|
|
|
if callback == nil {
|
|
return
|
|
}
|
|
|
|
// Get connected SSID if connected
|
|
ssid := ""
|
|
if state == StateConnected {
|
|
network, err := sm.station.GetConnectedNetwork()
|
|
if err == nil {
|
|
props, err := network.GetProperties()
|
|
if err == nil {
|
|
ssid = props.Name
|
|
}
|
|
}
|
|
}
|
|
|
|
callback(state, ssid)
|
|
}
|
|
|
|
// handleScanningChange processes scanning state changes
|
|
func (sm *SignalMonitor) handleScanningChange(scanning bool) {
|
|
// Detect scan completion (transition from true to false)
|
|
if sm.lastScanning && !scanning {
|
|
sm.mu.RLock()
|
|
callback := sm.onScanComplete
|
|
sm.mu.RUnlock()
|
|
|
|
if callback != nil {
|
|
callback()
|
|
}
|
|
}
|
|
|
|
sm.lastScanning = scanning
|
|
}
|
|
|
|
// handleConnectionChange processes connection changes
|
|
func (sm *SignalMonitor) handleConnectionChange() {
|
|
// Get current state and trigger state change callback
|
|
state, err := sm.station.GetState()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
sm.handleStateChange(state)
|
|
}
|