Migrate to a better architectured project
This commit is contained in:
parent
cc5ed5f23e
commit
b1b9eaa028
21 changed files with 2712 additions and 1540 deletions
132
internal/api/handlers/handlers.go
Normal file
132
internal/api/handlers/handlers.go
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/nemunaire/repeater/internal/device"
|
||||
"github.com/nemunaire/repeater/internal/hotspot"
|
||||
"github.com/nemunaire/repeater/internal/logging"
|
||||
"github.com/nemunaire/repeater/internal/models"
|
||||
"github.com/nemunaire/repeater/internal/wifi"
|
||||
)
|
||||
|
||||
// ScanWiFi handles WiFi network scanning
|
||||
func ScanWiFi(c *gin.Context) {
|
||||
networks, err := wifi.ScanNetworks()
|
||||
if err != nil {
|
||||
logging.AddLog("WiFi", "Erreur lors du scan: "+err.Error())
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur lors du scan WiFi"})
|
||||
return
|
||||
}
|
||||
|
||||
logging.AddLog("WiFi", "Scan terminé - "+string(rune(len(networks)))+" réseaux trouvés")
|
||||
c.JSON(http.StatusOK, networks)
|
||||
}
|
||||
|
||||
// ConnectWiFi handles WiFi connection requests
|
||||
func ConnectWiFi(c *gin.Context) {
|
||||
var req models.WiFiConnectRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
|
||||
return
|
||||
}
|
||||
|
||||
logging.AddLog("WiFi", "Tentative de connexion à "+req.SSID)
|
||||
|
||||
err := wifi.Connect(req.SSID, req.Password)
|
||||
if err != nil {
|
||||
logging.AddLog("WiFi", "Échec de connexion: "+err.Error())
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Échec de connexion: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
logging.AddLog("WiFi", "Connexion réussie à "+req.SSID)
|
||||
c.JSON(http.StatusOK, gin.H{"status": "success"})
|
||||
}
|
||||
|
||||
// DisconnectWiFi handles WiFi disconnection
|
||||
func DisconnectWiFi(c *gin.Context) {
|
||||
logging.AddLog("WiFi", "Tentative de déconnexion")
|
||||
|
||||
err := wifi.Disconnect()
|
||||
if err != nil {
|
||||
logging.AddLog("WiFi", "Erreur de déconnexion: "+err.Error())
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur de déconnexion: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
logging.AddLog("WiFi", "Déconnexion réussie")
|
||||
c.JSON(http.StatusOK, gin.H{"status": "success"})
|
||||
}
|
||||
|
||||
// ConfigureHotspot handles hotspot configuration
|
||||
func ConfigureHotspot(c *gin.Context) {
|
||||
var config models.HotspotConfig
|
||||
if err := c.ShouldBindJSON(&config); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Données invalides"})
|
||||
return
|
||||
}
|
||||
|
||||
err := hotspot.Configure(config)
|
||||
if err != nil {
|
||||
logging.AddLog("Hotspot", "Erreur de configuration: "+err.Error())
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur de configuration: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
logging.AddLog("Hotspot", "Configuration mise à jour: "+config.SSID)
|
||||
c.JSON(http.StatusOK, gin.H{"status": "success"})
|
||||
}
|
||||
|
||||
// ToggleHotspot handles hotspot enable/disable
|
||||
func ToggleHotspot(c *gin.Context, status *models.SystemStatus) {
|
||||
status.HotspotEnabled = !status.HotspotEnabled
|
||||
enabled := status.HotspotEnabled
|
||||
|
||||
var err error
|
||||
if enabled {
|
||||
err = hotspot.Start()
|
||||
logging.AddLog("Hotspot", "Hotspot activé")
|
||||
} else {
|
||||
err = hotspot.Stop()
|
||||
logging.AddLog("Hotspot", "Hotspot désactivé")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logging.AddLog("Hotspot", "Erreur: "+err.Error())
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"enabled": enabled})
|
||||
}
|
||||
|
||||
// GetDevices returns connected devices
|
||||
func GetDevices(c *gin.Context) {
|
||||
devices, err := device.GetConnectedDevices()
|
||||
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"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, devices)
|
||||
}
|
||||
|
||||
// GetStatus returns system status
|
||||
func GetStatus(c *gin.Context, status *models.SystemStatus) {
|
||||
c.JSON(http.StatusOK, status)
|
||||
}
|
||||
|
||||
// GetLogs returns system logs
|
||||
func GetLogs(c *gin.Context) {
|
||||
logs := logging.GetLogs()
|
||||
c.JSON(http.StatusOK, logs)
|
||||
}
|
||||
|
||||
// ClearLogs clears system logs
|
||||
func ClearLogs(c *gin.Context) {
|
||||
logging.ClearLogs()
|
||||
c.JSON(http.StatusOK, gin.H{"status": "success"})
|
||||
}
|
||||
38
internal/api/handlers/websocket.go
Normal file
38
internal/api/handlers/websocket.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/nemunaire/repeater/internal/logging"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
// WebSocketLogs handles WebSocket connections for real-time logs
|
||||
func WebSocketLogs(c *gin.Context) {
|
||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
log.Printf("Erreur WebSocket: %v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Register client
|
||||
logging.RegisterWebSocketClient(conn)
|
||||
defer logging.UnregisterWebSocketClient(conn)
|
||||
|
||||
// Keep connection alive
|
||||
for {
|
||||
_, _, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
64
internal/api/router.go
Normal file
64
internal/api/router.go
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/nemunaire/repeater/internal/api/handlers"
|
||||
"github.com/nemunaire/repeater/internal/models"
|
||||
)
|
||||
|
||||
// SetupRouter creates and configures the Gin router
|
||||
func SetupRouter(status *models.SystemStatus, assets embed.FS) *gin.Engine {
|
||||
// Set Gin to release mode (can be overridden with GIN_MODE env var)
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
r := gin.Default()
|
||||
|
||||
// API routes
|
||||
api := r.Group("/api")
|
||||
{
|
||||
// WiFi endpoints
|
||||
wifi := api.Group("/wifi")
|
||||
{
|
||||
wifi.GET("/scan", handlers.ScanWiFi)
|
||||
wifi.POST("/connect", handlers.ConnectWiFi)
|
||||
wifi.POST("/disconnect", handlers.DisconnectWiFi)
|
||||
}
|
||||
|
||||
// Hotspot endpoints
|
||||
hotspot := api.Group("/hotspot")
|
||||
{
|
||||
hotspot.POST("/config", handlers.ConfigureHotspot)
|
||||
hotspot.POST("/toggle", func(c *gin.Context) {
|
||||
handlers.ToggleHotspot(c, status)
|
||||
})
|
||||
}
|
||||
|
||||
// Device endpoints
|
||||
api.GET("/devices", handlers.GetDevices)
|
||||
|
||||
// Status endpoint
|
||||
api.GET("/status", func(c *gin.Context) {
|
||||
handlers.GetStatus(c, status)
|
||||
})
|
||||
|
||||
// Log endpoints
|
||||
api.GET("/logs", handlers.GetLogs)
|
||||
api.DELETE("/logs", handlers.ClearLogs)
|
||||
}
|
||||
|
||||
// WebSocket endpoint
|
||||
r.GET("/ws/logs", handlers.WebSocketLogs)
|
||||
|
||||
// Serve static files
|
||||
sub, err := fs.Sub(assets, "static")
|
||||
if err != nil {
|
||||
panic("Unable to access static directory: " + err.Error())
|
||||
}
|
||||
r.NoRoute(gin.WrapH(http.FileServer(http.FS(sub))))
|
||||
|
||||
return r
|
||||
}
|
||||
102
internal/app/app.go
Normal file
102
internal/app/app.go
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/nemunaire/repeater/internal/api"
|
||||
"github.com/nemunaire/repeater/internal/device"
|
||||
"github.com/nemunaire/repeater/internal/logging"
|
||||
"github.com/nemunaire/repeater/internal/models"
|
||||
"github.com/nemunaire/repeater/internal/wifi"
|
||||
)
|
||||
|
||||
// App represents the application
|
||||
type App struct {
|
||||
Status models.SystemStatus
|
||||
StatusMutex sync.RWMutex
|
||||
StartTime time.Time
|
||||
Assets embed.FS
|
||||
}
|
||||
|
||||
// New creates a new application instance
|
||||
func New(assets embed.FS) *App {
|
||||
return &App{
|
||||
Status: models.SystemStatus{
|
||||
Connected: false,
|
||||
ConnectedSSID: "",
|
||||
HotspotEnabled: true,
|
||||
ConnectedCount: 0,
|
||||
DataUsage: 0.0,
|
||||
Uptime: 0,
|
||||
},
|
||||
StartTime: time.Now(),
|
||||
Assets: assets,
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize initializes the application
|
||||
func (a *App) Initialize() error {
|
||||
// Initialize WiFi D-Bus connection
|
||||
if err := wifi.Initialize(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start periodic tasks
|
||||
go a.periodicStatusUpdate()
|
||||
go a.periodicDeviceUpdate()
|
||||
|
||||
logging.AddLog("Système", "Application initialisée")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run starts the HTTP server
|
||||
func (a *App) Run(addr string) error {
|
||||
router := api.SetupRouter(&a.Status, a.Assets)
|
||||
|
||||
logging.AddLog("Système", "Serveur API démarré sur "+addr)
|
||||
return router.Run(addr)
|
||||
}
|
||||
|
||||
// Shutdown gracefully shuts down the application
|
||||
func (a *App) Shutdown() {
|
||||
wifi.Close()
|
||||
logging.AddLog("Système", "Application arrêtée")
|
||||
}
|
||||
|
||||
// periodicStatusUpdate updates WiFi connection status periodically
|
||||
func (a *App) periodicStatusUpdate() {
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
a.StatusMutex.Lock()
|
||||
a.Status.Connected = wifi.IsConnected()
|
||||
if !a.Status.Connected {
|
||||
a.Status.ConnectedSSID = ""
|
||||
}
|
||||
a.Status.Uptime = int64(time.Since(a.StartTime).Seconds())
|
||||
a.StatusMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// periodicDeviceUpdate updates connected devices list periodically
|
||||
func (a *App) periodicDeviceUpdate() {
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
devices, err := device.GetConnectedDevices()
|
||||
if err != nil {
|
||||
log.Printf("Error getting connected devices: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
a.StatusMutex.Lock()
|
||||
a.Status.ConnectedDevices = devices
|
||||
a.Status.ConnectedCount = len(devices)
|
||||
a.StatusMutex.Unlock()
|
||||
}
|
||||
}
|
||||
132
internal/device/device.go
Normal file
132
internal/device/device.go
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
package device
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/nemunaire/repeater/internal/models"
|
||||
)
|
||||
|
||||
// GetConnectedDevices returns a list of connected devices
|
||||
func GetConnectedDevices() ([]models.ConnectedDevice, error) {
|
||||
var devices []models.ConnectedDevice
|
||||
|
||||
// Read DHCP leases
|
||||
leases, err := parseDHCPLeases()
|
||||
if err != nil {
|
||||
return devices, err
|
||||
}
|
||||
|
||||
// Get ARP information
|
||||
arpInfo, err := getARPInfo()
|
||||
if err != nil {
|
||||
return devices, err
|
||||
}
|
||||
|
||||
for _, lease := range leases {
|
||||
device := models.ConnectedDevice{
|
||||
Name: lease.Hostname,
|
||||
MAC: lease.MAC,
|
||||
IP: lease.IP,
|
||||
Type: guessDeviceType(lease.Hostname, lease.MAC),
|
||||
}
|
||||
|
||||
// Check if the device is still connected via ARP
|
||||
if _, exists := arpInfo[lease.IP]; exists {
|
||||
devices = append(devices, device)
|
||||
}
|
||||
}
|
||||
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
// parseDHCPLeases reads and parses DHCP lease file
|
||||
func parseDHCPLeases() ([]models.DHCPLease, error) {
|
||||
var leases []models.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 models.DHCPLease
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
if strings.HasPrefix(line, "lease ") {
|
||||
ip := strings.Fields(line)[1]
|
||||
currentLease = models.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 = models.DHCPLease{}
|
||||
}
|
||||
}
|
||||
|
||||
return leases, nil
|
||||
}
|
||||
|
||||
// getARPInfo retrieves ARP table information
|
||||
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
|
||||
}
|
||||
|
||||
// guessDeviceType attempts to guess device type from hostname and MAC address
|
||||
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"
|
||||
}
|
||||
|
||||
// Guess by MAC prefix (OUI)
|
||||
if len(mac) >= 8 {
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
return "unknown"
|
||||
}
|
||||
47
internal/hotspot/hotspot.go
Normal file
47
internal/hotspot/hotspot.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
package hotspot
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/nemunaire/repeater/internal/models"
|
||||
)
|
||||
|
||||
const (
|
||||
AP_INTERFACE = "wlan1"
|
||||
HOSTAPD_CONF = "/etc/hostapd/hostapd.conf"
|
||||
)
|
||||
|
||||
// Configure updates the hotspot configuration
|
||||
func Configure(config models.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)
|
||||
}
|
||||
|
||||
// Start starts the hotspot
|
||||
func Start() error {
|
||||
cmd := exec.Command("systemctl", "start", "hostapd")
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// Stop stops the hotspot
|
||||
func Stop() error {
|
||||
cmd := exec.Command("systemctl", "stop", "hostapd")
|
||||
return cmd.Run()
|
||||
}
|
||||
94
internal/logging/logging.go
Normal file
94
internal/logging/logging.go
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
package logging
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/nemunaire/repeater/internal/models"
|
||||
)
|
||||
|
||||
var (
|
||||
logEntries []models.LogEntry
|
||||
logMutex sync.RWMutex
|
||||
websocketClients = make(map[*websocket.Conn]bool)
|
||||
clientsMutex sync.RWMutex
|
||||
)
|
||||
|
||||
// AddLog adds a new log entry
|
||||
func AddLog(source, message string) {
|
||||
logMutex.Lock()
|
||||
entry := models.LogEntry{
|
||||
Timestamp: time.Now(),
|
||||
Source: source,
|
||||
Message: message,
|
||||
}
|
||||
logEntries = append(logEntries, entry)
|
||||
|
||||
// Keep only the last 100 logs
|
||||
if len(logEntries) > 100 {
|
||||
logEntries = logEntries[len(logEntries)-100:]
|
||||
}
|
||||
logMutex.Unlock()
|
||||
|
||||
// Broadcast to WebSocket clients
|
||||
broadcastToWebSockets(entry)
|
||||
|
||||
// Log to console
|
||||
log.Printf("[%s] %s", source, message)
|
||||
}
|
||||
|
||||
// GetLogs returns all log entries
|
||||
func GetLogs() []models.LogEntry {
|
||||
logMutex.RLock()
|
||||
defer logMutex.RUnlock()
|
||||
|
||||
logs := make([]models.LogEntry, len(logEntries))
|
||||
copy(logs, logEntries)
|
||||
return logs
|
||||
}
|
||||
|
||||
// ClearLogs clears all log entries
|
||||
func ClearLogs() {
|
||||
logMutex.Lock()
|
||||
logEntries = []models.LogEntry{}
|
||||
logMutex.Unlock()
|
||||
|
||||
AddLog("Système", "Logs effacés")
|
||||
}
|
||||
|
||||
// RegisterWebSocketClient registers a new WebSocket client
|
||||
func RegisterWebSocketClient(conn *websocket.Conn) {
|
||||
clientsMutex.Lock()
|
||||
websocketClients[conn] = true
|
||||
clientsMutex.Unlock()
|
||||
|
||||
// Send existing logs to the new client
|
||||
logMutex.RLock()
|
||||
for _, entry := range logEntries {
|
||||
conn.WriteJSON(entry)
|
||||
}
|
||||
logMutex.RUnlock()
|
||||
}
|
||||
|
||||
// UnregisterWebSocketClient removes a WebSocket client
|
||||
func UnregisterWebSocketClient(conn *websocket.Conn) {
|
||||
clientsMutex.Lock()
|
||||
delete(websocketClients, conn)
|
||||
clientsMutex.Unlock()
|
||||
}
|
||||
|
||||
// broadcastToWebSockets sends a log entry to all connected WebSocket clients
|
||||
func broadcastToWebSockets(entry models.LogEntry) {
|
||||
clientsMutex.RLock()
|
||||
defer clientsMutex.RUnlock()
|
||||
|
||||
for client := range websocketClients {
|
||||
err := client.WriteJSON(entry)
|
||||
if err != nil {
|
||||
client.Close()
|
||||
delete(websocketClients, client)
|
||||
}
|
||||
}
|
||||
}
|
||||
58
internal/models/models.go
Normal file
58
internal/models/models.go
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// WiFiNetwork represents a discovered WiFi network
|
||||
type WiFiNetwork struct {
|
||||
SSID string `json:"ssid"`
|
||||
Signal int `json:"signal"`
|
||||
Security string `json:"security"`
|
||||
Channel int `json:"channel"`
|
||||
BSSID string `json:"bssid"`
|
||||
}
|
||||
|
||||
// ConnectedDevice represents a device connected to the hotspot
|
||||
type ConnectedDevice struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
MAC string `json:"mac"`
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
// HotspotConfig represents hotspot configuration
|
||||
type HotspotConfig struct {
|
||||
SSID string `json:"ssid"`
|
||||
Password string `json:"password"`
|
||||
Channel int `json:"channel"`
|
||||
}
|
||||
|
||||
// SystemStatus represents overall system status
|
||||
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"`
|
||||
}
|
||||
|
||||
// WiFiConnectRequest represents a request to connect to WiFi
|
||||
type WiFiConnectRequest struct {
|
||||
SSID string `json:"ssid" binding:"required"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// LogEntry represents a system log entry
|
||||
type LogEntry struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Source string `json:"source"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// DHCPLease represents a DHCP lease entry
|
||||
type DHCPLease struct {
|
||||
IP string
|
||||
MAC string
|
||||
Hostname string
|
||||
}
|
||||
273
internal/wifi/wifi.go
Normal file
273
internal/wifi/wifi.go
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
package wifi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"github.com/nemunaire/repeater/internal/models"
|
||||
)
|
||||
|
||||
const (
|
||||
WLAN_INTERFACE = "wlan0"
|
||||
WPA_CONF = "/etc/wpa_supplicant/wpa_supplicant.conf"
|
||||
|
||||
// D-Bus constants for wpa_supplicant
|
||||
WPA_SUPPLICANT_SERVICE = "fi.w1.wpa_supplicant1"
|
||||
WPA_SUPPLICANT_PATH = "/fi/w1/wpa_supplicant1"
|
||||
WPA_SUPPLICANT_IFACE = "fi.w1.wpa_supplicant1"
|
||||
WPA_INTERFACE_IFACE = "fi.w1.wpa_supplicant1.Interface"
|
||||
WPA_BSS_IFACE = "fi.w1.wpa_supplicant1.BSS"
|
||||
WPA_NETWORK_IFACE = "fi.w1.wpa_supplicant1.Network"
|
||||
)
|
||||
|
||||
var (
|
||||
dbusConn *dbus.Conn
|
||||
wpaSupplicant dbus.BusObject
|
||||
)
|
||||
|
||||
// Initialize initializes the WiFi service with D-Bus connection
|
||||
func Initialize() error {
|
||||
var err error
|
||||
dbusConn, err = dbus.SystemBus()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to D-Bus: %v", err)
|
||||
}
|
||||
|
||||
wpaSupplicant = dbusConn.Object(WPA_SUPPLICANT_SERVICE, dbus.ObjectPath(WPA_SUPPLICANT_PATH))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the D-Bus connection
|
||||
func Close() {
|
||||
if dbusConn != nil {
|
||||
dbusConn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// ScanNetworks scans for available WiFi networks
|
||||
func ScanNetworks() ([]models.WiFiNetwork, error) {
|
||||
interfacePath, err := getWiFiInterfacePath()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("impossible d'obtenir l'interface WiFi: %v", err)
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
// Retrieve BSS list
|
||||
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 []models.WiFiNetwork
|
||||
seenSSIDs := make(map[string]bool)
|
||||
|
||||
for _, bssPath := range bssePaths.Value().([]dbus.ObjectPath) {
|
||||
bss := dbusConn.Object(WPA_SUPPLICANT_SERVICE, bssPath)
|
||||
|
||||
// Get BSS properties
|
||||
var props map[string]dbus.Variant
|
||||
err = bss.Call("org.freedesktop.DBus.Properties.GetAll", 0, WPA_BSS_IFACE).Store(&props)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
network := models.WiFiNetwork{}
|
||||
|
||||
// Extract SSID
|
||||
if ssidBytes, ok := props["SSID"].Value().([]byte); ok {
|
||||
network.SSID = string(ssidBytes)
|
||||
}
|
||||
|
||||
// Skip duplicates and empty SSIDs
|
||||
if network.SSID == "" || seenSSIDs[network.SSID] {
|
||||
continue
|
||||
}
|
||||
seenSSIDs[network.SSID] = true
|
||||
|
||||
// Extract BSSID
|
||||
if bssidBytes, ok := props["BSSID"].Value().([]byte); ok {
|
||||
network.BSSID = fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x",
|
||||
bssidBytes[0], bssidBytes[1], bssidBytes[2], bssidBytes[3], bssidBytes[4], bssidBytes[5])
|
||||
}
|
||||
|
||||
// Extract signal strength
|
||||
if signal, ok := props["Signal"].Value().(int16); ok {
|
||||
network.Signal = signalToStrength(int(signal))
|
||||
}
|
||||
|
||||
// Extract frequency and calculate channel
|
||||
if frequency, ok := props["Frequency"].Value().(uint16); ok {
|
||||
network.Channel = frequencyToChannel(int(frequency))
|
||||
}
|
||||
|
||||
// Determine security
|
||||
if privacyVal, ok := props["Privacy"].Value().(bool); ok && privacyVal {
|
||||
if wpaProps, ok := props["WPA"].Value().(map[string]dbus.Variant); ok && len(wpaProps) > 0 {
|
||||
network.Security = "WPA"
|
||||
} else if rsnProps, ok := props["RSN"].Value().(map[string]dbus.Variant); ok && len(rsnProps) > 0 {
|
||||
network.Security = "WPA2"
|
||||
} else {
|
||||
network.Security = "WEP"
|
||||
}
|
||||
} else {
|
||||
network.Security = "Open"
|
||||
}
|
||||
|
||||
networks = append(networks, network)
|
||||
}
|
||||
|
||||
// Sort by signal strength
|
||||
sort.Slice(networks, func(i, j int) bool {
|
||||
return networks[i].Signal > networks[j].Signal
|
||||
})
|
||||
|
||||
return networks, nil
|
||||
}
|
||||
|
||||
// Connect connects to a WiFi network using D-Bus
|
||||
func Connect(ssid, password string) error {
|
||||
interfacePath, err := getWiFiInterfacePath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("impossible d'obtenir l'interface WiFi: %v", err)
|
||||
}
|
||||
|
||||
wifiInterface := dbusConn.Object(WPA_SUPPLICANT_SERVICE, interfacePath)
|
||||
|
||||
// Create a new network
|
||||
networkConfig := map[string]dbus.Variant{
|
||||
"ssid": dbus.MakeVariant(ssid),
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Select the network
|
||||
err = wifiInterface.Call(WPA_INTERFACE_IFACE+".SelectNetwork", 0, networkPath).Err
|
||||
if err != nil {
|
||||
return fmt.Errorf("erreur lors de la sélection du réseau: %v", err)
|
||||
}
|
||||
|
||||
// Wait for connection
|
||||
for i := 0; i < 20; i++ {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
if IsConnected() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("timeout lors de la connexion")
|
||||
}
|
||||
|
||||
// Disconnect disconnects from the current WiFi network
|
||||
func Disconnect() error {
|
||||
interfacePath, err := getWiFiInterfacePath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("impossible d'obtenir l'interface WiFi: %v", err)
|
||||
}
|
||||
|
||||
wifiInterface := dbusConn.Object(WPA_SUPPLICANT_SERVICE, interfacePath)
|
||||
|
||||
// Disconnect
|
||||
err = wifiInterface.Call(WPA_INTERFACE_IFACE+".Disconnect", 0).Err
|
||||
if err != nil {
|
||||
return fmt.Errorf("erreur lors de la déconnexion: %v", err)
|
||||
}
|
||||
|
||||
// Remove all networks
|
||||
var networks []dbus.ObjectPath
|
||||
err = wifiInterface.Call(WPA_INTERFACE_IFACE+".Get", 0, WPA_INTERFACE_IFACE, "Networks").Store(&networks)
|
||||
if err == nil {
|
||||
for _, networkPath := range networks {
|
||||
wifiInterface.Call(WPA_INTERFACE_IFACE+".RemoveNetwork", 0, networkPath)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsConnected checks if WiFi is connected using D-Bus
|
||||
func IsConnected() 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"
|
||||
}
|
||||
|
||||
// IsConnectedLegacy checks if WiFi is connected using iwconfig (fallback)
|
||||
func IsConnectedLegacy() bool {
|
||||
cmd := exec.Command("iwconfig", WLAN_INTERFACE)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return strings.Contains(string(output), "Access Point:")
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("erreur lors de la récupération des interfaces: %v", err)
|
||||
}
|
||||
|
||||
return interfacePath, nil
|
||||
}
|
||||
|
||||
// frequencyToChannel converts WiFi frequency to channel number
|
||||
func frequencyToChannel(frequency int) int {
|
||||
if frequency >= 2412 && frequency <= 2484 {
|
||||
if frequency == 2484 {
|
||||
return 14
|
||||
}
|
||||
return (frequency-2412)/5 + 1
|
||||
} else if frequency >= 5170 && frequency <= 5825 {
|
||||
return (frequency - 5000) / 5
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// signalToStrength converts signal level (dBm) to strength (1-5)
|
||||
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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue