commit 8c8986e8dccae1325f6aa5c6c1787c9095abc14a Author: Pierre-Olivier Mercier Date: Thu Jul 17 13:32:39 2025 +0200 Initial version, from Claude diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e147207 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +repeater \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0f2fed9 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module git.nemunai.re/nemunaire/repeater + +go 1.24.4 + +require ( + github.com/godbus/dbus/v5 v5.1.0 + github.com/gorilla/mux v1.8.1 + github.com/gorilla/websocket v1.5.3 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0546936 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= diff --git a/main.go b/main.go new file mode 100644 index 0000000..d31d978 --- /dev/null +++ b/main.go @@ -0,0 +1,830 @@ +package main + +import ( + "bufio" + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "os/exec" + "regexp" + "sort" + "strings" + "sync" + "time" + + "github.com/godbus/dbus/v5" + "github.com/gorilla/mux" + "github.com/gorilla/websocket" +) + +// Structures de données +type WiFiNetwork struct { + SSID string `json:"ssid"` + Signal int `json:"signal"` + Security string `json:"security"` + Channel int `json:"channel"` + BSSID string `json:"bssid"` +} + +type ConnectedDevice struct { + Name string `json:"name"` + Type string `json:"type"` + MAC string `json:"mac"` + IP string `json:"ip"` +} + +type HotspotConfig struct { + SSID string `json:"ssid"` + Password string `json:"password"` + Channel int `json:"channel"` +} + +type SystemStatus struct { + Connected bool `json:"connected"` + ConnectedSSID string `json:"connectedSSID"` + HotspotEnabled bool `json:"hotspotEnabled"` + ConnectedCount int `json:"connectedCount"` + DataUsage float64 `json:"dataUsage"` + Uptime int64 `json:"uptime"` + ConnectedDevices []ConnectedDevice `json:"connectedDevices"` +} + +type WiFiConnectRequest struct { + SSID string `json:"ssid"` + Password string `json:"password"` +} + +type LogEntry struct { + Timestamp time.Time `json:"timestamp"` + Source string `json:"source"` + Message string `json:"message"` +} + +// Variables globales +var ( + currentStatus SystemStatus + statusMutex sync.RWMutex + logEntries []LogEntry + logMutex sync.RWMutex + websocketClients = make(map[*websocket.Conn]bool) + clientsMutex sync.RWMutex + upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, + } + startTime = time.Now() + dbusConn *dbus.Conn + wpaSupplicant dbus.BusObject +) + +const ( + WLAN_INTERFACE = "wlan0" + AP_INTERFACE = "wlan1" + HOSTAPD_CONF = "/etc/hostapd/hostapd.conf" + WPA_CONF = "/etc/wpa_supplicant/wpa_supplicant.conf" + + // D-Bus constantes pour 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" +) + +func main() { + // Initialiser D-Bus + var err error + dbusConn, err = dbus.SystemBus() + if err != nil { + log.Fatalf("Erreur de connexion D-Bus: %v", err) + } + defer dbusConn.Close() + + wpaSupplicant = dbusConn.Object(WPA_SUPPLICANT_SERVICE, dbus.ObjectPath(WPA_SUPPLICANT_PATH)) + + // Initialiser le statut système + initializeStatus() + + // Démarrer les tâches périodiques + go periodicStatusUpdate() + go periodicDeviceUpdate() + + // Configuration du routeur + r := mux.NewRouter() + + // Routes API + api := r.PathPrefix("/api").Subrouter() + api.HandleFunc("/wifi/scan", scanWiFiHandler).Methods("GET") + api.HandleFunc("/wifi/connect", connectWiFiHandler).Methods("POST") + api.HandleFunc("/wifi/disconnect", disconnectWiFiHandler).Methods("POST") + api.HandleFunc("/hotspot/config", configureHotspotHandler).Methods("POST") + api.HandleFunc("/hotspot/toggle", toggleHotspotHandler).Methods("POST") + api.HandleFunc("/devices", getDevicesHandler).Methods("GET") + api.HandleFunc("/status", getStatusHandler).Methods("GET") + api.HandleFunc("/logs", getLogsHandler).Methods("GET") + api.HandleFunc("/logs", clearLogsHandler).Methods("DELETE") + + // WebSocket pour les logs en temps réel + r.HandleFunc("/ws/logs", websocketHandler) + + // Servir les fichiers statiques + r.PathPrefix("/").Handler(http.FileServer(http.Dir("./static/"))) + + addLog("Système", "Serveur API démarré sur le port 8080") + log.Fatal(http.ListenAndServe(":8080", r)) +} + +func initializeStatus() { + statusMutex.Lock() + defer statusMutex.Unlock() + + currentStatus = SystemStatus{ + Connected: false, + ConnectedSSID: "", + HotspotEnabled: true, + ConnectedCount: 0, + DataUsage: 0.0, + Uptime: 0, + } +} + +// Handlers API + +func scanWiFiHandler(w http.ResponseWriter, r *http.Request) { + networks, err := scanWiFiNetworks() + if err != nil { + addLog("WiFi", fmt.Sprintf("Erreur lors du scan: %v", err)) + http.Error(w, "Erreur lors du scan WiFi", http.StatusInternalServerError) + return + } + + addLog("WiFi", fmt.Sprintf("Scan terminé - %d réseaux trouvés", len(networks))) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(networks) +} + +func connectWiFiHandler(w http.ResponseWriter, r *http.Request) { + var req WiFiConnectRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Données invalides", http.StatusBadRequest) + return + } + + addLog("WiFi", fmt.Sprintf("Tentative de connexion à %s", req.SSID)) + + err := connectToWiFiDBus(req.SSID, req.Password) + if err != nil { + addLog("WiFi", fmt.Sprintf("Échec de connexion: %v", err)) + http.Error(w, fmt.Sprintf("Échec de connexion: %v", err), http.StatusInternalServerError) + return + } + + statusMutex.Lock() + currentStatus.Connected = true + currentStatus.ConnectedSSID = req.SSID + statusMutex.Unlock() + + addLog("WiFi", fmt.Sprintf("Connexion réussie à %s", req.SSID)) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) +} + +func disconnectWiFiHandler(w http.ResponseWriter, r *http.Request) { + addLog("WiFi", "Tentative de déconnexion") + + err := disconnectWiFiDBus() + if err != nil { + addLog("WiFi", fmt.Sprintf("Erreur de déconnexion: %v", err)) + http.Error(w, fmt.Sprintf("Erreur de déconnexion: %v", err), http.StatusInternalServerError) + return + } + + statusMutex.Lock() + currentStatus.Connected = false + currentStatus.ConnectedSSID = "" + statusMutex.Unlock() + + addLog("WiFi", "Déconnexion réussie") + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) +} + +func configureHotspotHandler(w http.ResponseWriter, r *http.Request) { + var config HotspotConfig + if err := json.NewDecoder(r.Body).Decode(&config); err != nil { + http.Error(w, "Données invalides", http.StatusBadRequest) + return + } + + err := configureHotspot(config) + if err != nil { + addLog("Hotspot", fmt.Sprintf("Erreur de configuration: %v", err)) + http.Error(w, fmt.Sprintf("Erreur de configuration: %v", err), http.StatusInternalServerError) + return + } + + addLog("Hotspot", fmt.Sprintf("Configuration mise à jour: %s (Canal %d)", config.SSID, config.Channel)) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) +} + +func toggleHotspotHandler(w http.ResponseWriter, r *http.Request) { + statusMutex.Lock() + currentStatus.HotspotEnabled = !currentStatus.HotspotEnabled + enabled := currentStatus.HotspotEnabled + statusMutex.Unlock() + + var err error + if enabled { + err = startHotspot() + addLog("Hotspot", "Hotspot activé") + } else { + err = stopHotspot() + addLog("Hotspot", "Hotspot désactivé") + } + + if err != nil { + addLog("Hotspot", fmt.Sprintf("Erreur: %v", err)) + http.Error(w, fmt.Sprintf("Erreur: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]bool{"enabled": enabled}) +} + +func getDevicesHandler(w http.ResponseWriter, r *http.Request) { + devices, err := getConnectedDevices() + if err != nil { + addLog("Système", fmt.Sprintf("Erreur lors de la récupération des appareils: %v", err)) + http.Error(w, "Erreur lors de la récupération des appareils", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(devices) +} + +func getStatusHandler(w http.ResponseWriter, r *http.Request) { + statusMutex.RLock() + status := currentStatus + status.Uptime = int64(time.Since(startTime).Seconds()) + statusMutex.RUnlock() + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(status) +} + +func getLogsHandler(w http.ResponseWriter, r *http.Request) { + logMutex.RLock() + logs := make([]LogEntry, len(logEntries)) + copy(logs, logEntries) + logMutex.RUnlock() + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(logs) +} + +func clearLogsHandler(w http.ResponseWriter, r *http.Request) { + logMutex.Lock() + logEntries = []LogEntry{} + logMutex.Unlock() + + addLog("Système", "Logs effacés") + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) +} + +// Fonctions WiFi avec D-Bus + +func scanWiFiNetworks() ([]WiFiNetwork, error) { + interfacePath, err := getWiFiInterfacePath() + if err != nil { + return nil, fmt.Errorf("impossible d'obtenir l'interface WiFi: %v", err) + } + + // Déclencher un scan + wifiInterface := dbusConn.Object(WPA_SUPPLICANT_SERVICE, interfacePath) + call := wifiInterface.Call(WPA_INTERFACE_IFACE+".Scan", 0, map[string]dbus.Variant{}) + if call.Err != nil { + return nil, fmt.Errorf("erreur lors du scan: %v", call.Err) + } + + // Attendre un peu pour que le scan se termine + time.Sleep(2 * time.Second) + + // Récupérer la liste des BSS + var bssePaths []dbus.ObjectPath + err = wifiInterface.Call(WPA_INTERFACE_IFACE+".Get", 0, WPA_INTERFACE_IFACE, "BSSs").Store(&bssePaths) + if err != nil { + return nil, fmt.Errorf("erreur lors de la récupération des BSS: %v", err) + } + + var networks []WiFiNetwork + seenSSIDs := make(map[string]bool) + + for _, bssPath := range bssePaths { + bss := dbusConn.Object(WPA_SUPPLICANT_SERVICE, bssPath) + + // Récupérer les propriétés du BSS + var props map[string]dbus.Variant + err = bss.Call("org.freedesktop.DBus.Properties.GetAll", 0, WPA_BSS_IFACE).Store(&props) + if err != nil { + continue + } + + network := WiFiNetwork{} + + // Extraire SSID + if ssidBytes, ok := props["SSID"].Value().([]byte); ok { + network.SSID = string(ssidBytes) + } + + // Éviter les doublons + if network.SSID == "" || seenSSIDs[network.SSID] { + continue + } + seenSSIDs[network.SSID] = true + + // Extraire 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]) + } + + // Extraire la force du signal + if signal, ok := props["Signal"].Value().(int16); ok { + network.Signal = signalToStrength(int(signal)) + } + + // Extraire la fréquence et calculer le canal + if frequency, ok := props["Frequency"].Value().(uint16); ok { + network.Channel = frequencyToChannel(int(frequency)) + } + + // Déterminer la sécurité + 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) + } + + // Trier par force du signal + sort.Slice(networks, func(i, j int) bool { + return networks[i].Signal > networks[j].Signal + }) + + return networks, nil +} + +func connectToWiFiDBus(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) + + // Créer un nouveau réseau + networkConfig := map[string]dbus.Variant{ + "ssid": dbus.MakeVariant(ssid), + } + + if password != "" { + networkConfig["psk"] = dbus.MakeVariant(password) + } + + var networkPath dbus.ObjectPath + err = wifiInterface.Call(WPA_INTERFACE_IFACE+".AddNetwork", 0, networkConfig).Store(&networkPath) + if err != nil { + return fmt.Errorf("erreur lors de l'ajout du réseau: %v", err) + } + + // Sélectionner le réseau + 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) + } + + // Attendre la connexion + for i := 0; i < 20; i++ { + time.Sleep(500 * time.Millisecond) + if isConnectedDBus() { + return nil + } + } + + return fmt.Errorf("timeout lors de la connexion") +} + +func disconnectWiFiDBus() 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) + + // Déconnecter + err = wifiInterface.Call(WPA_INTERFACE_IFACE+".Disconnect", 0).Err + if err != nil { + return fmt.Errorf("erreur lors de la déconnexion: %v", err) + } + + // Supprimer tous les réseaux + 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 +} + +func getWiFiInterfacePath() (dbus.ObjectPath, error) { + var interfaces []dbus.ObjectPath + err := wpaSupplicant.Call(WPA_SUPPLICANT_IFACE+".Get", 0, WPA_SUPPLICANT_IFACE, "Interfaces").Store(&interfaces) + if err != nil { + return "", fmt.Errorf("erreur lors de la récupération des interfaces: %v", err) + } + + for _, interfacePath := range interfaces { + wifiInterface := dbusConn.Object(WPA_SUPPLICANT_SERVICE, interfacePath) + var ifname string + err = wifiInterface.Call(WPA_INTERFACE_IFACE+".Get", 0, WPA_INTERFACE_IFACE, "Ifname").Store(&ifname) + if err != nil { + continue + } + + if ifname == WLAN_INTERFACE { + return interfacePath, nil + } + } + + return "", fmt.Errorf("interface %s non trouvée", WLAN_INTERFACE) +} + +func isConnectedDBus() bool { + interfacePath, err := getWiFiInterfacePath() + 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" +} + +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 + } + return 0 +} + +func signalToStrength(level int) int { + if level >= -30 { + return 5 + } else if level >= -50 { + return 4 + } else if level >= -60 { + return 3 + } else if level >= -70 { + return 2 + } else { + return 1 + } +} + +func connectToWiFi(ssid, password string) error { + // Créer la configuration wpa_supplicant + config := fmt.Sprintf(`ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev +update_config=1 +country=FR + +network={ + ssid="%s" + psk="%s" +} +`, ssid, password) + + err := os.WriteFile(WPA_CONF, []byte(config), 0600) + if err != nil { + return err + } + + // Redémarrer wpa_supplicant + cmd := exec.Command("systemctl", "restart", "wpa_supplicant") + if err := cmd.Run(); err != nil { + return err + } + + // Attendre la connexion + for i := 0; i < 10; i++ { + time.Sleep(1 * time.Second) + if isConnected() { + return nil + } + } + + return fmt.Errorf("timeout lors de la connexion") +} + +func isConnected() bool { + cmd := exec.Command("iwconfig", WLAN_INTERFACE) + output, err := cmd.Output() + if err != nil { + return false + } + + return strings.Contains(string(output), "Access Point:") +} + +// Fonctions Hotspot + +func configureHotspot(config HotspotConfig) error { + hostapdConfig := fmt.Sprintf(`interface=%s +driver=nl80211 +ssid=%s +hw_mode=g +channel=%d +wmm_enabled=0 +macaddr_acl=0 +auth_algs=1 +ignore_broadcast_ssid=0 +wpa=2 +wpa_passphrase=%s +wpa_key_mgmt=WPA-PSK +wpa_pairwise=TKIP +rsn_pairwise=CCMP +`, AP_INTERFACE, config.SSID, config.Channel, config.Password) + + return os.WriteFile(HOSTAPD_CONF, []byte(hostapdConfig), 0644) +} + +func startHotspot() error { + cmd := exec.Command("systemctl", "start", "hostapd") + return cmd.Run() +} + +func stopHotspot() error { + cmd := exec.Command("systemctl", "stop", "hostapd") + return cmd.Run() +} + +// Fonctions pour les appareils connectés + +func getConnectedDevices() ([]ConnectedDevice, error) { + var devices []ConnectedDevice + + // Lire les baux DHCP + leases, err := parseDHCPLeases() + if err != nil { + return devices, err + } + + // Obtenir les informations ARP + arpInfo, err := getARPInfo() + if err != nil { + return devices, err + } + + for _, lease := range leases { + device := ConnectedDevice{ + Name: lease.Hostname, + MAC: lease.MAC, + IP: lease.IP, + Type: guessDeviceType(lease.Hostname, lease.MAC), + } + + // Vérifier si l'appareil est toujours connecté via ARP + if _, exists := arpInfo[lease.IP]; exists { + devices = append(devices, device) + } + } + + return devices, nil +} + +type DHCPLease struct { + IP string + MAC string + Hostname string +} + +func parseDHCPLeases() ([]DHCPLease, error) { + var leases []DHCPLease + + file, err := os.Open("/var/lib/dhcp/dhcpd.leases") + if err != nil { + return leases, err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + var currentLease DHCPLease + + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + if strings.HasPrefix(line, "lease ") { + ip := strings.Fields(line)[1] + currentLease = DHCPLease{IP: ip} + } else if strings.Contains(line, "hardware ethernet") { + mac := strings.Fields(line)[2] + mac = strings.TrimSuffix(mac, ";") + currentLease.MAC = mac + } else if strings.Contains(line, "client-hostname") { + hostname := strings.Fields(line)[1] + hostname = strings.Trim(hostname, `";`) + currentLease.Hostname = hostname + } else if line == "}" { + if currentLease.IP != "" && currentLease.MAC != "" { + leases = append(leases, currentLease) + } + currentLease = DHCPLease{} + } + } + + return leases, nil +} + +func getARPInfo() (map[string]string, error) { + arpInfo := make(map[string]string) + + cmd := exec.Command("arp", "-a") + output, err := cmd.Output() + if err != nil { + return arpInfo, err + } + + lines := strings.Split(string(output), "\n") + for _, line := range lines { + if matches := regexp.MustCompile(`\(([^)]+)\) at ([0-9a-fA-F:]{17})`).FindStringSubmatch(line); len(matches) > 2 { + ip := matches[1] + mac := matches[2] + arpInfo[ip] = mac + } + } + + return arpInfo, nil +} + +func guessDeviceType(hostname, mac string) string { + hostname = strings.ToLower(hostname) + + if strings.Contains(hostname, "iphone") || strings.Contains(hostname, "android") { + return "mobile" + } else if strings.Contains(hostname, "ipad") || strings.Contains(hostname, "tablet") { + return "tablet" + } else if strings.Contains(hostname, "macbook") || strings.Contains(hostname, "laptop") { + return "laptop" + } + + // Deviner par préfixe MAC (OUI) + macPrefix := strings.ToUpper(mac[:8]) + switch macPrefix { + case "00:50:56", "00:0C:29", "00:05:69": // VMware + return "laptop" + case "08:00:27": // VirtualBox + return "laptop" + default: + return "mobile" + } +} + +// Fonctions de logging + +func addLog(source, message string) { + logMutex.Lock() + entry := LogEntry{ + Timestamp: time.Now(), + Source: source, + Message: message, + } + logEntries = append(logEntries, entry) + + // Garder seulement les 100 derniers logs + if len(logEntries) > 100 { + logEntries = logEntries[len(logEntries)-100:] + } + logMutex.Unlock() + + // Envoyer aux clients WebSocket + broadcastToWebSockets(entry) + + // Log vers la console + log.Printf("[%s] %s", source, message) +} + +// WebSocket pour les logs en temps réel + +func websocketHandler(w http.ResponseWriter, r *http.Request) { + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Printf("Erreur WebSocket: %v", err) + return + } + defer conn.Close() + + clientsMutex.Lock() + websocketClients[conn] = true + clientsMutex.Unlock() + + defer func() { + clientsMutex.Lock() + delete(websocketClients, conn) + clientsMutex.Unlock() + }() + + // Envoyer les logs existants + logMutex.RLock() + for _, entry := range logEntries { + conn.WriteJSON(entry) + } + logMutex.RUnlock() + + // Maintenir la connexion + for { + _, _, err := conn.ReadMessage() + if err != nil { + break + } + } +} + +func broadcastToWebSockets(entry LogEntry) { + clientsMutex.RLock() + defer clientsMutex.RUnlock() + + for client := range websocketClients { + err := client.WriteJSON(entry) + if err != nil { + client.Close() + delete(websocketClients, client) + } + } +} + +// Tâches périodiques + +func periodicStatusUpdate() { + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for range ticker.C { + statusMutex.Lock() + currentStatus.Connected = isConnected() + if !currentStatus.Connected { + currentStatus.ConnectedSSID = "" + } + statusMutex.Unlock() + } +} + +func periodicDeviceUpdate() { + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + + for range ticker.C { + devices, err := getConnectedDevices() + if err != nil { + continue + } + + statusMutex.Lock() + currentStatus.ConnectedDevices = devices + currentStatus.ConnectedCount = len(devices) + statusMutex.Unlock() + } +} diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..010827a --- /dev/null +++ b/static/index.html @@ -0,0 +1,722 @@ + + + + + + WiFi Repeater Control + + + +
+
+

🌐 WiFi Repeater Control

+
+
+ En ligne +
+
+ +
+
+
0
+
Appareils connectés
+
+
+
0 MB
+
Données utilisées
+
+
+
00:00:00
+
Temps de fonctionnement
+
+
+ +
+
+

+ + + + Connexion WiFi Externe +

+ +
+ +
+ +
+
+ +
+ + +
+ + + + +
+ +
+

+ + + + Configuration Hotspot +

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + +
+
+ +
+
+

+ + + + Appareils connectés +

+ +
+ +
+
+ +
+

+ + + + Logs système +

+ +
+ +
+ + +
+
+
+ +
+ + + + \ No newline at end of file