diff --git a/cmd/repeater/main.go b/cmd/repeater/main.go index aee995e..3fd1e66 100644 --- a/cmd/repeater/main.go +++ b/cmd/repeater/main.go @@ -25,7 +25,7 @@ func main() { application := app.New(assets) // Initialize the application - if err := application.Initialize(); err != nil { + if err := application.Initialize(cfg); err != nil { log.Fatalf("Failed to initialize application: %v", err) } defer application.Shutdown() diff --git a/internal/api/handlers/handlers.go b/internal/api/handlers/handlers.go index 49d6c3a..4bb2302 100644 --- a/internal/api/handlers/handlers.go +++ b/internal/api/handlers/handlers.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/gin-gonic/gin" + "github.com/nemunaire/repeater/internal/config" "github.com/nemunaire/repeater/internal/device" "github.com/nemunaire/repeater/internal/hotspot" "github.com/nemunaire/repeater/internal/logging" @@ -103,8 +104,8 @@ func ToggleHotspot(c *gin.Context, status *models.SystemStatus) { } // GetDevices returns connected devices -func GetDevices(c *gin.Context) { - devices, err := device.GetConnectedDevices() +func GetDevices(c *gin.Context, cfg *config.Config) { + devices, err := device.GetConnectedDevices(cfg) if err != nil { logging.AddLog("Système", "Erreur lors de la récupération des appareils: "+err.Error()) c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur lors de la récupération des appareils"}) diff --git a/internal/api/router.go b/internal/api/router.go index 5558494..a06c328 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -7,11 +7,12 @@ import ( "github.com/gin-gonic/gin" "github.com/nemunaire/repeater/internal/api/handlers" + "github.com/nemunaire/repeater/internal/config" "github.com/nemunaire/repeater/internal/models" ) // SetupRouter creates and configures the Gin router -func SetupRouter(status *models.SystemStatus, assets embed.FS) *gin.Engine { +func SetupRouter(status *models.SystemStatus, 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) @@ -38,7 +39,9 @@ func SetupRouter(status *models.SystemStatus, assets embed.FS) *gin.Engine { } // Device endpoints - api.GET("/devices", handlers.GetDevices) + api.GET("/devices", func(c *gin.Context) { + handlers.GetDevices(c, cfg) + }) // Status endpoint api.GET("/status", func(c *gin.Context) { diff --git a/internal/app/app.go b/internal/app/app.go index 39181e6..e4d5d0e 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -7,6 +7,7 @@ import ( "time" "github.com/nemunaire/repeater/internal/api" + "github.com/nemunaire/repeater/internal/config" "github.com/nemunaire/repeater/internal/device" "github.com/nemunaire/repeater/internal/logging" "github.com/nemunaire/repeater/internal/models" @@ -19,6 +20,7 @@ type App struct { StatusMutex sync.RWMutex StartTime time.Time Assets embed.FS + Config *config.Config } // New creates a new application instance @@ -38,9 +40,12 @@ func New(assets embed.FS) *App { } // Initialize initializes the application -func (a *App) Initialize() error { +func (a *App) Initialize(cfg *config.Config) error { + // Store config reference + a.Config = cfg + // Initialize WiFi D-Bus connection - if err := wifi.Initialize(); err != nil { + if err := wifi.Initialize(cfg.WifiInterface); err != nil { return err } @@ -54,7 +59,7 @@ func (a *App) Initialize() error { // Run starts the HTTP server func (a *App) Run(addr string) error { - router := api.SetupRouter(&a.Status, a.Assets) + router := api.SetupRouter(&a.Status, a.Config, a.Assets) logging.AddLog("Système", "Serveur API démarré sur "+addr) return router.Run(addr) @@ -88,7 +93,7 @@ func (a *App) periodicDeviceUpdate() { defer ticker.Stop() for range ticker.C { - devices, err := device.GetConnectedDevices() + devices, err := device.GetConnectedDevices(a.Config) if err != nil { log.Printf("Error getting connected devices: %v", err) continue diff --git a/internal/config/cli.go b/internal/config/cli.go index 0cb90c6..f61db13 100644 --- a/internal/config/cli.go +++ b/internal/config/cli.go @@ -7,6 +7,10 @@ import ( // declareFlags registers flags for the structure Options. func declareFlags(o *Config) { flag.StringVar(&o.Bind, "bind", ":8081", "Bind port/socket") + flag.StringVar(&o.WifiInterface, "wifi-interface", "wlan0", "WiFi interface name") + flag.BoolVar(&o.UseARPDiscovery, "use-arp-discovery", true, "Use ARP table for device discovery instead of DHCP leases") + flag.StringVar(&o.DHCPLeasesPath, "dhcp-leases-path", "/var/lib/dhcp/dhcpd.leases", "Path to DHCP leases file") + flag.StringVar(&o.ARPTablePath, "arp-table-path", "/proc/net/arp", "Path to ARP table file") } // parseCLI parse the flags and treats extra args as configuration filename. diff --git a/internal/config/config.go b/internal/config/config.go index aed3693..329354b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -9,7 +9,11 @@ import ( ) type Config struct { - Bind string + Bind string + WifiInterface string + UseARPDiscovery bool + DHCPLeasesPath string + ARPTablePath string } // ConsolidateConfig fills an Options struct by reading configuration from @@ -19,7 +23,11 @@ type Config struct { func ConsolidateConfig() (opts *Config, err error) { // Define defaults options opts = &Config{ - Bind: ":8080", + Bind: ":8080", + WifiInterface: "wlan0", + UseARPDiscovery: true, + DHCPLeasesPath: "/var/lib/dhcp/dhcpd.leases", + ARPTablePath: "/proc/net/arp", } declareFlags(opts) diff --git a/internal/device/device.go b/internal/device/device.go index 5eaaf06..f57dfa5 100644 --- a/internal/device/device.go +++ b/internal/device/device.go @@ -2,25 +2,71 @@ package device import ( "bufio" + "fmt" + "net" "os" "os/exec" "regexp" "strings" + "github.com/nemunaire/repeater/internal/config" "github.com/nemunaire/repeater/internal/models" ) +// ARPEntry represents an entry in the ARP table +type ARPEntry struct { + IP net.IP + HWType int + Flags int + HWAddress net.HardwareAddr + Mask string + Device string +} + // GetConnectedDevices returns a list of connected devices -func GetConnectedDevices() ([]models.ConnectedDevice, error) { +func GetConnectedDevices(cfg *config.Config) ([]models.ConnectedDevice, error) { + if cfg.UseARPDiscovery { + return getDevicesFromARP(cfg) + } + return getDevicesFromDHCP(cfg) +} + +// getDevicesFromARP discovers devices using ARP table +func getDevicesFromARP(cfg *config.Config) ([]models.ConnectedDevice, error) { var devices []models.ConnectedDevice - // Read DHCP leases - leases, err := parseDHCPLeases() + arpEntries, err := parseARPTable(cfg.ARPTablePath) if err != nil { return devices, err } - // Get ARP information + for _, entry := range arpEntries { + // Only include entries with valid flags (2 = COMPLETE, 6 = COMPLETE|PERM) + if entry.Flags == 2 || entry.Flags == 6 { + device := models.ConnectedDevice{ + Name: "", // No hostname available from ARP + MAC: entry.HWAddress.String(), + IP: entry.IP.String(), + Type: guessDeviceType("", entry.HWAddress.String()), + } + devices = append(devices, device) + } + } + + return devices, nil +} + +// getDevicesFromDHCP discovers devices using DHCP leases and ARP validation +func getDevicesFromDHCP(cfg *config.Config) ([]models.ConnectedDevice, error) { + var devices []models.ConnectedDevice + + // Read DHCP leases + leases, err := parseDHCPLeases(cfg.DHCPLeasesPath) + if err != nil { + return devices, err + } + + // Get ARP information for validation arpInfo, err := getARPInfo() if err != nil { return devices, err @@ -43,11 +89,57 @@ func GetConnectedDevices() ([]models.ConnectedDevice, error) { return devices, nil } +// parseARPTable reads and parses ARP table from /proc/net/arp format +func parseARPTable(path string) ([]ARPEntry, error) { + var entries []ARPEntry + + content, err := os.ReadFile(path) + if err != nil { + return entries, err + } + + for _, line := range strings.Split(string(content), "\n") { + fields := strings.Fields(line) + if len(fields) > 5 { + var entry ARPEntry + + // Parse HWType (hex format) + if _, err := fmt.Sscanf(fields[1], "0x%x", &entry.HWType); err != nil { + continue + } + + // Parse Flags (hex format) + if _, err := fmt.Sscanf(fields[2], "0x%x", &entry.Flags); err != nil { + continue + } + + // Parse IP address + entry.IP = net.ParseIP(fields[0]) + if entry.IP == nil { + continue + } + + // Parse MAC address + entry.HWAddress, err = net.ParseMAC(fields[3]) + if err != nil { + continue + } + + entry.Mask = fields[4] + entry.Device = fields[5] + + entries = append(entries, entry) + } + } + + return entries, nil +} + // parseDHCPLeases reads and parses DHCP lease file -func parseDHCPLeases() ([]models.DHCPLease, error) { +func parseDHCPLeases(path string) ([]models.DHCPLease, error) { var leases []models.DHCPLease - file, err := os.Open("/var/lib/dhcp/dhcpd.leases") + file, err := os.Open(path) if err != nil { return leases, err } diff --git a/internal/wifi/wifi.go b/internal/wifi/wifi.go index e10731b..e7cb20d 100644 --- a/internal/wifi/wifi.go +++ b/internal/wifi/wifi.go @@ -12,8 +12,7 @@ import ( ) const ( - WLAN_INTERFACE = "wlan0" - WPA_CONF = "/etc/wpa_supplicant/wpa_supplicant.conf" + WPA_CONF = "/etc/wpa_supplicant/wpa_supplicant.conf" // D-Bus constants for wpa_supplicant WPA_SUPPLICANT_SERVICE = "fi.w1.wpa_supplicant1" @@ -25,12 +24,14 @@ const ( ) var ( + wlanInterface string dbusConn *dbus.Conn wpaSupplicant dbus.BusObject ) // Initialize initializes the WiFi service with D-Bus connection -func Initialize() error { +func Initialize(interfaceName string) error { + wlanInterface = interfaceName var err error dbusConn, err = dbus.SystemBus() if err != nil { @@ -55,15 +56,29 @@ func ScanNetworks() ([]models.WiFiNetwork, error) { return nil, fmt.Errorf("impossible d'obtenir l'interface WiFi: %v", err) } - // Trigger a scan wifiInterface := dbusConn.Object(WPA_SUPPLICANT_SERVICE, interfacePath) - call := wifiInterface.Call(WPA_INTERFACE_IFACE+".Scan", 0, map[string]dbus.Variant{"Type": dbus.MakeVariant("active")}) - if call.Err != nil { - return nil, fmt.Errorf("erreur lors du scan: %v", call.Err) - } - // Wait for scan to complete - time.Sleep(2 * time.Second) + // 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 + 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) + } + } // Retrieve BSS list bssePaths, err := wifiInterface.GetProperty(WPA_INTERFACE_IFACE + ".BSSs") @@ -224,7 +239,7 @@ func IsConnected() bool { // IsConnectedLegacy checks if WiFi is connected using iwconfig (fallback) func IsConnectedLegacy() bool { - cmd := exec.Command("iwconfig", WLAN_INTERFACE) + cmd := exec.Command("iwconfig", wlanInterface) output, err := cmd.Output() if err != nil { return false @@ -236,7 +251,7 @@ func IsConnectedLegacy() bool { // 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, WLAN_INTERFACE).Store(&interfacePath) + 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) }