Initial version, from Claude
This commit is contained in:
commit
8c8986e8dc
5 changed files with 1568 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
repeater
|
||||
9
go.mod
Normal file
9
go.mod
Normal file
|
|
@ -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
|
||||
)
|
||||
6
go.sum
Normal file
6
go.sum
Normal file
|
|
@ -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=
|
||||
830
main.go
Normal file
830
main.go
Normal file
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
722
static/index.html
Normal file
722
static/index.html
Normal file
|
|
@ -0,0 +1,722 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WiFi Repeater Control</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 20px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: #333;
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 20px;
|
||||
border-radius: 25px;
|
||||
font-weight: 500;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.status-online {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.status-offline {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 25px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.4em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #555;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-group input, .form-group select {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.form-group input:focus, .form-group select:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #dc3545;
|
||||
}
|
||||
|
||||
.wifi-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.wifi-item {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.wifi-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.wifi-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.wifi-item.selected {
|
||||
background-color: #e7f3ff;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.wifi-signal {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.signal-strength {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.signal-bars {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.signal-bar {
|
||||
width: 3px;
|
||||
background: #ccc;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.signal-bar.active {
|
||||
background: #28a745;
|
||||
}
|
||||
|
||||
.devices-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.device-card {
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.device-card:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.device-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: 0 auto 10px;
|
||||
fill: #667eea;
|
||||
}
|
||||
|
||||
.log-container {
|
||||
background: #1a1a1a;
|
||||
color: #00ff00;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.log-timestamp {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.9em;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.notification {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 15px 20px;
|
||||
border-radius: 10px;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
z-index: 1000;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.notification.show {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.notification.success {
|
||||
background: #28a745;
|
||||
}
|
||||
|
||||
.notification.error {
|
||||
background: #dc3545;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🌐 WiFi Repeater Control</h1>
|
||||
<div class="status-indicator" id="connectionStatus">
|
||||
<div class="status-dot"></div>
|
||||
<span>En ligne</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="connectedDevices">0</div>
|
||||
<div class="stat-label">Appareils connectés</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="dataUsage">0 MB</div>
|
||||
<div class="stat-label">Données utilisées</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" id="uptime">00:00:00</div>
|
||||
<div class="stat-label">Temps de fonctionnement</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h2>
|
||||
<svg class="icon" viewBox="0 0 24 24">
|
||||
<path d="M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.07 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z"/>
|
||||
</svg>
|
||||
Connexion WiFi Externe
|
||||
</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Réseaux disponibles</label>
|
||||
<div class="wifi-list" id="wifiList">
|
||||
<!-- Liste des réseaux WiFi sera remplie par JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="wifiPassword">Mot de passe</label>
|
||||
<input type="password" id="wifiPassword" placeholder="Entrez le mot de passe WiFi">
|
||||
</div>
|
||||
|
||||
<button class="btn" onclick="connectToWifi()">
|
||||
<span id="connectBtn">Se connecter</span>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-secondary" onclick="scanWifi()">
|
||||
<span id="scanBtn">Scanner les réseaux</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>
|
||||
<svg class="icon" viewBox="0 0 24 24">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
||||
</svg>
|
||||
Configuration Hotspot
|
||||
</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="hotspotName">Nom du réseau (SSID)</label>
|
||||
<input type="text" id="hotspotName" value="MyRepeater" placeholder="Nom du hotspot">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="hotspotPassword">Mot de passe</label>
|
||||
<input type="password" id="hotspotPassword" value="password123" placeholder="Mot de passe du hotspot">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="hotspotChannel">Canal</label>
|
||||
<select id="hotspotChannel">
|
||||
<option value="1">Canal 1</option>
|
||||
<option value="6" selected>Canal 6</option>
|
||||
<option value="11">Canal 11</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button class="btn" onclick="updateHotspot()">Mettre à jour</button>
|
||||
<button class="btn btn-danger" onclick="toggleHotspot()">
|
||||
<span id="hotspotBtn">Arrêter le hotspot</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h2>
|
||||
<svg class="icon" viewBox="0 0 24 24">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/>
|
||||
</svg>
|
||||
Appareils connectés
|
||||
</h2>
|
||||
|
||||
<div class="devices-grid" id="devicesList">
|
||||
<!-- Liste des appareils sera remplie par JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>
|
||||
<svg class="icon" viewBox="0 0 24 24">
|
||||
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/>
|
||||
</svg>
|
||||
Logs système
|
||||
</h2>
|
||||
|
||||
<div class="log-container" id="logContainer">
|
||||
<!-- Les logs seront ajoutés ici -->
|
||||
</div>
|
||||
|
||||
<button class="btn btn-secondary" onclick="clearLogs()" style="margin-top: 15px;">
|
||||
Effacer les logs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="notification" id="notification"></div>
|
||||
|
||||
<script>
|
||||
// État global de l'application
|
||||
let appState = {
|
||||
selectedWifi: null,
|
||||
hotspotEnabled: true,
|
||||
connectedDevices: [],
|
||||
wifiNetworks: [],
|
||||
uptime: 0,
|
||||
dataUsage: 0
|
||||
};
|
||||
|
||||
// Simulation de données
|
||||
const mockWifiNetworks = [
|
||||
{ ssid: "Hotel_WiFi", signal: 4, security: "WPA2", channel: 6 },
|
||||
{ ssid: "Starbucks_Free", signal: 3, security: "Open", channel: 11 },
|
||||
{ ssid: "AndroidAP", signal: 2, security: "WPA2", channel: 1 },
|
||||
{ ssid: "iPhone_Hotspot", signal: 5, security: "WPA2", channel: 6 },
|
||||
{ ssid: "Guest_Network", signal: 1, security: "WPA", channel: 11 }
|
||||
];
|
||||
|
||||
const mockDevices = [
|
||||
{ name: "iPhone 13", type: "mobile", mac: "AA:BB:CC:DD:EE:FF", ip: "192.168.1.101" },
|
||||
{ name: "MacBook Pro", type: "laptop", mac: "11:22:33:44:55:66", ip: "192.168.1.102" },
|
||||
{ name: "iPad", type: "tablet", mac: "77:88:99:AA:BB:CC", ip: "192.168.1.103" }
|
||||
];
|
||||
|
||||
// Initialisation
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initializeApp();
|
||||
startPeriodicUpdates();
|
||||
});
|
||||
|
||||
function initializeApp() {
|
||||
updateWifiList();
|
||||
updateDevicesList();
|
||||
updateStats();
|
||||
addLog("Système", "Interface web initialisée");
|
||||
}
|
||||
|
||||
function updateWifiList() {
|
||||
const wifiList = document.getElementById('wifiList');
|
||||
wifiList.innerHTML = '';
|
||||
|
||||
mockWifiNetworks.forEach((network, index) => {
|
||||
const wifiItem = document.createElement('div');
|
||||
wifiItem.className = 'wifi-item';
|
||||
wifiItem.onclick = () => selectWifi(network, wifiItem);
|
||||
|
||||
wifiItem.innerHTML = `
|
||||
<div>
|
||||
<strong>${network.ssid}</strong>
|
||||
<div style="font-size: 0.8em; color: #666;">${network.security} • Canal ${network.channel}</div>
|
||||
</div>
|
||||
<div class="wifi-signal">
|
||||
${generateSignalBars(network.signal)}
|
||||
</div>
|
||||
`;
|
||||
|
||||
wifiList.appendChild(wifiItem);
|
||||
});
|
||||
}
|
||||
|
||||
function generateSignalBars(strength) {
|
||||
const bars = [];
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const height = i * 3;
|
||||
const active = i <= strength ? 'active' : '';
|
||||
bars.push(`<div class="signal-bar ${active}" style="height: ${height}px;"></div>`);
|
||||
}
|
||||
return `<div class="signal-bars">${bars.join('')}</div>`;
|
||||
}
|
||||
|
||||
function selectWifi(network, element) {
|
||||
// Retirer la sélection précédente
|
||||
document.querySelectorAll('.wifi-item').forEach(item => {
|
||||
item.classList.remove('selected');
|
||||
});
|
||||
|
||||
// Ajouter la sélection
|
||||
element.classList.add('selected');
|
||||
appState.selectedWifi = network;
|
||||
|
||||
addLog("WiFi", `Réseau sélectionné: ${network.ssid}`);
|
||||
}
|
||||
|
||||
function updateDevicesList() {
|
||||
const devicesList = document.getElementById('devicesList');
|
||||
devicesList.innerHTML = '';
|
||||
|
||||
mockDevices.forEach(device => {
|
||||
const deviceCard = document.createElement('div');
|
||||
deviceCard.className = 'device-card';
|
||||
|
||||
const deviceIcon = getDeviceIcon(device.type);
|
||||
|
||||
deviceCard.innerHTML = `
|
||||
${deviceIcon}
|
||||
<div style="font-weight: 500;">${device.name}</div>
|
||||
<div style="font-size: 0.8em; color: #666;">${device.ip}</div>
|
||||
`;
|
||||
|
||||
devicesList.appendChild(deviceCard);
|
||||
});
|
||||
|
||||
document.getElementById('connectedDevices').textContent = mockDevices.length;
|
||||
}
|
||||
|
||||
function getDeviceIcon(type) {
|
||||
const icons = {
|
||||
mobile: '<svg class="device-icon" viewBox="0 0 24 24"><path d="M17 1H7c-1.1 0-2 .9-2 2v18c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-2-2-2zm0 18H7V5h10v14z"/></svg>',
|
||||
laptop: '<svg class="device-icon" viewBox="0 0 24 24"><path d="M20 18c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2H0v2h24v-2h-4zM4 6h16v10H4V6z"/></svg>',
|
||||
tablet: '<svg class="device-icon" viewBox="0 0 24 24"><path d="M21 4H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 14H3V6h18v12z"/></svg>'
|
||||
};
|
||||
return icons[type] || icons.mobile;
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
appState.uptime += 1;
|
||||
appState.dataUsage += Math.random() * 0.5;
|
||||
|
||||
const hours = Math.floor(appState.uptime / 3600);
|
||||
const minutes = Math.floor((appState.uptime % 3600) / 60);
|
||||
const seconds = appState.uptime % 60;
|
||||
|
||||
document.getElementById('uptime').textContent =
|
||||
`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
|
||||
document.getElementById('dataUsage').textContent = `${appState.dataUsage.toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
function addLog(source, message) {
|
||||
const logContainer = document.getElementById('logContainer');
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const logEntry = document.createElement('div');
|
||||
logEntry.className = 'log-entry';
|
||||
logEntry.innerHTML = `<span class="log-timestamp">[${timestamp}]</span> ${source}: ${message}`;
|
||||
|
||||
logContainer.appendChild(logEntry);
|
||||
logContainer.scrollTop = logContainer.scrollHeight;
|
||||
}
|
||||
|
||||
function showNotification(message, type = 'success') {
|
||||
const notification = document.getElementById('notification');
|
||||
notification.textContent = message;
|
||||
notification.className = `notification ${type}`;
|
||||
notification.classList.add('show');
|
||||
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('show');
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Fonctions d'action
|
||||
function scanWifi() {
|
||||
const scanBtn = document.getElementById('scanBtn');
|
||||
const originalText = scanBtn.textContent;
|
||||
|
||||
scanBtn.innerHTML = '<div class="loading"></div> Scan en cours...';
|
||||
|
||||
setTimeout(() => {
|
||||
updateWifiList();
|
||||
scanBtn.textContent = originalText;
|
||||
showNotification('Scan terminé - Réseaux mis à jour');
|
||||
addLog("WiFi", "Scan des réseaux terminé");
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function connectToWifi() {
|
||||
if (!appState.selectedWifi) {
|
||||
showNotification('Veuillez sélectionner un réseau WiFi', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const password = document.getElementById('wifiPassword').value;
|
||||
if (!password && appState.selectedWifi.security !== 'Open') {
|
||||
showNotification('Mot de passe requis', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const connectBtn = document.getElementById('connectBtn');
|
||||
const originalText = connectBtn.textContent;
|
||||
|
||||
connectBtn.innerHTML = '<div class="loading"></div> Connexion...';
|
||||
|
||||
setTimeout(() => {
|
||||
connectBtn.textContent = originalText;
|
||||
showNotification(`Connecté à ${appState.selectedWifi.ssid}`);
|
||||
addLog("WiFi", `Connexion établie avec ${appState.selectedWifi.ssid}`);
|
||||
|
||||
// Mettre à jour le statut
|
||||
document.getElementById('connectionStatus').innerHTML = `
|
||||
<div class="status-dot"></div>
|
||||
<span>Connecté à ${appState.selectedWifi.ssid}</span>
|
||||
`;
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function updateHotspot() {
|
||||
const name = document.getElementById('hotspotName').value;
|
||||
const password = document.getElementById('hotspotPassword').value;
|
||||
const channel = document.getElementById('hotspotChannel').value;
|
||||
|
||||
if (!name || !password) {
|
||||
showNotification('Nom et mot de passe requis', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
showNotification('Configuration du hotspot mise à jour');
|
||||
addLog("Hotspot", `Configuration mise à jour: ${name} (Canal ${channel})`);
|
||||
}
|
||||
|
||||
function toggleHotspot() {
|
||||
appState.hotspotEnabled = !appState.hotspotEnabled;
|
||||
const btn = document.getElementById('hotspotBtn');
|
||||
|
||||
if (appState.hotspotEnabled) {
|
||||
btn.textContent = 'Arrêter le hotspot';
|
||||
showNotification('Hotspot activé');
|
||||
addLog("Hotspot", "Hotspot activé");
|
||||
} else {
|
||||
btn.textContent = 'Démarrer le hotspot';
|
||||
showNotification('Hotspot désactivé');
|
||||
addLog("Hotspot", "Hotspot désactivé");
|
||||
}
|
||||
}
|
||||
|
||||
function clearLogs() {
|
||||
document.getElementById('logContainer').innerHTML = '';
|
||||
addLog("Système", "Logs effacés");
|
||||
}
|
||||
|
||||
// Mises à jour périodiques
|
||||
function startPeriodicUpdates() {
|
||||
setInterval(updateStats, 1000);
|
||||
setInterval(() => {
|
||||
// Simulation de nouveaux logs
|
||||
if (Math.random() > 0.95) {
|
||||
const events = [
|
||||
"Nouveau client connecté",
|
||||
"Paquet routé vers l'extérieur",
|
||||
"Vérification de la connexion",
|
||||
"Mise à jour des tables de routage"
|
||||
];
|
||||
const randomEvent = events[Math.floor(Math.random() * events.length)];
|
||||
addLog("Système", randomEvent);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue