repeater/main.go

826 lines
20 KiB
Go

package main
import (
"bufio"
"embed"
"encoding/json"
"fmt"
"io/fs"
"log"
"net/http"
"os"
"os/exec"
"regexp"
"sort"
"strings"
"sync"
"time"
"github.com/godbus/dbus/v5"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
)
//go:embed all:static
var _assets embed.FS
// 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
sub, err := fs.Sub(_assets, "static")
if err != nil {
log.Fatal("Unable to cd to static/ directory:", err)
}
Assets := http.FS(sub)
r.PathPrefix("/").Handler(http.FileServer(Assets))
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{"Type": dbus.MakeVariant("active")})
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
bssePaths, err := wifiInterface.GetProperty(WPA_INTERFACE_IFACE + ".BSSs")
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.Value().([]dbus.ObjectPath) {
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 interfacePath dbus.ObjectPath
err := wpaSupplicant.Call(WPA_SUPPLICANT_IFACE+".GetInterface", 0, WLAN_INTERFACE).Store(&interfacePath)
if err != nil {
return "", fmt.Errorf("erreur lors de la récupération des interfaces: %v", err)
}
return interfacePath, nil
}
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()
}
}