diff --git a/cmd/repeater/static/app.js b/cmd/repeater/static/app.js index bd29eec..3813ab8 100644 --- a/cmd/repeater/static/app.js +++ b/cmd/repeater/static/app.js @@ -5,7 +5,11 @@ const appState = { autoScrollLogs: true, ws: null, reconnectAttempts: 0, - maxReconnectAttempts: 5 + maxReconnectAttempts: 5, + connectedSSID: null, + networks: [], + uptime: 0, + uptimeInterval: null }; // Initialize the application @@ -30,7 +34,8 @@ async function initializeApp() { // Start periodic updates startPeriodicUpdates(); - showToast('success', 'Connecté', 'Interface web chargée avec succès'); + // Start uptime counter + startUptimeCounter(); } // ===== API Functions ===== @@ -62,6 +67,7 @@ async function scanWifi() { const response = await fetch('/api/wifi/scan'); const networks = await response.json(); + appState.networks = networks; displayWifiNetworks(networks); showToast('success', 'Scan WiFi', `${networks.length} réseau(x) trouvé(s)`); } catch (error) { @@ -128,6 +134,10 @@ async function disconnectWifi() { if (response.ok) { showToast('success', 'Déconnecté', 'Déconnexion WiFi réussie'); await loadStatus(); + // Force refresh WiFi list to remove green highlighting + if (appState.networks.length > 0) { + displayWifiNetworks(appState.networks); + } } else { throw new Error('Disconnection failed'); } @@ -288,10 +298,22 @@ function updateStatusDisplay(status) { appState.hotspotEnabled = status.hotspotEnabled; + // Check if connectedSSID changed and refresh WiFi list if needed + const connectedSSIDChanged = appState.connectedSSID !== status.connectedSSID; + appState.connectedSSID = status.connectedSSID; + + if (connectedSSIDChanged && appState.networks.length > 0) { + displayWifiNetworks(appState.networks); + } + // Update stats document.getElementById('connectedDevices').textContent = status.connectedCount; document.getElementById('dataUsage').textContent = `${status.dataUsage.toFixed(1)} MB`; + + // Update uptime in state (will be incremented by the interval) + appState.uptime = status.uptime; document.getElementById('uptime').textContent = formatUptime(status.uptime); + document.getElementById('currentNetwork').textContent = status.connectedSSID || '-'; } @@ -307,15 +329,21 @@ function displayWifiNetworks(networks) { networks.forEach(network => { const wifiItem = document.createElement('div'); wifiItem.className = 'wifi-item'; + + // Mark the currently connected network + if (appState.connectedSSID && network.ssid === appState.connectedSSID) { + wifiItem.classList.add('connected'); + } + wifiItem.onclick = () => selectWifi(network, wifiItem); wifiItem.innerHTML = `
${escapeHtml(network.ssid)}
- ${network.security} - Canal ${network.channel} - ${network.bssid} + ${escapeHtml(network.security)} + Canal ${escapeHtml(String(network.channel))} + ${escapeHtml(network.bssid)}
@@ -533,3 +561,16 @@ function startPeriodicUpdates() { loadDevices(); }, 10000); } + +function startUptimeCounter() { + // Clear any existing interval + if (appState.uptimeInterval) { + clearInterval(appState.uptimeInterval); + } + + // Increment uptime every second + appState.uptimeInterval = setInterval(() => { + appState.uptime++; + document.getElementById('uptime').textContent = formatUptime(appState.uptime); + }, 1000); +} diff --git a/cmd/repeater/static/style.css b/cmd/repeater/static/style.css index 2dc82b0..e2de87c 100644 --- a/cmd/repeater/static/style.css +++ b/cmd/repeater/static/style.css @@ -130,6 +130,7 @@ body { gap: 1rem; align-items: center; transition: all 0.3s ease; + max-width: 100%; } .stat-card:hover { @@ -154,6 +155,7 @@ body { .stat-content { flex: 1; + min-width: 0; } .stat-value { @@ -162,6 +164,9 @@ body { color: var(--text-primary); line-height: 1; margin-bottom: 0.25rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .stat-label { @@ -413,6 +418,16 @@ body { border-left: 4px solid #667eea; } +.wifi-item.connected { + background: #d1fae5; + border-left-color: var(--success-color) !important; +} + +.wifi-item.connected .wifi-ssid { + color: var(--success-color); + font-weight: 700; +} + .wifi-item.loading { justify-content: center; color: var(--text-secondary); diff --git a/internal/app/app.go b/internal/app/app.go index e4d5d0e..3924f2a 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -3,6 +3,9 @@ package app import ( "embed" "log" + "os" + "strconv" + "strings" "sync" "time" @@ -71,6 +74,52 @@ func (a *App) Shutdown() { 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() { ticker := time.NewTicker(5 * time.Second) @@ -79,10 +128,16 @@ func (a *App) periodicStatusUpdate() { for range ticker.C { a.StatusMutex.Lock() a.Status.Connected = wifi.IsConnected() - if !a.Status.Connected { - a.Status.ConnectedSSID = "" + a.Status.ConnectedSSID = wifi.GetConnectedSSID() + a.Status.Uptime = getSystemUptime() + + // 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.Status.Uptime = int64(time.Since(a.StartTime).Seconds()) + a.StatusMutex.Unlock() } } diff --git a/internal/wifi/iwd/agent.go b/internal/wifi/iwd/agent.go new file mode 100644 index 0000000..3a53ba6 --- /dev/null +++ b/internal/wifi/iwd/agent.go @@ -0,0 +1,170 @@ +package iwd + +import ( + "fmt" + "sync" + + "github.com/godbus/dbus/v5" + "github.com/godbus/dbus/v5/introspect" +) + +const agentIntrospectXML = ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +` + +// Agent implements the net.connman.iwd.Agent interface for credential callbacks +type Agent struct { + conn *dbus.Conn + path dbus.ObjectPath + passphraseStore map[string]string + mu sync.RWMutex +} + +// NewAgent creates a new Agent instance +func NewAgent(conn *dbus.Conn, path dbus.ObjectPath) *Agent { + return &Agent{ + conn: conn, + path: path, + passphraseStore: make(map[string]string), + } +} + +// SetPassphrase stores a passphrase for a given network SSID +func (a *Agent) SetPassphrase(ssid, passphrase string) { + a.mu.Lock() + defer a.mu.Unlock() + a.passphraseStore[ssid] = passphrase +} + +// ClearPassphrase removes the stored passphrase for a network +func (a *Agent) ClearPassphrase(ssid string) { + a.mu.Lock() + defer a.mu.Unlock() + delete(a.passphraseStore, ssid) +} + +// Export registers the agent object on D-Bus +func (a *Agent) Export() error { + err := a.conn.Export(a, a.path, AgentInterface) + if err != nil { + return fmt.Errorf("failed to export agent: %v", err) + } + + err = a.conn.Export(introspect.Introspectable(agentIntrospectXML), a.path, "org.freedesktop.DBus.Introspectable") + if err != nil { + a.conn.Export(nil, a.path, AgentInterface) + return fmt.Errorf("failed to export introspection: %v", err) + } + + return nil +} + +// Unexport unregisters the agent from D-Bus +func (a *Agent) Unexport() { + a.conn.Export(nil, a.path, AgentInterface) + a.conn.Export(nil, a.path, "org.freedesktop.DBus.Introspectable") +} + +// getNetworkSSID queries the network object to get its SSID (Name property) +func (a *Agent) getNetworkSSID(networkPath dbus.ObjectPath) (string, error) { + obj := a.conn.Object(Service, networkPath) + variant, err := obj.GetProperty(NetworkInterface + ".Name") + if err != nil { + return "", fmt.Errorf("failed to get network name: %v", err) + } + + name, ok := variant.Value().(string) + if !ok { + return "", fmt.Errorf("network name is not a string") + } + + return name, nil +} + +// RequestPassphrase is called by iwd when connecting to PSK networks +func (a *Agent) RequestPassphrase(network dbus.ObjectPath) (string, *dbus.Error) { + fmt.Printf("[Agent] RequestPassphrase called for network: %s\n", network) + + ssid, err := a.getNetworkSSID(network) + if err != nil { + fmt.Printf("[Agent] Failed to get SSID: %v\n", err) + return "", dbus.MakeFailedError(fmt.Errorf("failed to get network SSID: %v", err)) + } + + fmt.Printf("[Agent] Network SSID: %s\n", ssid) + + a.mu.RLock() + passphrase, ok := a.passphraseStore[ssid] + a.mu.RUnlock() + + if !ok { + fmt.Printf("[Agent] No passphrase stored for SSID: %s\n", ssid) + return "", dbus.MakeFailedError(fmt.Errorf("no passphrase stored for network '%s'", ssid)) + } + + fmt.Printf("[Agent] Returning passphrase for SSID: %s\n", ssid) + return passphrase, nil +} + +// RequestPrivateKeyPassphrase is called for encrypted private keys +func (a *Agent) RequestPrivateKeyPassphrase(network dbus.ObjectPath) (string, *dbus.Error) { + // Not implemented for now + return "", dbus.MakeFailedError(fmt.Errorf("RequestPrivateKeyPassphrase not implemented")) +} + +// RequestUserNameAndPassword is called for enterprise networks +func (a *Agent) RequestUserNameAndPassword(network dbus.ObjectPath) (string, string, *dbus.Error) { + // Not implemented for now + return "", "", dbus.MakeFailedError(fmt.Errorf("RequestUserNameAndPassword not implemented")) +} + +// RequestUserPassword is called for enterprise networks with known username +func (a *Agent) RequestUserPassword(network dbus.ObjectPath, user string) (string, *dbus.Error) { + // Not implemented for now + return "", dbus.MakeFailedError(fmt.Errorf("RequestUserPassword not implemented")) +} + +// Cancel is called when a request is canceled +func (a *Agent) Cancel(reason string) *dbus.Error { + // Nothing to do, just acknowledge + return nil +} + +// Release is called when the agent is unregistered +func (a *Agent) Release() *dbus.Error { + // Cleanup if needed + a.mu.Lock() + a.passphraseStore = make(map[string]string) + a.mu.Unlock() + return nil +} diff --git a/internal/wifi/iwd/agentmanager.go b/internal/wifi/iwd/agentmanager.go new file mode 100644 index 0000000..7ea6c77 --- /dev/null +++ b/internal/wifi/iwd/agentmanager.go @@ -0,0 +1,39 @@ +package iwd + +import ( + "fmt" + + "github.com/godbus/dbus/v5" +) + +// AgentManager handles agent registration with iwd +type AgentManager struct { + conn *dbus.Conn + obj dbus.BusObject +} + +// NewAgentManager creates a new AgentManager instance +func NewAgentManager(conn *dbus.Conn) *AgentManager { + return &AgentManager{ + conn: conn, + obj: conn.Object(Service, "/net/connman/iwd"), + } +} + +// RegisterAgent registers an agent with iwd +func (am *AgentManager) RegisterAgent(agentPath dbus.ObjectPath) error { + err := am.obj.Call(AgentManagerInterface+".RegisterAgent", 0, agentPath).Err + if err != nil { + return fmt.Errorf("failed to register agent: %v", err) + } + return nil +} + +// UnregisterAgent unregisters an agent from iwd +func (am *AgentManager) UnregisterAgent(agentPath dbus.ObjectPath) error { + err := am.obj.Call(AgentManagerInterface+".UnregisterAgent", 0, agentPath).Err + if err != nil { + return fmt.Errorf("failed to unregister agent: %v", err) + } + return nil +} diff --git a/internal/wifi/iwd/manager.go b/internal/wifi/iwd/manager.go new file mode 100644 index 0000000..f636664 --- /dev/null +++ b/internal/wifi/iwd/manager.go @@ -0,0 +1,71 @@ +package iwd + +import ( + "fmt" + "strings" + + "github.com/godbus/dbus/v5" +) + +// Manager handles iwd object discovery via ObjectManager +type Manager struct { + conn *dbus.Conn + obj dbus.BusObject +} + +// NewManager creates a new Manager instance +func NewManager(conn *dbus.Conn) *Manager { + return &Manager{ + conn: conn, + obj: conn.Object(Service, dbus.ObjectPath(ManagerPath)), + } +} + +// GetManagedObjects returns all iwd managed objects +func (m *Manager) GetManagedObjects() (map[dbus.ObjectPath]map[string]map[string]dbus.Variant, error) { + var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant + err := m.obj.Call("org.freedesktop.DBus.ObjectManager.GetManagedObjects", 0).Store(&objects) + if err != nil { + return nil, fmt.Errorf("failed to get managed objects: %v", err) + } + return objects, nil +} + +// FindStation finds the Station object for the given interface name +func (m *Manager) FindStation(interfaceName string) (*Station, error) { + objects, err := m.GetManagedObjects() + if err != nil { + return nil, err + } + + // First, find the device with matching interface name + var devicePath dbus.ObjectPath + for path, interfaces := range objects { + if deviceProps, ok := interfaces[DeviceInterface]; ok { + if nameVariant, ok := deviceProps["Name"]; ok { + if name, ok := nameVariant.Value().(string); ok && name == interfaceName { + devicePath = path + break + } + } + } + } + + if devicePath == "" { + return nil, fmt.Errorf("device with interface '%s' not found", interfaceName) + } + + // Now find the station object under this device + // Station path is typically the same as device path or a child of it + for path, interfaces := range objects { + if _, ok := interfaces[StationInterface]; ok { + // Check if this station belongs to our device + // Station path should be the device path or start with it + if path == devicePath || strings.HasPrefix(string(path), string(devicePath)+"/") { + return NewStation(m.conn, path), nil + } + } + } + + return nil, fmt.Errorf("station for device '%s' not found", interfaceName) +} diff --git a/internal/wifi/iwd/network.go b/internal/wifi/iwd/network.go new file mode 100644 index 0000000..1563a95 --- /dev/null +++ b/internal/wifi/iwd/network.go @@ -0,0 +1,64 @@ +package iwd + +import ( + "fmt" + + "github.com/godbus/dbus/v5" +) + +// Network represents an iwd Network interface +type Network struct { + path dbus.ObjectPath + conn *dbus.Conn + obj dbus.BusObject +} + +// NewNetwork creates a new Network instance +func NewNetwork(conn *dbus.Conn, path dbus.ObjectPath) *Network { + return &Network{ + path: path, + conn: conn, + obj: conn.Object(Service, path), + } +} + +// GetProperties retrieves all network properties +func (n *Network) GetProperties() (*NetworkProperties, error) { + var props map[string]dbus.Variant + err := n.obj.Call("org.freedesktop.DBus.Properties.GetAll", 0, NetworkInterface).Store(&props) + if err != nil { + return nil, fmt.Errorf("failed to get network properties: %v", err) + } + + netProps := &NetworkProperties{} + + if nameVariant, ok := props["Name"]; ok { + if name, ok := nameVariant.Value().(string); ok { + netProps.Name = name + } + } + + if typeVariant, ok := props["Type"]; ok { + if netType, ok := typeVariant.Value().(string); ok { + netProps.Type = netType + } + } + + if connectedVariant, ok := props["Connected"]; ok { + if connected, ok := connectedVariant.Value().(bool); ok { + netProps.Connected = connected + } + } + + return netProps, nil +} + +// Connect initiates a connection to this network +// Credentials are provided via the registered agent's RequestPassphrase callback +func (n *Network) Connect() error { + err := n.obj.Call(NetworkInterface+".Connect", 0).Err + if err != nil { + return fmt.Errorf("connect failed: %v", err) + } + return nil +} diff --git a/internal/wifi/iwd/station.go b/internal/wifi/iwd/station.go new file mode 100644 index 0000000..7abed90 --- /dev/null +++ b/internal/wifi/iwd/station.go @@ -0,0 +1,137 @@ +package iwd + +import ( + "fmt" + + "github.com/godbus/dbus/v5" +) + +// Station represents an iwd Station interface +type Station struct { + path dbus.ObjectPath + conn *dbus.Conn + obj dbus.BusObject +} + +// NewStation creates a new Station instance +func NewStation(conn *dbus.Conn, path dbus.ObjectPath) *Station { + return &Station{ + path: path, + conn: conn, + obj: conn.Object(Service, path), + } +} + +// Scan triggers a network scan +func (s *Station) Scan() error { + err := s.obj.Call(StationInterface+".Scan", 0).Err + if err != nil { + return fmt.Errorf("scan failed: %v", err) + } + return nil +} + +// IsScanning checks if a scan is currently in progress +func (s *Station) IsScanning() (bool, error) { + prop, err := s.obj.GetProperty(StationInterface + ".Scanning") + if err != nil { + return false, fmt.Errorf("failed to get Scanning property: %v", err) + } + + scanning, ok := prop.Value().(bool) + if !ok { + return false, fmt.Errorf("Scanning property is not a boolean") + } + + return scanning, nil +} + +// GetOrderedNetworks returns networks sorted by signal strength +func (s *Station) GetOrderedNetworks() ([]NetworkInfo, error) { + var result []struct { + Path dbus.ObjectPath + Signal int16 + } + + err := s.obj.Call(StationInterface+".GetOrderedNetworks", 0).Store(&result) + if err != nil { + return nil, fmt.Errorf("failed to get ordered networks: %v", err) + } + + networks := make([]NetworkInfo, len(result)) + for i, r := range result { + networks[i] = NetworkInfo{ + Path: r.Path, + Signal: r.Signal, + } + } + + return networks, nil +} + +// GetState returns the current connection state +func (s *Station) GetState() (StationState, error) { + prop, err := s.obj.GetProperty(StationInterface + ".State") + if err != nil { + return "", fmt.Errorf("failed to get State property: %v", err) + } + + state, ok := prop.Value().(string) + if !ok { + return "", fmt.Errorf("State property is not a string") + } + + return StationState(state), nil +} + +// Disconnect disconnects from the current network +func (s *Station) Disconnect() error { + err := s.obj.Call(StationInterface+".Disconnect", 0).Err + if err != nil { + return fmt.Errorf("disconnect failed: %v", err) + } + return nil +} + +// GetNetwork finds and returns a Network object by SSID +func (s *Station) GetNetwork(ssid string) (*Network, error) { + networks, err := s.GetOrderedNetworks() + if err != nil { + return nil, err + } + + // Find the network with matching SSID + for _, netInfo := range networks { + network := NewNetwork(s.conn, netInfo.Path) + props, err := network.GetProperties() + if err != nil { + continue + } + + if props.Name == ssid { + return network, nil + } + } + + return nil, fmt.Errorf("network '%s' not found", ssid) +} + +// GetConnectedNetwork returns the currently connected network +func (s *Station) GetConnectedNetwork() (*Network, error) { + prop, err := s.obj.GetProperty(StationInterface + ".ConnectedNetwork") + if err != nil { + return nil, fmt.Errorf("failed to get ConnectedNetwork property: %v", err) + } + + path, ok := prop.Value().(dbus.ObjectPath) + if !ok { + return nil, fmt.Errorf("ConnectedNetwork property is not an ObjectPath") + } + + // Check if path is empty (not connected) + if path == "/" || path == "" { + return nil, fmt.Errorf("not connected to any network") + } + + return NewNetwork(s.conn, path), nil +} diff --git a/internal/wifi/iwd/types.go b/internal/wifi/iwd/types.go new file mode 100644 index 0000000..76624c4 --- /dev/null +++ b/internal/wifi/iwd/types.go @@ -0,0 +1,38 @@ +package iwd + +import "github.com/godbus/dbus/v5" + +const ( + // D-Bus service and interfaces + Service = "net.connman.iwd" + ManagerPath = "/" + DeviceInterface = "net.connman.iwd.Device" + StationInterface = "net.connman.iwd.Station" + NetworkInterface = "net.connman.iwd.Network" + AgentInterface = "net.connman.iwd.Agent" + AgentManagerInterface = "net.connman.iwd.AgentManager" +) + +// NetworkInfo represents a network with its signal strength +type NetworkInfo struct { + Path dbus.ObjectPath + Signal int16 // 100 * dBm (0 to -10000) +} + +// NetworkProperties holds network properties +type NetworkProperties struct { + Name string // SSID + Type string // "open", "wep", "psk", "8021x" + Connected bool +} + +// StationState represents the connection state +type StationState string + +const ( + StateConnected StationState = "connected" + StateDisconnected StationState = "disconnected" + StateConnecting StationState = "connecting" + StateDisconnecting StationState = "disconnecting" + StateRoaming StationState = "roaming" +) diff --git a/internal/wifi/wifi.go b/internal/wifi/wifi.go index e7cb20d..5dd6656 100644 --- a/internal/wifi/wifi.go +++ b/internal/wifi/wifi.go @@ -2,48 +2,67 @@ package wifi import ( "fmt" - "os/exec" "sort" "strings" "time" "github.com/godbus/dbus/v5" "github.com/nemunaire/repeater/internal/models" + "github.com/nemunaire/repeater/internal/wifi/iwd" ) const ( - WPA_CONF = "/etc/wpa_supplicant/wpa_supplicant.conf" - - // D-Bus constants for wpa_supplicant - WPA_SUPPLICANT_SERVICE = "fi.w1.wpa_supplicant1" - WPA_SUPPLICANT_PATH = "/fi/w1/wpa_supplicant1" - WPA_SUPPLICANT_IFACE = "fi.w1.wpa_supplicant1" - WPA_INTERFACE_IFACE = "fi.w1.wpa_supplicant1.Interface" - WPA_BSS_IFACE = "fi.w1.wpa_supplicant1.BSS" - WPA_NETWORK_IFACE = "fi.w1.wpa_supplicant1.Network" + AGENT_PATH = "/com/github/nemunaire/repeater/agent" ) var ( wlanInterface string dbusConn *dbus.Conn - wpaSupplicant dbus.BusObject + iwdManager *iwd.Manager + station *iwd.Station + agent *iwd.Agent + agentManager *iwd.AgentManager ) -// Initialize initializes the WiFi service with D-Bus connection +// Initialize initializes the WiFi service with iwd D-Bus connection func Initialize(interfaceName string) error { wlanInterface = interfaceName var err error + + // Connect to D-Bus dbusConn, err = dbus.SystemBus() if err != nil { - return fmt.Errorf("failed to connect to D-Bus: %v", err) + return fmt.Errorf("échec de connexion à D-Bus: %v", err) + } + + // Find station for interface + iwdManager = iwd.NewManager(dbusConn) + station, err = iwdManager.FindStation(interfaceName) + if err != nil { + return fmt.Errorf("impossible de trouver la station pour %s: %v", interfaceName, err) + } + + // Create and register agent for credential callbacks + agent = iwd.NewAgent(dbusConn, dbus.ObjectPath(AGENT_PATH)) + if err := agent.Export(); err != nil { + return fmt.Errorf("échec de l'export de l'agent: %v", err) + } + + agentManager = iwd.NewAgentManager(dbusConn) + if err := agentManager.RegisterAgent(dbus.ObjectPath(AGENT_PATH)); err != nil { + agent.Unexport() + return fmt.Errorf("échec de l'enregistrement de l'agent: %v", err) } - wpaSupplicant = dbusConn.Object(WPA_SUPPLICANT_SERVICE, dbus.ObjectPath(WPA_SUPPLICANT_PATH)) return nil } -// Close closes the D-Bus connection +// Close closes the D-Bus connection and unregisters the agent func Close() { + if agentManager != nil && agent != nil { + agentManager.UnregisterAgent(dbus.ObjectPath(AGENT_PATH)) + agent.Unexport() + } if dbusConn != nil { dbusConn.Close() } @@ -51,100 +70,52 @@ func Close() { // ScanNetworks scans for available WiFi networks func ScanNetworks() ([]models.WiFiNetwork, error) { - interfacePath, err := getWiFiInterfacePath() - if err != nil { - return nil, fmt.Errorf("impossible d'obtenir l'interface WiFi: %v", err) - } - - wifiInterface := dbusConn.Object(WPA_SUPPLICANT_SERVICE, interfacePath) - - // Check current scanning state - scanning, err := wifiInterface.GetProperty(WPA_INTERFACE_IFACE + ".Scanning") - if err == nil && scanning.Value().(bool) { - // Scan already in progress, wait for it to complete + // Check if already scanning + scanning, err := station.IsScanning() + if err == nil && scanning { time.Sleep(3 * time.Second) } else { - // Trigger a scan - call := wifiInterface.Call(WPA_INTERFACE_IFACE+".Scan", 0, map[string]dbus.Variant{"Type": dbus.MakeVariant("active")}) - if call.Err != nil { - // If scan is rejected, it might be too soon after a previous scan - // Try to use cached results instead - if strings.Contains(call.Err.Error(), "rejected") { - // Continue to retrieve existing BSS list - } else { - return nil, fmt.Errorf("erreur lors du scan: %v", call.Err) - } - } else { - // Wait for scan to complete - time.Sleep(2 * time.Second) + // Trigger scan + err := station.Scan() + if err != nil && !strings.Contains(err.Error(), "rejected") { + return nil, fmt.Errorf("erreur lors du scan: %v", err) } + time.Sleep(2 * time.Second) } - // Retrieve BSS list - bssePaths, err := wifiInterface.GetProperty(WPA_INTERFACE_IFACE + ".BSSs") + // Get ordered networks + networkInfos, err := station.GetOrderedNetworks() if err != nil { - return nil, fmt.Errorf("erreur lors de la récupération des BSS: %v", err) + return nil, fmt.Errorf("erreur lors de la récupération des réseaux: %v", err) } var networks []models.WiFiNetwork seenSSIDs := make(map[string]bool) - for _, bssPath := range bssePaths.Value().([]dbus.ObjectPath) { - bss := dbusConn.Object(WPA_SUPPLICANT_SERVICE, bssPath) - - // Get BSS properties - var props map[string]dbus.Variant - err = bss.Call("org.freedesktop.DBus.Properties.GetAll", 0, WPA_BSS_IFACE).Store(&props) + for _, netInfo := range networkInfos { + network := iwd.NewNetwork(dbusConn, netInfo.Path) + props, err := network.GetProperties() if err != nil { continue } - network := models.WiFiNetwork{} - - // Extract SSID - if ssidBytes, ok := props["SSID"].Value().([]byte); ok { - network.SSID = string(ssidBytes) - } - - // Skip duplicates and empty SSIDs - if network.SSID == "" || seenSSIDs[network.SSID] { + if props.Name == "" || seenSSIDs[props.Name] { continue } - seenSSIDs[network.SSID] = true + seenSSIDs[props.Name] = true - // Extract BSSID - if bssidBytes, ok := props["BSSID"].Value().([]byte); ok { - network.BSSID = fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", - bssidBytes[0], bssidBytes[1], bssidBytes[2], bssidBytes[3], bssidBytes[4], bssidBytes[5]) + wifiNet := models.WiFiNetwork{ + SSID: props.Name, + Signal: signalToStrength(int(netInfo.Signal) / 100), + Security: mapSecurityType(props.Type), + BSSID: generateSyntheticBSSID(props.Name), + Channel: 0, } - // Extract signal strength - if signal, ok := props["Signal"].Value().(int16); ok { - network.Signal = signalToStrength(int(signal)) - } - - // Extract frequency and calculate channel - if frequency, ok := props["Frequency"].Value().(uint16); ok { - network.Channel = frequencyToChannel(int(frequency)) - } - - // Determine security - if privacyVal, ok := props["Privacy"].Value().(bool); ok && privacyVal { - if wpaProps, ok := props["WPA"].Value().(map[string]dbus.Variant); ok && len(wpaProps) > 0 { - network.Security = "WPA" - } else if rsnProps, ok := props["RSN"].Value().(map[string]dbus.Variant); ok && len(rsnProps) > 0 { - network.Security = "WPA2" - } else { - network.Security = "WEP" - } - } else { - network.Security = "Open" - } - - networks = append(networks, network) + networks = append(networks, wifiNet) } - // Sort by signal strength + // Sort by signal strength (descending) sort.Slice(networks, func(i, j int) bool { return networks[i].Signal > networks[j].Signal }) @@ -152,37 +123,32 @@ func ScanNetworks() ([]models.WiFiNetwork, error) { return networks, nil } -// Connect connects to a WiFi network using D-Bus +// Connect connects to a WiFi network using iwd agent callback func Connect(ssid, password string) error { - interfacePath, err := getWiFiInterfacePath() - if err != nil { - return fmt.Errorf("impossible d'obtenir l'interface WiFi: %v", err) - } - - wifiInterface := dbusConn.Object(WPA_SUPPLICANT_SERVICE, interfacePath) - - // Create a new network - networkConfig := map[string]dbus.Variant{ - "ssid": dbus.MakeVariant(ssid), - } - + // Store passphrase in agent for callback if password != "" { - networkConfig["psk"] = dbus.MakeVariant(password) + agent.SetPassphrase(ssid, password) } - var networkPath dbus.ObjectPath - err = wifiInterface.Call(WPA_INTERFACE_IFACE+".AddNetwork", 0, networkConfig).Store(&networkPath) + // Ensure passphrase is cleared after connection attempt + defer func() { + if password != "" { + agent.ClearPassphrase(ssid) + } + }() + + // Get network object + network, err := station.GetNetwork(ssid) if err != nil { - return fmt.Errorf("erreur lors de l'ajout du réseau: %v", err) + return fmt.Errorf("réseau '%s' non trouvé: %v", ssid, err) } - // Select the network - err = wifiInterface.Call(WPA_INTERFACE_IFACE+".SelectNetwork", 0, networkPath).Err - if err != nil { - return fmt.Errorf("erreur lors de la sélection du réseau: %v", err) + // Connect - iwd will call agent.RequestPassphrase() if needed + if err := network.Connect(); err != nil { + return fmt.Errorf("erreur lors de la connexion: %v", err) } - // Wait for connection + // Poll for connection for i := 0; i < 20; i++ { time.Sleep(500 * time.Millisecond) if IsConnected() { @@ -195,81 +161,69 @@ func Connect(ssid, password string) error { // Disconnect disconnects from the current WiFi network func Disconnect() error { - interfacePath, err := getWiFiInterfacePath() - if err != nil { - return fmt.Errorf("impossible d'obtenir l'interface WiFi: %v", err) - } - - wifiInterface := dbusConn.Object(WPA_SUPPLICANT_SERVICE, interfacePath) - - // Disconnect - err = wifiInterface.Call(WPA_INTERFACE_IFACE+".Disconnect", 0).Err - if err != nil { + if err := station.Disconnect(); err != nil { return fmt.Errorf("erreur lors de la déconnexion: %v", err) } - - // Remove all networks - var networks []dbus.ObjectPath - err = wifiInterface.Call(WPA_INTERFACE_IFACE+".Get", 0, WPA_INTERFACE_IFACE, "Networks").Store(&networks) - if err == nil { - for _, networkPath := range networks { - wifiInterface.Call(WPA_INTERFACE_IFACE+".RemoveNetwork", 0, networkPath) - } - } - return nil } -// IsConnected checks if WiFi is connected using D-Bus +// IsConnected checks if WiFi is connected using iwd func IsConnected() bool { - interfacePath, err := getWiFiInterfacePath() + state, err := station.GetState() if err != nil { return false } - - wifiInterface := dbusConn.Object(WPA_SUPPLICANT_SERVICE, interfacePath) - var state string - err = wifiInterface.Call(WPA_INTERFACE_IFACE+".Get", 0, WPA_INTERFACE_IFACE, "State").Store(&state) - if err != nil { - return false - } - - return state == "completed" + return state == iwd.StateConnected } -// IsConnectedLegacy checks if WiFi is connected using iwconfig (fallback) -func IsConnectedLegacy() bool { - cmd := exec.Command("iwconfig", wlanInterface) - output, err := cmd.Output() +// GetConnectedSSID returns the SSID of the currently connected network +func GetConnectedSSID() string { + network, err := station.GetConnectedNetwork() if err != nil { - return false + return "" } - return strings.Contains(string(output), "Access Point:") + props, err := network.GetProperties() + if err != nil { + return "" + } + + return props.Name } -// getWiFiInterfacePath retrieves the D-Bus path for the WiFi interface -func getWiFiInterfacePath() (dbus.ObjectPath, error) { - var interfacePath dbus.ObjectPath - err := wpaSupplicant.Call(WPA_SUPPLICANT_IFACE+".GetInterface", 0, wlanInterface).Store(&interfacePath) - if err != nil { - return "", fmt.Errorf("erreur lors de la récupération des interfaces: %v", err) +// mapSecurityType maps iwd security types to display format +func mapSecurityType(iwdType string) string { + switch iwdType { + case "open": + return "Open" + case "wep": + return "WEP" + case "psk": + return "WPA2" + case "8021x": + return "WPA2" + default: + return "WPA2" } - - return interfacePath, nil } -// frequencyToChannel converts WiFi frequency to channel number -func frequencyToChannel(frequency int) int { - if frequency >= 2412 && frequency <= 2484 { - if frequency == 2484 { - return 14 - } - return (frequency-2412)/5 + 1 - } else if frequency >= 5170 && frequency <= 5825 { - return (frequency - 5000) / 5 +// generateSyntheticBSSID generates a consistent fake BSSID from SSID +func generateSyntheticBSSID(ssid string) string { + // Use a simple hash approach - consistent per SSID + hash := 0 + for _, c := range ssid { + hash = ((hash << 5) - hash) + int(c) } - return 0 + + // Generate 6 bytes for MAC address + b1 := byte((hash >> 0) & 0xff) + b2 := byte((hash >> 8) & 0xff) + b3 := byte((hash >> 16) & 0xff) + b4 := byte((hash >> 24) & 0xff) + b5 := byte(len(ssid) & 0xff) + b6 := byte((len(ssid) >> 8) & 0xff) + + return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", b1, b2, b3, b4, b5, b6) } // signalToStrength converts signal level (dBm) to strength (1-5)