diff --git a/.gitignore b/.gitignore index ea33d90..e147207 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1 @@ -internal/api/routes.gen.go -internal/api/types.gen.go -/repeater \ No newline at end of file +repeater \ No newline at end of file diff --git a/Makefile b/Makefile deleted file mode 100644 index 2d57547..0000000 --- a/Makefile +++ /dev/null @@ -1,32 +0,0 @@ -.PHONY: build run clean install test - -BINARY_NAME=repeater -CMD_PATH=./cmd/repeater -BUILD_DIR=. - -build: - go build -v -o $(BUILD_DIR)/$(BINARY_NAME) $(CMD_PATH) - -run: build - sudo ./$(BINARY_NAME) - -clean: - go clean - rm -f $(BUILD_DIR)/$(BINARY_NAME) - -install: build - sudo install -m 755 $(BUILD_DIR)/$(BINARY_NAME) /usr/local/bin/ - -test: - go test -v ./... - -tidy: - go mod tidy - -fmt: - go fmt ./... - -vet: - go vet ./... - -all: fmt vet build diff --git a/README.md b/README.md deleted file mode 100644 index 54691f9..0000000 --- a/README.md +++ /dev/null @@ -1,102 +0,0 @@ -# Travel Router Control - -A Go application for controlling a mini travel router with dual WiFi interfaces and Ethernet connectivity. The router can operate as a WiFi repeater, connecting to upstream networks while providing a hotspot for client devices. - -## Features - -- WiFi network scanning and connection management -- Hotspot (access point) configuration and control -- Connected device monitoring -- Real-time system logs via WebSocket -- RESTful API following OpenAPI 3.0 specification -- Web interface for easy management - -## Architecture - -The application follows a clean architecture pattern: - -``` -. -├── cmd/ -│ └── repeater/ # Application entry point -│ ├── main.go -│ └── static/ # Embedded web assets -├── internal/ -│ ├── api/ # HTTP API layer -│ │ ├── router.go # Gin router setup -│ │ └── handlers/ # HTTP handlers -│ ├── app/ # Application logic & lifecycle -│ ├── device/ # Device management -│ ├── hotspot/ # Hotspot control -│ ├── logging/ # Logging system -│ ├── models/ # Data structures -│ └── wifi/ # WiFi operations (wpa_supplicant via D-Bus) -├── openapi.yaml # API specification -└── go.mod -``` - -## Building - -```bash -go build -o repeater ./cmd/repeater -``` - -## Running - -```bash -sudo ./repeater -``` - -The application requires root privileges to: -- Access D-Bus system bus for wpa_supplicant -- Control systemd services (hostapd) -- Read DHCP leases and ARP tables - -The server will start on port 8080. - -## API Endpoints - -### WiFi Operations -- `GET /api/wifi/scan` - Scan for available networks -- `POST /api/wifi/connect` - Connect to a network -- `POST /api/wifi/disconnect` - Disconnect from current network - -### Hotspot Operations -- `POST /api/hotspot/config` - Configure hotspot settings -- `POST /api/hotspot/toggle` - Enable/disable hotspot - -### Device Management -- `GET /api/devices` - Get connected devices - -### System -- `GET /api/status` - Get system status -- `GET /api/logs` - Get system logs -- `DELETE /api/logs` - Clear logs - -### WebSocket -- `GET /ws/logs` - Real-time log streaming - -See `openapi.yaml` for complete API documentation. - -## Configuration - -The application uses the following system resources: - -- **WiFi Interface**: `wlan0` (for upstream connection) -- **AP Interface**: `wlan1` (for hotspot) -- **Hostapd Config**: `/etc/hostapd/hostapd.conf` -- **WPA Supplicant Config**: `/etc/wpa_supplicant/wpa_supplicant.conf` - -These can be modified in the respective package constants. - -## Dependencies - -- **Gin**: HTTP web framework -- **godbus**: D-Bus client for wpa_supplicant control -- **gorilla/websocket**: WebSocket support -- **wpa_supplicant**: WiFi connection management -- **hostapd**: Hotspot functionality - -## License - -MIT diff --git a/cmd/repeater/main.go b/cmd/repeater/main.go deleted file mode 100644 index 3fd1e66..0000000 --- a/cmd/repeater/main.go +++ /dev/null @@ -1,47 +0,0 @@ -package main - -import ( - "embed" - "log" - "os" - "os/signal" - "syscall" - - "github.com/nemunaire/repeater/internal/app" - "github.com/nemunaire/repeater/internal/config" -) - -//go:embed all:static -var assets embed.FS - -func main() { - // Load and parse options - cfg, err := config.ConsolidateConfig() - if err != nil { - log.Fatal(err) - } - - // Create application instance - application := app.New(assets) - - // Initialize the application - if err := application.Initialize(cfg); err != nil { - log.Fatalf("Failed to initialize application: %v", err) - } - defer application.Shutdown() - - // Handle graceful shutdown - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - go func() { - <-sigChan - log.Println("Shutting down gracefully...") - application.Shutdown() - os.Exit(0) - }() - - // Start the server - if err := application.Run(cfg.Bind); err != nil { - log.Fatalf("Failed to start server: %v", err) - } -} diff --git a/cmd/repeater/static/app.js b/cmd/repeater/static/app.js deleted file mode 100644 index a7c367d..0000000 --- a/cmd/repeater/static/app.js +++ /dev/null @@ -1,791 +0,0 @@ -// Application state -const appState = { - selectedWifi: null, - hotspotEnabled: true, - autoScrollLogs: true, - ws: null, - wifiWs: null, - reconnectAttempts: 0, - wifiReconnectAttempts: 0, - maxReconnectAttempts: 5, - connectedSSID: null, - connectionState: 'disconnected', - networks: [], - uptime: 0, - uptimeInterval: null -}; - -// Initialize the application -document.addEventListener('DOMContentLoaded', function() { - initializeApp(); -}); - -async function initializeApp() { - console.log('Initializing Travel Router Control Panel...'); - - // Load initial data - await Promise.all([ - loadStatus(), - scanWifi(), - loadDevices(), - loadLogs() - ]); - - // Set up WebSockets for real-time updates - connectWebSocket(); - connectWifiWebSocket(); - - // Start periodic updates - startPeriodicUpdates(); - - // Start uptime counter - startUptimeCounter(); -} - -// ===== API Functions ===== - -async function loadStatus() { - try { - const response = await fetch('/api/status'); - const status = await response.json(); - - updateStatusDisplay(status); - return status; - } catch (error) { - console.error('Error loading status:', error); - showToast('error', 'Erreur', 'Impossible de charger le statut'); - } -} - -async function scanWifi() { - const wifiList = document.getElementById('wifiList'); - const scanBtn = document.querySelector('[onclick="scanWifi()"]'); - - if (scanBtn) { - scanBtn.disabled = true; - } - - wifiList.innerHTML = '
Recherche des réseaux disponibles...
'; - - try { - const response = await fetch('/api/wifi/scan'); - const networks = await response.json(); - - appState.networks = networks; - displayWifiNetworks(networks); - showToast('success', 'Scan WiFi', `${networks.length} réseau(x) trouvé(s)`); - } catch (error) { - console.error('Error scanning WiFi:', error); - - // Fallback to cached networks - try { - const fallbackResponse = await fetch('/api/wifi/networks'); - const cachedNetworks = await fallbackResponse.json(); - - appState.networks = cachedNetworks; - displayWifiNetworks(cachedNetworks); - showToast('warning', 'Scan échoué', `Affichage des réseaux en cache (${cachedNetworks.length})`); - } catch (fallbackError) { - console.error('Error loading cached networks:', fallbackError); - wifiList.innerHTML = '
Erreur lors du scan
'; - showToast('error', 'Erreur', 'Échec du scan WiFi'); - } - } finally { - if (scanBtn) { - scanBtn.disabled = false; - } - } -} - -async function connectToWifi() { - if (!appState.selectedWifi) { - showToast('warning', 'Attention', 'Veuillez sélectionner un réseau WiFi'); - return; - } - - const password = document.getElementById('wifiPassword').value; - - // Password requirement disabled - // if (appState.selectedWifi.security !== 'Open' && !password) { - // showToast('warning', 'Attention', 'Mot de passe requis pour ce réseau'); - // return; - // } - - showLoading(true); - - try { - const response = await fetch('/api/wifi/connect', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - ssid: appState.selectedWifi.ssid, - password: password - }) - }); - - const result = await response.json(); - - if (response.ok) { - showToast('success', 'Connecté', `Connexion établie avec ${appState.selectedWifi.ssid}`); - document.getElementById('wifiPassword').value = ''; - await loadStatus(); - } else { - throw new Error(result.error || 'Connection failed'); - } - } catch (error) { - console.error('Error connecting to WiFi:', error); - showToast('error', 'Erreur', 'Échec de la connexion: ' + error.message); - } finally { - showLoading(false); - } -} - -async function disconnectWifi() { - showLoading(true); - - try { - const response = await fetch('/api/wifi/disconnect', { - method: 'POST' - }); - - if (response.ok) { - showToast('success', 'Déconnecté', 'Déconnexion WiFi réussie'); - await loadStatus(); - // Force refresh WiFi list to remove green highlighting - if (appState.networks.length > 0) { - displayWifiNetworks(appState.networks); - } - } else { - throw new Error('Disconnection failed'); - } - } catch (error) { - console.error('Error disconnecting WiFi:', error); - showToast('error', 'Erreur', 'Échec de la déconnexion'); - } finally { - showLoading(false); - } -} - -async function toggleHotspot() { - const toggle = document.getElementById('hotspotToggle'); - const enabled = toggle.checked; - - showLoading(true); - - try { - const response = await fetch('/api/hotspot/toggle', { - method: 'POST' - }); - - const result = await response.json(); - - if (response.ok) { - appState.hotspotEnabled = result.enabled; - toggle.checked = result.enabled; - showToast('success', 'Hotspot', result.enabled ? 'Hotspot activé' : 'Hotspot désactivé'); - await loadStatus(); - } else { - toggle.checked = !enabled; - throw new Error('Toggle failed'); - } - } catch (error) { - console.error('Error toggling hotspot:', error); - toggle.checked = !enabled; - showToast('error', 'Erreur', 'Échec du basculement'); - } finally { - showLoading(false); - } -} - -async function loadDevices() { - try { - const response = await fetch('/api/devices'); - const devices = await response.json(); - - displayDevices(devices); - document.getElementById('deviceCount').textContent = devices.length; - } catch (error) { - console.error('Error loading devices:', error); - const devicesList = document.getElementById('devicesList'); - devicesList.innerHTML = '

Erreur de chargement

'; - } -} - -async function loadLogs() { - try { - const response = await fetch('/api/logs'); - const logs = await response.json(); - - const logContainer = document.getElementById('logContainer'); - logContainer.innerHTML = ''; - - if (logs.length === 0) { - logContainer.innerHTML = '

Aucun log disponible

'; - } else { - logs.forEach(log => addLogEntry(log)); - } - } catch (error) { - console.error('Error loading logs:', error); - } -} - -async function clearLogs() { - try { - const response = await fetch('/api/logs', { method: 'DELETE' }); - - if (response.ok) { - const logContainer = document.getElementById('logContainer'); - logContainer.innerHTML = '

Aucun log disponible

'; - showToast('success', 'Logs', 'Logs effacés'); - } - } catch (error) { - console.error('Error clearing logs:', error); - showToast('error', 'Erreur', 'Échec de la suppression des logs'); - } -} - -// ===== Display Functions ===== - -function updateStatusDisplay(status) { - // Update WiFi status badge - const wifiStatus = document.getElementById('wifiStatus'); - const wifiDot = wifiStatus.querySelector('.status-dot'); - const wifiText = wifiStatus.querySelector('.status-text'); - - // Use connectionState for more detailed status - const state = status.connectionState || (status.connected ? 'connected' : 'disconnected'); - appState.connectionState = state; - - switch (state) { - case 'connected': - wifiDot.className = 'status-dot active'; - wifiText.textContent = `Connecté: ${status.connectedSSID}`; - break; - case 'connecting': - wifiDot.className = 'status-dot connecting'; - wifiText.textContent = status.connectedSSID ? `Connexion à ${status.connectedSSID}...` : 'Connexion...'; - break; - case 'disconnecting': - wifiDot.className = 'status-dot disconnecting'; - wifiText.textContent = 'Déconnexion...'; - break; - case 'roaming': - wifiDot.className = 'status-dot active'; - wifiText.textContent = `Roaming: ${status.connectedSSID}`; - break; - case 'disconnected': - default: - wifiDot.className = 'status-dot offline'; - wifiText.textContent = 'Déconnecté'; - break; - } - - // Update hotspot status badge - const hotspotStatus = document.getElementById('hotspotStatus'); - const hotspotDot = hotspotStatus.querySelector('.status-dot'); - const hotspotText = hotspotStatus.querySelector('.status-text'); - const hotspotToggle = document.getElementById('hotspotToggle'); - - const isHotspotEnabled = status.hotspotStatus && status.hotspotStatus.state === 'ENABLED'; - - if (isHotspotEnabled) { - hotspotDot.className = 'status-dot active'; - const numStations = status.hotspotStatus.numStations || 0; - hotspotText.textContent = `Hotspot actif (${numStations} client${numStations > 1 ? 's' : ''})`; - hotspotToggle.checked = true; - } else { - hotspotDot.className = 'status-dot offline'; - hotspotText.textContent = 'Hotspot inactif'; - hotspotToggle.checked = false; - } - - appState.hotspotEnabled = isHotspotEnabled; - - // Update hotspot details if available - updateHotspotDetails(status.hotspotStatus); - - // Check if connectedSSID or state changed and refresh WiFi list if needed - const prevSSID = appState.connectedSSID; - const prevState = appState.connectionState; - appState.connectedSSID = status.connectedSSID; - - const connectedChanged = prevSSID !== status.connectedSSID || prevState !== state; - - if (connectedChanged && appState.networks.length > 0) { - displayWifiNetworks(appState.networks); - } - - // Update stats - document.getElementById('connectedDevices').textContent = status.connectedCount; - document.getElementById('dataUsage').textContent = `${status.dataUsage.toFixed(1)} MB`; - - // Update uptime in state (will be incremented by the interval) - appState.uptime = status.uptime; - document.getElementById('uptime').textContent = formatUptime(status.uptime); - - document.getElementById('currentNetwork').textContent = status.connectedSSID || '-'; -} - -function displayWifiNetworks(networks) { - const wifiList = document.getElementById('wifiList'); - wifiList.innerHTML = ''; - - if (networks.length === 0) { - wifiList.innerHTML = '
Aucun réseau trouvé
'; - return; - } - - networks.forEach(network => { - const wifiItem = document.createElement('div'); - wifiItem.className = 'wifi-item'; - - // Mark the network based on connection state - if (appState.connectedSSID && network.ssid === appState.connectedSSID) { - switch (appState.connectionState) { - case 'connected': - case 'roaming': - wifiItem.classList.add('connected'); - break; - case 'connecting': - wifiItem.classList.add('connecting'); - break; - case 'disconnecting': - wifiItem.classList.add('disconnecting'); - break; - } - } - - wifiItem.onclick = () => selectWifi(network, wifiItem); - - wifiItem.innerHTML = ` -
-
${escapeHtml(network.ssid)}
-
- ${escapeHtml(network.security)} - Canal ${escapeHtml(String(network.channel))} - ${escapeHtml(network.bssid)} -
-
-
- ${generateSignalBars(network.signal)} -
- `; - - wifiList.appendChild(wifiItem); - }); -} - -function selectWifi(network, element) { - document.querySelectorAll('.wifi-item').forEach(item => item.classList.remove('selected')); - element.classList.add('selected'); - appState.selectedWifi = network; -} - -function displayDevices(devices) { - const devicesList = document.getElementById('devicesList'); - devicesList.innerHTML = ''; - - if (devices.length === 0) { - devicesList.innerHTML = ` -
- - - -

Aucun appareil connecté

-
- `; - return; - } - - devices.forEach(device => { - const deviceCard = document.createElement('div'); - deviceCard.className = 'device-card'; - - deviceCard.innerHTML = ` - ${getDeviceIcon(device.type)} -
${escapeHtml(device.name)}
-
${device.type}
-
-
${device.ip}
-
${device.mac}
-
- `; - - devicesList.appendChild(deviceCard); - }); -} - -function addLogEntry(log) { - const logContainer = document.getElementById('logContainer'); - - // Remove placeholder if it exists - const placeholder = logContainer.querySelector('.log-placeholder'); - if (placeholder) { - placeholder.remove(); - } - - const logEntry = document.createElement('div'); - logEntry.className = 'log-entry'; - - const timestamp = new Date(log.timestamp).toLocaleTimeString('fr-FR'); - - const timestampSpan = document.createElement('span'); - timestampSpan.className = 'log-timestamp'; - timestampSpan.textContent = timestamp; - - const sourceSpan = document.createElement('span'); - sourceSpan.className = 'log-source'; - sourceSpan.textContent = `[${log.source}]`; - - const messageSpan = document.createElement('span'); - messageSpan.className = 'log-message'; - messageSpan.textContent = log.message; - - logEntry.appendChild(timestampSpan); - logEntry.appendChild(sourceSpan); - logEntry.appendChild(messageSpan); - - logContainer.appendChild(logEntry); - - if (appState.autoScrollLogs) { - logContainer.scrollTop = logContainer.scrollHeight; - } -} - -function createHotspotInfoItem(label, value) { - const item = document.createElement('div'); - item.className = 'hotspot-info-item'; - - const labelSpan = document.createElement('span'); - labelSpan.className = 'info-label'; - labelSpan.textContent = label + ':'; - - const valueSpan = document.createElement('span'); - valueSpan.className = 'info-value'; - valueSpan.textContent = value; - - item.appendChild(labelSpan); - item.appendChild(valueSpan); - - return item; -} - -function updateHotspotDetails(hotspotStatus) { - const detailsContainer = document.getElementById('hotspotDetails'); - if (!detailsContainer) return; - - // Clear existing content - detailsContainer.innerHTML = ''; - - if (!hotspotStatus || hotspotStatus.state !== 'ENABLED') { - detailsContainer.appendChild(createHotspotInfoItem('État', 'Inactif')); - return; - } - - detailsContainer.appendChild(createHotspotInfoItem('État', hotspotStatus.state)); - - if (hotspotStatus.ssid) { - detailsContainer.appendChild(createHotspotInfoItem('SSID', hotspotStatus.ssid)); - } - - if (hotspotStatus.channel) { - detailsContainer.appendChild(createHotspotInfoItem('Canal', `${hotspotStatus.channel} (${hotspotStatus.frequency} MHz)`)); - } - - if (hotspotStatus.numStations !== undefined) { - detailsContainer.appendChild(createHotspotInfoItem('Clients connectés', hotspotStatus.numStations.toString())); - } - - if (hotspotStatus.bssid) { - detailsContainer.appendChild(createHotspotInfoItem('BSSID', hotspotStatus.bssid)); - } -} - -// ===== WebSocket Functions ===== - -function connectWebSocket() { - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsUrl = `${protocol}//${window.location.host}/ws/logs`; - - try { - appState.ws = new WebSocket(wsUrl); - - appState.ws.onopen = function() { - console.log('WebSocket connected'); - appState.reconnectAttempts = 0; - }; - - appState.ws.onmessage = function(event) { - try { - const log = JSON.parse(event.data); - addLogEntry(log); - } catch (error) { - console.error('Error parsing WebSocket message:', error); - } - }; - - appState.ws.onerror = function(error) { - console.error('WebSocket error:', error); - }; - - appState.ws.onclose = function() { - console.log('WebSocket disconnected'); - - // Attempt to reconnect - if (appState.reconnectAttempts < appState.maxReconnectAttempts) { - appState.reconnectAttempts++; - setTimeout(connectWebSocket, 5000); - } - }; - } catch (error) { - console.error('Error creating WebSocket:', error); - } -} - -function connectWifiWebSocket() { - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsUrl = `${protocol}//${window.location.host}/ws/wifi`; - - try { - appState.wifiWs = new WebSocket(wsUrl); - - appState.wifiWs.onopen = function() { - console.log('WiFi WebSocket connected'); - appState.wifiReconnectAttempts = 0; - }; - - appState.wifiWs.onmessage = function(event) { - try { - const msg = JSON.parse(event.data); - handleWifiEvent(msg); - } catch (error) { - console.error('Error parsing WiFi WebSocket message:', error); - } - }; - - appState.wifiWs.onerror = function(error) { - console.error('WiFi WebSocket error:', error); - }; - - appState.wifiWs.onclose = function() { - console.log('WiFi WebSocket disconnected'); - - // Attempt to reconnect - if (appState.wifiReconnectAttempts < appState.maxReconnectAttempts) { - appState.wifiReconnectAttempts++; - setTimeout(connectWifiWebSocket, 5000); - } - }; - } catch (error) { - console.error('Error creating WiFi WebSocket:', error); - } -} - -function handleWifiEvent(event) { - console.log('WiFi event received:', event.type, event); - - switch (event.type) { - case 'scan_update': - handleScanUpdate(event.data); - break; - case 'state_change': - handleStateChange(event.data); - break; - case 'signal_update': - handleSignalUpdate(event.data); - break; - default: - console.warn('Unknown WiFi event type:', event.type); - } -} - -function handleScanUpdate(data) { - // Update the network list in real-time - appState.networks = data.networks; - displayWifiNetworks(data.networks); - console.log(`Scan update: ${data.networks.length} network(s) found`); -} - -function handleStateChange(data) { - // Update WiFi status badge - const wifiStatus = document.getElementById('wifiStatus'); - const wifiDot = wifiStatus.querySelector('.status-dot'); - const wifiText = wifiStatus.querySelector('.status-text'); - - // Update state in appState - appState.connectionState = data.state; - - switch (data.state) { - case 'connected': - wifiDot.className = 'status-dot active'; - wifiText.textContent = `Connecté: ${data.ssid}`; - appState.connectedSSID = data.ssid; - - // Refresh network list to show connected network - if (appState.networks.length > 0) { - displayWifiNetworks(appState.networks); - } - break; - - case 'connecting': - wifiDot.className = 'status-dot connecting'; - wifiText.textContent = data.ssid ? `Connexion à ${data.ssid}...` : 'Connexion...'; - - // Refresh network list to show connecting state - if (appState.networks.length > 0) { - displayWifiNetworks(appState.networks); - } - break; - - case 'disconnecting': - wifiDot.className = 'status-dot disconnecting'; - wifiText.textContent = 'Déconnexion...'; - - // Refresh network list to show disconnecting state - if (appState.networks.length > 0) { - displayWifiNetworks(appState.networks); - } - break; - - case 'roaming': - wifiDot.className = 'status-dot active'; - wifiText.textContent = `Roaming: ${data.ssid}`; - appState.connectedSSID = data.ssid; - break; - - case 'disconnected': - wifiDot.className = 'status-dot offline'; - wifiText.textContent = 'Déconnecté'; - appState.connectedSSID = null; - - // Refresh network list to remove connected highlighting - if (appState.networks.length > 0) { - displayWifiNetworks(appState.networks); - } - break; - - default: - console.warn('Unknown WiFi state:', data.state); - break; - } - - console.log(`WiFi state changed: ${data.previous_state} → ${data.state}`, data.ssid); -} - -function handleSignalUpdate(data) { - // Update signal strength display if needed - console.log(`Signal update for ${data.ssid}: ${data.signal}/5 (${data.dbm} dBm)`); - // Could update the network list to reflect new signal strength -} - -// ===== Utility Functions ===== - -function generateSignalBars(strength) { - const bars = []; - for (let i = 1; i <= 4; i++) { - const active = i <= strength ? 'active' : ''; - bars.push(`
`); - } - return `
${bars.join('')}
`; -} - -function getDeviceIcon(type) { - const icons = { - mobile: ``, - laptop: ``, - tablet: ``, - desktop: ``, - unknown: `` - }; - return icons[type] || icons.unknown; -} - -function formatUptime(seconds) { - const hours = Math.floor(seconds / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - const secs = seconds % 60; - - return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; -} - -function escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; -} - -function showToast(type, title, message) { - const toast = document.getElementById('toast'); - const toastIcon = document.getElementById('toastIcon'); - const toastTitle = document.getElementById('toastTitle'); - const toastMessage = document.getElementById('toastMessage'); - - const icons = { - success: '', - error: '', - warning: '', - info: '' - }; - - toast.className = `toast ${type}`; - toastIcon.innerHTML = icons[type]; - toastTitle.textContent = title; - toastMessage.textContent = message; - - setTimeout(() => toast.classList.add('show'), 10); - - setTimeout(() => toast.classList.remove('show'), 5000); -} - -function hideToast() { - document.getElementById('toast').classList.remove('show'); -} - -function showLoading(show) { - const overlay = document.getElementById('loadingOverlay'); - if (show) { - overlay.classList.add('show'); - } else { - overlay.classList.remove('show'); - } -} - -function toggleAutoScroll() { - appState.autoScrollLogs = !appState.autoScrollLogs; - const btn = document.getElementById('autoScrollBtn'); - - if (appState.autoScrollLogs) { - btn.style.opacity = '1'; - showToast('info', 'Auto-scroll', 'Auto-scroll activé'); - } else { - btn.style.opacity = '0.5'; - showToast('info', 'Auto-scroll', 'Auto-scroll désactivé'); - } -} - -// ===== Periodic Updates ===== - -function startPeriodicUpdates() { - // Update status every 5 seconds - setInterval(() => { - loadStatus(); - }, 5000); - - // Update devices every 10 seconds - setInterval(() => { - loadDevices(); - }, 10000); -} - -function startUptimeCounter() { - // Clear any existing interval - if (appState.uptimeInterval) { - clearInterval(appState.uptimeInterval); - } - - // Increment uptime every second - appState.uptimeInterval = setInterval(() => { - appState.uptime++; - document.getElementById('uptime').textContent = formatUptime(appState.uptime); - }, 1000); -} diff --git a/cmd/repeater/static/index.html b/cmd/repeater/static/index.html deleted file mode 100644 index d53a645..0000000 --- a/cmd/repeater/static/index.html +++ /dev/null @@ -1,231 +0,0 @@ - - - - - - Travel Router Control Panel - - - -
-
-
-

- - - - Travel Router Control -

-
-
- - Déconnecté -
-
- - Hotspot actif -
-
-
-
- -
-
-
- - - -
-
-
0
-
Appareils connectés
-
-
-
-
- - - -
-
-
0 MB
-
Données utilisées
-
-
-
-
- - - -
-
-
00:00:00
-
Temps de fonctionnement
-
-
-
-
- - - -
-
-
-
-
Réseau actuel
-
-
-
- -
-
-
-

- - - - Connexion WiFi Upstream -

- -
- -
- -
-
- Chargement des réseaux... -
-
-
- -
- - -
- -
- - -
-
- -
-
-

- - - - Hotspot Status -

-
- - -
-
- -
-
- État: - Chargement... -
-
-
-
- -
-
-
-

- - - - Appareils connectés -

- 0 -
- -
-
- - - -

Aucun appareil connecté

-
-
-
- -
-
-

- - - - Logs système -

-
- - -
-
- -
-
- - - -

Aucun log disponible

-
-
-
-
-
- - -
-
-
-
-
-
- -
- - -
-
-

Chargement...

-
- - - - diff --git a/cmd/repeater/static/style.css b/cmd/repeater/static/style.css deleted file mode 100644 index b518696..0000000 --- a/cmd/repeater/static/style.css +++ /dev/null @@ -1,878 +0,0 @@ -/* CSS Variables for theming */ -:root { - --primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - --secondary-gradient: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); - --success-color: #10b981; - --danger-color: #ef4444; - --warning-color: #f59e0b; - --info-color: #3b82f6; - --background: #f3f4f6; - --card-background: #ffffff; - --text-primary: #1f2937; - --text-secondary: #6b7280; - --border-color: #e5e7eb; - --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); - --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); - --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1); - --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1); - --radius-sm: 0.375rem; - --radius-md: 0.5rem; - --radius-lg: 0.75rem; - --radius-xl: 1rem; -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; - background: var(--primary-gradient); - min-height: 100vh; - padding: 1.5rem; - color: var(--text-primary); -} - -.container { - max-width: 1400px; - margin: 0 auto; - background: rgba(255, 255, 255, 0.98); - backdrop-filter: blur(20px); - border-radius: var(--radius-xl); - padding: 2rem; - box-shadow: var(--shadow-xl); -} - -/* Header Styles */ -.header { - margin-bottom: 2rem; - padding-bottom: 1.5rem; - border-bottom: 2px solid var(--border-color); -} - -.header-content { - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - gap: 1rem; -} - -.header h1 { - color: var(--text-primary); - font-size: 2rem; - font-weight: 700; - display: flex; - align-items: center; - gap: 0.75rem; -} - -.logo-icon { - color: #667eea; -} - -.connection-info { - display: flex; - gap: 1rem; - flex-wrap: wrap; -} - -.status-badge { - display: inline-flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 1rem; - border-radius: 9999px; - font-size: 0.875rem; - font-weight: 500; - background: var(--background); - border: 1px solid var(--border-color); -} - -.status-dot { - width: 0.625rem; - height: 0.625rem; - border-radius: 50%; - animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; -} - -.status-dot.active { - background: var(--success-color); -} - -.status-dot.offline { - background: var(--text-secondary); - animation: none; -} - -.status-dot.connecting { - background: var(--warning-color); - animation: blink 1s ease-in-out infinite; -} - -.status-dot.disconnecting { - background: var(--warning-color); - animation: blink 1s ease-in-out infinite; -} - -@keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } -} - -@keyframes blink { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.3; } -} - -/* Stats Grid */ -.stats-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); - gap: 1.5rem; - margin-bottom: 2rem; -} - -.stat-card { - background: var(--card-background); - border-radius: var(--radius-lg); - padding: 1.5rem; - box-shadow: var(--shadow-md); - border: 1px solid var(--border-color); - display: flex; - gap: 1rem; - align-items: center; - transition: all 0.3s ease; - max-width: 100%; -} - -.stat-card:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-lg); -} - -.stat-icon { - width: 3rem; - height: 3rem; - border-radius: var(--radius-md); - background: var(--primary-gradient); - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; -} - -.stat-icon svg { - color: white; -} - -.stat-content { - flex: 1; - min-width: 0; -} - -.stat-value { - font-size: 1.875rem; - font-weight: 700; - color: var(--text-primary); - line-height: 1; - margin-bottom: 0.25rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.stat-label { - font-size: 0.875rem; - color: var(--text-secondary); - font-weight: 500; -} - -/* Grid Layout */ -.grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(450px, 1fr)); - gap: 1.5rem; - margin-bottom: 1.5rem; -} - -/* Card Styles */ -.card { - background: var(--card-background); - border-radius: var(--radius-lg); - padding: 1.5rem; - box-shadow: var(--shadow-md); - border: 1px solid var(--border-color); - transition: all 0.3s ease; -} - -.card:hover { - box-shadow: var(--shadow-lg); -} - -.card-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1.5rem; - padding-bottom: 1rem; - border-bottom: 1px solid var(--border-color); -} - -.card-header h2 { - color: var(--text-primary); - font-size: 1.25rem; - font-weight: 600; - display: flex; - align-items: center; - gap: 0.5rem; - margin: 0; -} - -.icon { - color: #667eea; -} - -/* Form Elements */ -.form-group { - margin-bottom: 1.25rem; -} - -.form-group label { - display: flex; - align-items: center; - gap: 0.5rem; - margin-bottom: 0.5rem; - color: var(--text-primary); - font-weight: 500; - font-size: 0.875rem; -} - -.input-icon { - color: var(--text-secondary); -} - -.form-group input, -.form-group select { - width: 100%; - padding: 0.75rem 1rem; - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - font-size: 0.9375rem; - transition: all 0.2s ease; - background: var(--card-background); - color: var(--text-primary); -} - -.form-group input:focus, -.form-group select:focus { - outline: none; - border-color: #667eea; - box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); -} - -/* Buttons */ -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 0.5rem; - padding: 0.75rem 1.5rem; - border: none; - border-radius: var(--radius-md); - font-size: 0.9375rem; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; - width: 100%; - color: white; -} - -.btn svg { - flex-shrink: 0; -} - -.btn-primary { - background: var(--primary-gradient); -} - -.btn-primary:hover { - transform: translateY(-1px); - box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3); -} - -.btn-secondary { - background: var(--text-secondary); -} - -.btn-secondary:hover { - background: #4b5563; - transform: translateY(-1px); -} - -.btn-danger { - background: var(--danger-color); -} - -.btn-danger:hover { - background: #dc2626; - transform: translateY(-1px); -} - -.btn:active { - transform: translateY(0); -} - -.btn:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.btn-icon { - background: transparent; - border: none; - padding: 0.5rem; - border-radius: var(--radius-sm); - cursor: pointer; - color: var(--text-secondary); - transition: all 0.2s ease; - display: inline-flex; - align-items: center; - justify-content: center; -} - -.btn-icon:hover { - background: var(--background); - color: var(--text-primary); -} - -.button-group { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 0.75rem; -} - -/* Toggle Switch */ -.toggle-switch { - position: relative; - display: inline-block; - width: 50px; - height: 26px; -} - -.toggle-switch input { - opacity: 0; - width: 0; - height: 0; -} - -.toggle-switch label { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: var(--text-secondary); - transition: 0.3s; - border-radius: 34px; -} - -.toggle-switch label:before { - position: absolute; - content: ""; - height: 20px; - width: 20px; - left: 3px; - bottom: 3px; - background-color: white; - transition: 0.3s; - border-radius: 50%; -} - -.toggle-switch input:checked + label { - background: var(--success-color); -} - -.toggle-switch input:checked + label:before { - transform: translateX(24px); -} - -/* WiFi List */ -.wifi-list { - max-height: 320px; - overflow-y: auto; - border: 1px solid var(--border-color); - border-radius: var(--radius-md); - background: var(--background); -} - -.wifi-item { - padding: 1rem; - border-bottom: 1px solid var(--border-color); - cursor: pointer; - transition: all 0.2s ease; - display: flex; - justify-content: space-between; - align-items: center; - background: var(--card-background); -} - -.wifi-item:hover { - background: var(--background); -} - -.wifi-item:last-child { - border-bottom: none; -} - -.wifi-item.selected { - background: #eff6ff; - border-left: 4px solid #667eea; -} - -.wifi-item.connected { - background: #d1fae5; - border-left-color: var(--success-color) !important; -} - -.wifi-item.connected .wifi-ssid { - color: var(--success-color); - font-weight: 700; -} - -.wifi-item.connecting { - background: #fef3c7; - border-left-color: var(--warning-color) !important; - animation: pulse-item 1.5s ease-in-out infinite; -} - -.wifi-item.connecting .wifi-ssid { - color: var(--warning-color); - font-weight: 700; -} - -.wifi-item.disconnecting { - background: #fee2e2; - border-left-color: #dc2626 !important; - animation: pulse-item 1.5s ease-in-out infinite; -} - -.wifi-item.disconnecting .wifi-ssid { - color: #dc2626; - font-weight: 700; -} - -@keyframes pulse-item { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.7; } -} - -.wifi-item.loading { - justify-content: center; - color: var(--text-secondary); - cursor: default; -} - -.wifi-info { - flex: 1; -} - -.wifi-ssid { - font-weight: 600; - color: var(--text-primary); - margin-bottom: 0.25rem; -} - -.wifi-details { - font-size: 0.8125rem; - color: var(--text-secondary); - display: flex; - gap: 1rem; -} - -.wifi-signal { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.signal-bars { - display: flex; - gap: 2px; - align-items: flex-end; - height: 16px; -} - -.signal-bar { - width: 3px; - background: var(--border-color); - border-radius: 2px; -} - -.signal-bar:nth-child(1) { height: 25%; } -.signal-bar:nth-child(2) { height: 50%; } -.signal-bar:nth-child(3) { height: 75%; } -.signal-bar:nth-child(4) { height: 100%; } - -.signal-bar.active { - background: var(--success-color); -} - -/* Devices */ -.devices-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - gap: 1rem; - min-height: 200px; -} - -.device-card { - background: var(--background); - border-radius: var(--radius-md); - padding: 1.25rem; - text-align: center; - border: 1px solid var(--border-color); - transition: all 0.2s ease; -} - -.device-card:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-md); -} - -.device-icon { - width: 48px; - height: 48px; - margin: 0 auto 0.75rem; - color: #667eea; -} - -.device-name { - font-weight: 600; - color: var(--text-primary); - margin-bottom: 0.25rem; - font-size: 0.9375rem; -} - -.device-type { - font-size: 0.75rem; - color: var(--text-secondary); - text-transform: uppercase; - letter-spacing: 0.05em; - margin-bottom: 0.5rem; -} - -.device-info { - font-size: 0.8125rem; - color: var(--text-secondary); -} - -.device-placeholder { - grid-column: 1 / -1; - text-align: center; - padding: 3rem 1rem; - color: var(--text-secondary); -} - -.device-placeholder svg { - margin-bottom: 1rem; -} - -.device-count { - background: var(--primary-gradient); - color: white; - padding: 0.25rem 0.75rem; - border-radius: 9999px; - font-size: 0.875rem; - font-weight: 600; -} - -/* Logs */ -.log-container { - background: #1f2937; - color: #10b981; - padding: 1rem; - border-radius: var(--radius-md); - font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Mono', 'Consolas', 'Courier New', monospace; - font-size: 0.8125rem; - max-height: 400px; - overflow-y: auto; - line-height: 1.6; - min-height: 200px; -} - -.log-entry { - margin-bottom: 0.5rem; - display: flex; - gap: 0.75rem; -} - -.log-timestamp { - color: #6b7280; - flex-shrink: 0; -} - -.log-source { - color: #3b82f6; - font-weight: 600; - flex-shrink: 0; - min-width: 80px; -} - -.log-message { - color: #d1d5db; -} - -.log-controls { - display: flex; - gap: 0.5rem; -} - -.log-placeholder { - text-align: center; - padding: 3rem 1rem; - color: #6b7280; -} - -.log-placeholder svg { - margin-bottom: 1rem; -} - -/* Toast Notifications */ -.toast { - position: fixed; - top: 1.5rem; - right: 1.5rem; - max-width: 400px; - background: white; - border-radius: var(--radius-lg); - box-shadow: var(--shadow-xl); - padding: 1rem; - display: flex; - gap: 0.75rem; - align-items: start; - transform: translateX(calc(100% + 2rem)); - transition: transform 0.3s ease; - z-index: 1000; - border: 1px solid var(--border-color); -} - -.toast.show { - transform: translateX(0); -} - -.toast-icon { - width: 40px; - height: 40px; - border-radius: var(--radius-md); - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; -} - -.toast.success .toast-icon { - background: #d1fae5; - color: var(--success-color); -} - -.toast.error .toast-icon { - background: #fee2e2; - color: var(--danger-color); -} - -.toast.warning .toast-icon { - background: #fef3c7; - color: var(--warning-color); -} - -.toast.info .toast-icon { - background: #dbeafe; - color: var(--info-color); -} - -.toast-content { - flex: 1; -} - -.toast-title { - font-weight: 600; - color: var(--text-primary); - margin-bottom: 0.25rem; -} - -.toast-message { - font-size: 0.875rem; - color: var(--text-secondary); -} - -.toast-close { - background: transparent; - border: none; - color: var(--text-secondary); - cursor: pointer; - padding: 0.25rem; - border-radius: var(--radius-sm); - display: flex; - align-items: center; - justify-content: center; - transition: all 0.2s ease; -} - -.toast-close:hover { - background: var(--background); - color: var(--text-primary); -} - -/* Loading Overlay */ -.loading-overlay { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.5); - backdrop-filter: blur(4px); - display: none; - align-items: center; - justify-content: center; - z-index: 2000; - flex-direction: column; - gap: 1rem; -} - -.loading-overlay.show { - display: flex; -} - -.loading-overlay p { - color: white; - font-weight: 500; -} - -.spinner { - width: 48px; - height: 48px; - border: 4px solid rgba(255, 255, 255, 0.3); - border-top-color: white; - border-radius: 50%; - animation: spin 0.8s linear infinite; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -/* Scrollbar Styling */ -.wifi-list::-webkit-scrollbar, -.log-container::-webkit-scrollbar { - width: 8px; -} - -.wifi-list::-webkit-scrollbar-track, -.log-container::-webkit-scrollbar-track { - background: transparent; -} - -.wifi-list::-webkit-scrollbar-thumb { - background: var(--border-color); - border-radius: 4px; -} - -.log-container::-webkit-scrollbar-thumb { - background: #374151; - border-radius: 4px; -} - -.wifi-list::-webkit-scrollbar-thumb:hover, -.log-container::-webkit-scrollbar-thumb:hover { - background: var(--text-secondary); -} - -/* Hotspot Details */ -.hotspot-details { - margin-top: 1.5rem; - padding: 1rem; - background: var(--background); - border-radius: var(--radius-md); - border: 1px solid var(--border-color); -} - -.hotspot-info-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0.5rem 0; - border-bottom: 1px solid var(--border-color); -} - -.hotspot-info-item:last-child { - border-bottom: none; -} - -.info-label { - font-weight: 500; - color: var(--text-secondary); - font-size: 0.875rem; -} - -.info-value { - font-weight: 600; - color: var(--text-primary); - font-size: 0.875rem; -} - -/* Responsive Design */ -@media (max-width: 1024px) { - .grid { - grid-template-columns: 1fr; - } -} - -@media (max-width: 768px) { - body { - padding: 1rem; - } - - .container { - padding: 1.5rem; - } - - .header h1 { - font-size: 1.5rem; - } - - .header-content { - flex-direction: column; - align-items: flex-start; - } - - .stats-grid { - grid-template-columns: 1fr; - } - - .grid { - grid-template-columns: 1fr; - } - - .button-group { - grid-template-columns: 1fr; - } - - .toast { - left: 1rem; - right: 1rem; - max-width: none; - } -} - -@media (max-width: 480px) { - .stat-card { - flex-direction: column; - text-align: center; - } - - .devices-grid { - grid-template-columns: 1fr; - } -} diff --git a/generate.go b/generate.go deleted file mode 100644 index c01b94b..0000000 --- a/generate.go +++ /dev/null @@ -1,4 +0,0 @@ -package main - -//go:generate go tool oapi-codegen -config oapi-types.cfg.yaml openapi.yaml -//go:generate go tool oapi-codegen -config oapi-gin.cfg.yaml openapi.yaml diff --git a/go.mod b/go.mod index 5e17b30..0f2fed9 100644 --- a/go.mod +++ b/go.mod @@ -1,62 +1,9 @@ -module github.com/nemunaire/repeater +module git.nemunai.re/nemunaire/repeater go 1.24.4 require ( - github.com/getkin/kin-openapi v0.132.0 - github.com/gin-gonic/gin v1.11.0 github.com/godbus/dbus/v5 v5.1.0 github.com/gorilla/mux v1.8.1 github.com/gorilla/websocket v1.5.3 ) - -require ( - github.com/bytedance/sonic v1.14.0 // indirect - github.com/bytedance/sonic/loader v0.3.0 // indirect - github.com/cloudwego/base64x v0.1.6 // indirect - github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect - github.com/gabriel-vasile/mimetype v1.4.8 // indirect - github.com/gin-contrib/sse v1.1.0 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/swag v0.23.0 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.27.0 // indirect - github.com/goccy/go-json v0.10.2 // indirect - github.com/goccy/go-yaml v1.18.0 // indirect - github.com/josharian/intern v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.3.0 // indirect - github.com/leodido/go-urn v1.4.0 // indirect - github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect - github.com/oapi-codegen/oapi-codegen/v2 v2.5.0 // indirect - github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect - github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/perimeterx/marshmallow v1.1.5 // indirect - github.com/quic-go/qpack v0.5.1 // indirect - github.com/quic-go/quic-go v0.54.0 // indirect - github.com/speakeasy-api/jsonpath v0.6.0 // indirect - github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.3.0 // indirect - github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect - go.uber.org/mock v0.5.0 // indirect - golang.org/x/arch v0.20.0 // indirect - golang.org/x/crypto v0.40.0 // indirect - golang.org/x/mod v0.25.0 // indirect - golang.org/x/net v0.42.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.27.0 // indirect - golang.org/x/tools v0.34.0 // indirect - google.golang.org/protobuf v1.36.9 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) - -tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen diff --git a/go.sum b/go.sum index 17956cd..0546936 100644 --- a/go.sum +++ b/go.sum @@ -1,239 +1,6 @@ -github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= -github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= -github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= -github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= -github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58= -github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w= -github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= -github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= -github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk= -github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58= -github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= -github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= -github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= -github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= -github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= -github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= -github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 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/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 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= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= -github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= -github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/oapi-codegen/oapi-codegen/v2 v2.5.0 h1:iJvF8SdB/3/+eGOXEpsWkD8FQAHj6mqkb6Fnsoc8MFU= -github.com/oapi-codegen/oapi-codegen/v2 v2.5.0/go.mod h1:fwlMxUEMuQK5ih9aymrxKPQqNm2n8bdLk1ppjH+lr9w= -github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= -github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= -github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= -github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= -github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= -github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= -github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= -github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g8DHMXJ8= -github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= -github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU= -github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= -github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= -github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= -github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= -go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= -golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= -golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= -golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= -golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= -golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= -google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/api/handlers/handlers.go b/internal/api/handlers/handlers.go deleted file mode 100644 index 7c87a7c..0000000 --- a/internal/api/handlers/handlers.go +++ /dev/null @@ -1,148 +0,0 @@ -package handlers - -import ( - "net/http" - - "github.com/gin-gonic/gin" - "github.com/nemunaire/repeater/internal/config" - "github.com/nemunaire/repeater/internal/hotspot" - "github.com/nemunaire/repeater/internal/logging" - "github.com/nemunaire/repeater/internal/models" - "github.com/nemunaire/repeater/internal/station" - "github.com/nemunaire/repeater/internal/wifi" -) - -// GetWiFiNetworks returns cached WiFi networks without scanning -func GetWiFiNetworks(c *gin.Context) { - networks, err := wifi.GetCachedNetworks() - if err != nil { - logging.AddLog("WiFi", "Erreur lors de la récupération des réseaux: "+err.Error()) - c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur lors de la récupération des réseaux"}) - return - } - - c.JSON(http.StatusOK, networks) -} - -// 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) { - // Determine current state - isEnabled := status.HotspotStatus != nil && status.HotspotStatus.State == "ENABLED" - - var err error - if !isEnabled { - 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 - } - - // Update status immediately - status.HotspotStatus = hotspot.GetDetailedStatus() - - c.JSON(http.StatusOK, gin.H{"enabled": !isEnabled}) -} - -// GetDevices returns connected devices -func GetDevices(c *gin.Context, cfg *config.Config) { - devices, err := station.GetStations() - 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"}) -} diff --git a/internal/api/handlers/websocket.go b/internal/api/handlers/websocket.go deleted file mode 100644 index 87d2823..0000000 --- a/internal/api/handlers/websocket.go +++ /dev/null @@ -1,38 +0,0 @@ -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 - } - } -} diff --git a/internal/api/handlers/websocket_wifi.go b/internal/api/handlers/websocket_wifi.go deleted file mode 100644 index ff8d132..0000000 --- a/internal/api/handlers/websocket_wifi.go +++ /dev/null @@ -1,30 +0,0 @@ -package handlers - -import ( - "log" - - "github.com/gin-gonic/gin" - "github.com/nemunaire/repeater/internal/wifi" -) - -// WebSocketWifi handles WebSocket connections for real-time WiFi events -func WebSocketWifi(c *gin.Context) { - conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) - if err != nil { - log.Printf("Erreur WebSocket WiFi: %v", err) - return - } - defer conn.Close() - - // Register client - wifi.RegisterWebSocketClient(conn) - defer wifi.UnregisterWebSocketClient(conn) - - // Keep connection alive - for { - _, _, err := conn.ReadMessage() - if err != nil { - break - } - } -} diff --git a/internal/api/router.go b/internal/api/router.go deleted file mode 100644 index 956661e..0000000 --- a/internal/api/router.go +++ /dev/null @@ -1,69 +0,0 @@ -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/config" - "github.com/nemunaire/repeater/internal/models" -) - -// SetupRouter creates and configures the Gin router -func SetupRouter(status *models.SystemStatus, cfg *config.Config, assets embed.FS) *gin.Engine { - // Set Gin to release mode (can be overridden with GIN_MODE env var) - gin.SetMode(gin.ReleaseMode) - - r := gin.Default() - - // API routes - api := r.Group("/api") - { - // WiFi endpoints - wifi := api.Group("/wifi") - { - wifi.GET("/networks", handlers.GetWiFiNetworks) - 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", func(c *gin.Context) { - handlers.GetDevices(c, cfg) - }) - - // 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 endpoints - r.GET("/ws/logs", handlers.WebSocketLogs) - r.GET("/ws/wifi", handlers.WebSocketWifi) - - // 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 -} diff --git a/internal/app/app.go b/internal/app/app.go deleted file mode 100644 index 3e55b49..0000000 --- a/internal/app/app.go +++ /dev/null @@ -1,287 +0,0 @@ -package app - -import ( - "embed" - "log" - "os" - "strconv" - "strings" - "sync" - "time" - - "github.com/nemunaire/repeater/internal/api" - "github.com/nemunaire/repeater/internal/config" - "github.com/nemunaire/repeater/internal/hotspot" - "github.com/nemunaire/repeater/internal/logging" - "github.com/nemunaire/repeater/internal/models" - "github.com/nemunaire/repeater/internal/station" - "github.com/nemunaire/repeater/internal/station/backend" - "github.com/nemunaire/repeater/internal/syslog" - "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 - Config *config.Config - SyslogTailer *syslog.SyslogTailer -} - -// New creates a new application instance -func New(assets embed.FS) *App { - return &App{ - Status: models.SystemStatus{ - Connected: false, - ConnectionState: "disconnected", - ConnectedSSID: "", - HotspotStatus: nil, - ConnectedCount: 0, - DataUsage: 0.0, - Uptime: 0, - }, - StartTime: time.Now(), - Assets: assets, - } -} - -// Initialize initializes the application -func (a *App) Initialize(cfg *config.Config) error { - // Store config reference - a.Config = cfg - - // Initialize WiFi backend - if err := wifi.Initialize(cfg.WifiInterface, cfg.WifiBackend); err != nil { - return err - } - - // Start WiFi event monitoring - if err := wifi.StartEventMonitoring(); err != nil { - log.Printf("Warning: WiFi event monitoring failed: %v", err) - // Don't fail - polling fallback still works - } - - // Initialize station backend - stationConfig := backend.BackendConfig{ - InterfaceName: cfg.HotspotInterface, - ARPTablePath: cfg.ARPTablePath, - DHCPLeasesPath: cfg.DHCPLeasesPath, - HostapdInterface: cfg.HotspotInterface, - } - if err := station.Initialize(cfg.StationBackend, stationConfig); err != nil { - log.Printf("Warning: Station backend initialization failed: %v", err) - // Don't fail - will continue without station discovery - } else { - // Start event monitoring for station events - if err := station.StartEventMonitoring(backend.EventCallbacks{ - OnStationConnected: a.handleStationConnected, - OnStationDisconnected: a.handleStationDisconnected, - OnStationUpdated: a.handleStationUpdated, - }); err != nil { - log.Printf("Warning: Station event monitoring failed: %v", err) - // Don't fail - polling fallback still works - } - } - - // Start syslog tailing if enabled - if cfg.SyslogEnabled { - a.SyslogTailer = syslog.NewSyslogTailer( - cfg.SyslogPath, - cfg.SyslogFilter, - cfg.SyslogSource, - ) - if err := a.SyslogTailer.Start(); err != nil { - log.Printf("Warning: Failed to start syslog tailing: %v", err) - // Don't fail - app continues without syslog - } - } - - // 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.Config, 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() { - // Stop syslog tailing if running - if a.SyslogTailer != nil { - a.SyslogTailer.Stop() - } - - // Stop station monitoring and close backend - station.StopEventMonitoring() - station.Close() - - wifi.StopEventMonitoring() - wifi.Close() - logging.AddLog("Système", "Application arrêtée") -} - -// getSystemUptime reads system uptime from /proc/uptime -func getSystemUptime() int64 { - data, err := os.ReadFile("/proc/uptime") - if err != nil { - log.Printf("Error reading /proc/uptime: %v", err) - return 0 - } - - fields := strings.Fields(string(data)) - if len(fields) == 0 { - return 0 - } - - uptime, err := strconv.ParseFloat(fields[0], 64) - if err != nil { - log.Printf("Error parsing uptime: %v", err) - return 0 - } - - return int64(uptime) -} - -// getInterfaceBytes reads rx and tx bytes for a network interface -func getInterfaceBytes(interfaceName string) (rxBytes, txBytes int64) { - rxPath := "/sys/class/net/" + interfaceName + "/statistics/rx_bytes" - txPath := "/sys/class/net/" + interfaceName + "/statistics/tx_bytes" - - // Read RX bytes - rxData, err := os.ReadFile(rxPath) - if err != nil { - log.Printf("Error reading rx_bytes for %s: %v", interfaceName, err) - } else { - rxBytes, _ = strconv.ParseInt(strings.TrimSpace(string(rxData)), 10, 64) - } - - // Read TX bytes - txData, err := os.ReadFile(txPath) - if err != nil { - log.Printf("Error reading tx_bytes for %s: %v", interfaceName, err) - } else { - txBytes, _ = strconv.ParseInt(strings.TrimSpace(string(txData)), 10, 64) - } - - return rxBytes, txBytes -} - -// 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() - a.Status.ConnectionState = wifi.GetConnectionState() - a.Status.ConnectedSSID = wifi.GetConnectedSSID() - a.Status.Uptime = getSystemUptime() - - // Get detailed hotspot status - a.Status.HotspotStatus = hotspot.GetDetailedStatus() - - // Get network data usage for WiFi interface - if a.Config != nil { - rxBytes, txBytes := getInterfaceBytes(a.Config.WifiInterface) - // Convert to MB and sum rx + tx - a.Status.DataUsage = float64(rxBytes+txBytes) / (1024 * 1024) - } - - 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 := station.GetStations() - if err != nil { - log.Printf("Error getting connected devices: %v", err) - } - - a.StatusMutex.Lock() - a.Status.ConnectedDevices = devices - a.Status.ConnectedCount = len(devices) - a.StatusMutex.Unlock() - } -} - -// handleStationConnected handles station connection events -func (a *App) handleStationConnected(st backend.Station) { - a.StatusMutex.Lock() - defer a.StatusMutex.Unlock() - - // Convert backend.Station to models.ConnectedDevice - device := models.ConnectedDevice{ - Name: st.Hostname, - Type: st.Type, - MAC: st.MAC, - IP: st.IP, - } - - // Check if device already exists - found := false - for i, d := range a.Status.ConnectedDevices { - if d.MAC == device.MAC { - a.Status.ConnectedDevices[i] = device - found = true - break - } - } - - // Add new device if not found - if !found { - a.Status.ConnectedDevices = append(a.Status.ConnectedDevices, device) - a.Status.ConnectedCount = len(a.Status.ConnectedDevices) - logging.AddLog("Stations", "Device connected: "+device.MAC+" ("+device.IP+")") - } -} - -// handleStationDisconnected handles station disconnection events -func (a *App) handleStationDisconnected(mac string) { - a.StatusMutex.Lock() - defer a.StatusMutex.Unlock() - - // Remove device from list - for i, d := range a.Status.ConnectedDevices { - if d.MAC == mac { - a.Status.ConnectedDevices = append(a.Status.ConnectedDevices[:i], a.Status.ConnectedDevices[i+1:]...) - a.Status.ConnectedCount = len(a.Status.ConnectedDevices) - logging.AddLog("Stations", "Device disconnected: "+mac) - break - } - } -} - -// handleStationUpdated handles station update events -func (a *App) handleStationUpdated(st backend.Station) { - a.StatusMutex.Lock() - defer a.StatusMutex.Unlock() - - // Update existing device - for i, d := range a.Status.ConnectedDevices { - if d.MAC == st.MAC { - a.Status.ConnectedDevices[i] = models.ConnectedDevice{ - Name: st.Hostname, - Type: st.Type, - MAC: st.MAC, - IP: st.IP, - } - break - } - } -} diff --git a/internal/config/cli.go b/internal/config/cli.go deleted file mode 100644 index 49013b3..0000000 --- a/internal/config/cli.go +++ /dev/null @@ -1,34 +0,0 @@ -package config - -import ( - "flag" -) - -// declareFlags registers flags for the structure Options. -func declareFlags(o *Config) { - flag.StringVar(&o.Bind, "bind", ":8081", "Bind port/socket") - flag.StringVar(&o.WifiInterface, "wifi-interface", "wlan0", "WiFi interface name") - flag.StringVar(&o.HotspotInterface, "hotspot-interface", "wlan1", "Hotspot WiFi interface name") - flag.StringVar(&o.WifiBackend, "wifi-backend", "", "WiFi backend to use: 'iwd' or 'wpasupplicant' (required)") - flag.StringVar(&o.StationBackend, "station-backend", "hostapd", "Station discovery backend: 'arp', 'dhcp', or 'hostapd'") - flag.StringVar(&o.DHCPLeasesPath, "dhcp-leases-path", "/var/lib/dhcp/dhcpd.leases", "Path to DHCP leases file") - flag.StringVar(&o.ARPTablePath, "arp-table-path", "/proc/net/arp", "Path to ARP table file") - flag.BoolVar(&o.SyslogEnabled, "syslog-enabled", false, "Enable syslog tailing for iwd messages") - flag.StringVar(&o.SyslogPath, "syslog-path", "/var/log/messages", "Path to syslog file") - flag.Var(&StringArray{&o.SyslogFilter}, "daemon.info iwd:", "Filter string for syslog lines") - flag.StringVar(&o.SyslogSource, "syslog-source", "iwd", "Source name for syslog entries in logs") -} - -// parseCLI parse the flags and treats extra args as configuration filename. -func parseCLI(o *Config) error { - flag.Parse() - - for _, conf := range flag.Args() { - err := parseFile(o, conf) - if err != nil { - return err - } - } - - return nil -} diff --git a/internal/config/config.go b/internal/config/config.go deleted file mode 100644 index 3cd2fa7..0000000 --- a/internal/config/config.go +++ /dev/null @@ -1,106 +0,0 @@ -package config - -import ( - "flag" - "log" - "os" - "path" - "strings" -) - -type Config struct { - Bind string - WifiInterface string - HotspotInterface string - WifiBackend string - StationBackend string // "arp", "dhcp", or "hostapd" - DHCPLeasesPath string - ARPTablePath string - SyslogEnabled bool - SyslogPath string - SyslogFilter []string - SyslogSource string -} - -// ConsolidateConfig fills an Options struct by reading configuration from -// config files, environment, then command line. -// -// Should be called only one time. -func ConsolidateConfig() (opts *Config, err error) { - // Define defaults options - opts = &Config{ - Bind: ":8080", - WifiInterface: "wlan0", - HotspotInterface: "wlan1", - DHCPLeasesPath: "/var/lib/dhcp/dhcpd.leases", - ARPTablePath: "/proc/net/arp", - SyslogEnabled: false, - SyslogPath: "/var/log/messages", - SyslogFilter: []string{"daemon.info wpa_supplicant:", "daemon.info iwd:", "daemon.info hostapd:"}, - SyslogSource: "iwd", - } - - declareFlags(opts) - - // Establish a list of possible configuration file locations - configLocations := []string{ - "repeater.conf", - } - - if home, err := os.UserConfigDir(); err == nil { - configLocations = append(configLocations, path.Join(home, "repeater", "repeater.conf")) - } - - configLocations = append(configLocations, path.Join("etc", "repeater.conf")) - - // If config file exists, read configuration from it - for _, filename := range configLocations { - if _, e := os.Stat(filename); !os.IsNotExist(e) { - log.Printf("Loading configuration from %s\n", filename) - err = parseFile(opts, filename) - if err != nil { - return - } - break - } - } - - // Then, overwrite that by what is present in the environment - err = parseEnvironmentVariables(opts) - if err != nil { - return - } - - // Finaly, command line takes precedence - err = parseCLI(opts) - if err != nil { - return - } - - // Validate configuration - if opts.WifiBackend != "iwd" && opts.WifiBackend != "wpasupplicant" { - log.Fatalf("wifi-backend must be set to 'iwd' or 'wpasupplicant' (got: '%s')", opts.WifiBackend) - } - - if opts.StationBackend != "arp" && opts.StationBackend != "dhcp" && opts.StationBackend != "hostapd" { - log.Fatalf("station-backend must be set to 'arp', 'dhcp', or 'hostapd' (got: '%s')", opts.StationBackend) - } - - return -} - -// parseLine treats a config line and place the read value in the variable -// declared to the corresponding flag. -func parseLine(o *Config, line string) (err error) { - fields := strings.SplitN(line, "=", 2) - orig_key := strings.TrimSpace(fields[0]) - value := strings.TrimSpace(fields[1]) - - key := strings.TrimPrefix(orig_key, "REPEATER_") - key = strings.Replace(key, "_", "-", -1) - key = strings.ToLower(key) - - err = flag.Set(key, value) - - return -} diff --git a/internal/config/custom.go b/internal/config/custom.go deleted file mode 100644 index f038b7e..0000000 --- a/internal/config/custom.go +++ /dev/null @@ -1,45 +0,0 @@ -package config - -import ( - "fmt" - "net/url" -) - -// StringArray is a custom type for handling multiple string values in flags. -type StringArray struct { - Array *[]string -} - -// String returns a string representation of the StringArray. -func (i *StringArray) String() string { - return fmt.Sprintf("%v", i.Array) -} - -// Set appends a new string value to the StringArray. -func (i *StringArray) Set(value string) error { - *i.Array = append(*i.Array, value) - - return nil -} - -type URL struct { - URL *url.URL -} - -func (i *URL) String() string { - if i.URL != nil { - return i.URL.String() - } else { - return "" - } -} - -func (i *URL) Set(value string) error { - u, err := url.Parse(value) - if err != nil { - return err - } - - *i.URL = *u - return nil -} diff --git a/internal/config/env.go b/internal/config/env.go deleted file mode 100644 index 8888746..0000000 --- a/internal/config/env.go +++ /dev/null @@ -1,21 +0,0 @@ -package config - -import ( - "fmt" - "os" - "strings" -) - -// parseEnvironmentVariables analyzes all the environment variables to find -// each one starting by REPEATER_ -func parseEnvironmentVariables(o *Config) (err error) { - for _, line := range os.Environ() { - if strings.HasPrefix(line, "REPEATER_") { - err := parseLine(o, line) - if err != nil { - return fmt.Errorf("error in environment (%q): %w", line, err) - } - } - } - return -} diff --git a/internal/config/file.go b/internal/config/file.go deleted file mode 100644 index 8ba6a10..0000000 --- a/internal/config/file.go +++ /dev/null @@ -1,33 +0,0 @@ -package config - -import ( - "bufio" - "fmt" - "os" - "strings" -) - -// parseFile opens the file at the given filename path, then treat each line -// not starting with '#' as a configuration statement. -func parseFile(o *Config, filename string) error { - fp, err := os.Open(filename) - if err != nil { - return err - } - defer fp.Close() - - scanner := bufio.NewScanner(fp) - n := 0 - for scanner.Scan() { - n += 1 - line := strings.TrimSpace(scanner.Text()) - if len(line) > 0 && !strings.HasPrefix(line, "#") && strings.Index(line, "=") > 0 { - err := parseLine(o, line) - if err != nil { - return fmt.Errorf("%v:%d: error in configuration: %w", filename, n, err) - } - } - } - - return nil -} diff --git a/internal/hotspot/hotspot.go b/internal/hotspot/hotspot.go deleted file mode 100644 index 793352f..0000000 --- a/internal/hotspot/hotspot.go +++ /dev/null @@ -1,111 +0,0 @@ -package hotspot - -import ( - "fmt" - "os" - "os/exec" - "strconv" - "strings" - - "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("/etc/init.d/hostapd", "start") - return cmd.Run() -} - -// Stop stops the hotspot -func Stop() error { - cmd := exec.Command("/etc/init.d/hostapd", "stop") - return cmd.Run() -} - -// Status checks if the hotspot is running. -// Returns nil if the service is running, or an error if it's stopped or crashed. -func Status() error { - cmd := exec.Command("/etc/init.d/hostapd", "status") - return cmd.Run() -} - -// GetDetailedStatus retrieves detailed status information from hostapd_cli. -// Returns nil if hostapd is not running or if there's an error. -func GetDetailedStatus() *models.HotspotStatus { - cmd := exec.Command("hostapd_cli", "status") - output, err := cmd.Output() - if err != nil { - return nil - } - - status := &models.HotspotStatus{} - lines := strings.Split(string(output), "\n") - - for _, line := range lines { - line = strings.TrimSpace(line) - if line == "" || strings.HasPrefix(line, "Selected interface") { - continue - } - - parts := strings.SplitN(line, "=", 2) - if len(parts) != 2 { - continue - } - - key := strings.TrimSpace(parts[0]) - value := strings.TrimSpace(parts[1]) - - switch key { - case "state": - status.State = value - case "channel": - if ch, err := strconv.Atoi(value); err == nil { - status.Channel = ch - } - case "freq": - if freq, err := strconv.Atoi(value); err == nil { - status.Frequency = freq - } - case "ssid[0]": - status.SSID = value - case "bssid[0]": - status.BSSID = value - case "num_sta[0]": - if num, err := strconv.Atoi(value); err == nil { - status.NumStations = num - } - case "hw_mode": - status.HWMode = value - case "country_code": - status.CountryCode = value - } - } - - return status -} diff --git a/internal/logging/logging.go b/internal/logging/logging.go deleted file mode 100644 index 064a813..0000000 --- a/internal/logging/logging.go +++ /dev/null @@ -1,94 +0,0 @@ -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) - } - } -} diff --git a/internal/models/models.go b/internal/models/models.go deleted file mode 100644 index 64d5c79..0000000 --- a/internal/models/models.go +++ /dev/null @@ -1,71 +0,0 @@ -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"` -} - -// HotspotStatus represents detailed hotspot status -type HotspotStatus struct { - State string `json:"state"` // ENABLED, DISABLED, etc. - SSID string `json:"ssid"` // Current SSID being broadcast - BSSID string `json:"bssid"` // MAC address of the AP - Channel int `json:"channel"` // Current channel - Frequency int `json:"frequency"` // Frequency in MHz - NumStations int `json:"numStations"` // Number of connected stations - HWMode string `json:"hwMode"` // Hardware mode (g, a, n, ac, etc.) - CountryCode string `json:"countryCode"` // Country code -} - -// SystemStatus represents overall system status -type SystemStatus struct { - Connected bool `json:"connected"` - ConnectionState string `json:"connectionState"` // Connection state: connected, disconnected, connecting, disconnecting, roaming - ConnectedSSID string `json:"connectedSSID"` - HotspotStatus *HotspotStatus `json:"hotspotStatus,omitempty"` // Detailed hotspot status - 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 -} diff --git a/internal/station/arp/backend.go b/internal/station/arp/backend.go deleted file mode 100644 index 5b4e975..0000000 --- a/internal/station/arp/backend.go +++ /dev/null @@ -1,177 +0,0 @@ -package arp - -import ( - "sync" - "time" - - "github.com/nemunaire/repeater/internal/station/backend" -) - -// Backend implements StationBackend using ARP table discovery -type Backend struct { - arpTablePath string - lastStations map[string]backend.Station // Key: MAC address - callbacks backend.EventCallbacks - stopChan chan struct{} - mu sync.RWMutex - running bool -} - -// NewBackend creates a new ARP backend -func NewBackend() *Backend { - return &Backend{ - lastStations: make(map[string]backend.Station), - stopChan: make(chan struct{}), - } -} - -// Initialize initializes the ARP backend -func (b *Backend) Initialize(config backend.BackendConfig) error { - b.mu.Lock() - defer b.mu.Unlock() - - b.arpTablePath = config.ARPTablePath - if b.arpTablePath == "" { - b.arpTablePath = "/proc/net/arp" - } - - return nil -} - -// Close cleans up backend resources -func (b *Backend) Close() error { - b.StopEventMonitoring() - return nil -} - -// GetStations returns all connected stations from ARP table -func (b *Backend) GetStations() ([]backend.Station, error) { - b.mu.RLock() - arpTablePath := b.arpTablePath - b.mu.RUnlock() - - arpEntries, err := parseARPTable(arpTablePath) - if err != nil { - return nil, err - } - - var stations []backend.Station - for _, entry := range arpEntries { - // Only include entries with valid flags (2 = COMPLETE, 6 = COMPLETE|PERM) - if entry.Flags == 2 || entry.Flags == 6 { - st := backend.Station{ - MAC: entry.HWAddress.String(), - IP: entry.IP.String(), - Hostname: "", // No hostname available from ARP - Type: backend.GuessDeviceType("", entry.HWAddress.String()), - Signal: 0, // Not available from ARP - RxBytes: 0, // Not available from ARP - TxBytes: 0, // Not available from ARP - ConnectedAt: time.Now(), - } - stations = append(stations, st) - } - } - - return stations, nil -} - -// StartEventMonitoring starts monitoring for station events via polling -func (b *Backend) StartEventMonitoring(callbacks backend.EventCallbacks) error { - b.mu.Lock() - defer b.mu.Unlock() - - if b.running { - return nil - } - - b.callbacks = callbacks - b.running = true - - // Start polling goroutine - go b.pollLoop() - - return nil -} - -// StopEventMonitoring stops event monitoring -func (b *Backend) StopEventMonitoring() { - b.mu.Lock() - if !b.running { - b.mu.Unlock() - return - } - b.running = false - b.mu.Unlock() - - close(b.stopChan) -} - -// SupportsRealTimeEvents returns false (ARP is polling-based) -func (b *Backend) SupportsRealTimeEvents() bool { - return false -} - -// pollLoop polls the ARP table and simulates events -func (b *Backend) pollLoop() { - ticker := time.NewTicker(10 * time.Second) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - b.checkForChanges() - case <-b.stopChan: - return - } - } -} - -// checkForChanges compares current state with last state and triggers callbacks -func (b *Backend) checkForChanges() { - // Get current stations - current, err := b.GetStations() - if err != nil { - return - } - - // Build map of current stations - currentMap := make(map[string]backend.Station) - for _, station := range current { - currentMap[station.MAC] = station - } - - b.mu.Lock() - defer b.mu.Unlock() - - // Check for new stations (connected) - for mac, station := range currentMap { - if _, exists := b.lastStations[mac]; !exists { - // New station connected - if b.callbacks.OnStationConnected != nil { - go b.callbacks.OnStationConnected(station) - } - } else { - // Check for updates (IP change, etc.) - oldStation := b.lastStations[mac] - if oldStation.IP != station.IP || oldStation.Hostname != station.Hostname { - if b.callbacks.OnStationUpdated != nil { - go b.callbacks.OnStationUpdated(station) - } - } - } - } - - // Check for disconnected stations - for mac := range b.lastStations { - if _, exists := currentMap[mac]; !exists { - // Station disconnected - if b.callbacks.OnStationDisconnected != nil { - go b.callbacks.OnStationDisconnected(mac) - } - } - } - - // Update last state - b.lastStations = currentMap -} diff --git a/internal/station/arp/parser.go b/internal/station/arp/parser.go deleted file mode 100644 index ca40f01..0000000 --- a/internal/station/arp/parser.go +++ /dev/null @@ -1,64 +0,0 @@ -package arp - -import ( - "fmt" - "net" - "os" - "strings" -) - -// ARPEntry represents an entry in the ARP table -type ARPEntry struct { - IP net.IP - HWType int - Flags int - HWAddress net.HardwareAddr - Mask string - Device string -} - -// parseARPTable reads and parses ARP table from /proc/net/arp format -func parseARPTable(path string) ([]ARPEntry, error) { - var entries []ARPEntry - - content, err := os.ReadFile(path) - if err != nil { - return entries, err - } - - for _, line := range strings.Split(string(content), "\n") { - fields := strings.Fields(line) - if len(fields) > 5 { - var entry ARPEntry - - // Parse HWType (hex format) - if _, err := fmt.Sscanf(fields[1], "0x%x", &entry.HWType); err != nil { - continue - } - - // Parse Flags (hex format) - if _, err := fmt.Sscanf(fields[2], "0x%x", &entry.Flags); err != nil { - continue - } - - // Parse IP address - entry.IP = net.ParseIP(fields[0]) - if entry.IP == nil { - continue - } - - // Parse MAC address - entry.HWAddress, err = net.ParseMAC(fields[3]) - if err != nil { - continue - } - - entry.Mask = fields[4] - entry.Device = fields[5] - - entries = append(entries, entry) - } - } - - return entries, nil -} diff --git a/internal/station/backend/types.go b/internal/station/backend/types.go deleted file mode 100644 index 9874d5c..0000000 --- a/internal/station/backend/types.go +++ /dev/null @@ -1,99 +0,0 @@ -package backend - -import ( - "strings" - "time" -) - -// StationBackend defines the interface for station/device discovery backends. -// Implementations include ARP-based, DHCP-based, and Hostapd DBus-based discovery. -type StationBackend interface { - // Initialize initializes the backend with the given configuration - Initialize(config BackendConfig) error - - // Close cleans up backend resources - Close() error - - // GetStations returns all currently connected stations - GetStations() ([]Station, error) - - // StartEventMonitoring starts monitoring for station events - // Backends that don't support real-time events will poll and simulate events - StartEventMonitoring(callbacks EventCallbacks) error - - // StopEventMonitoring stops event monitoring - StopEventMonitoring() - - // SupportsRealTimeEvents returns true if backend supports real-time events (e.g., DBus) - // Returns false for polling-based backends (ARP, DHCP) - SupportsRealTimeEvents() bool -} - -// Station represents a connected device in a backend-agnostic format -type Station struct { - MAC string // Hardware MAC address (required, primary identifier) - IP string // IP address (may be empty for some backends initially) - Hostname string // Device hostname (may be empty) - Type string // Device type: "mobile", "laptop", "tablet", "unknown" - Signal int32 // Signal strength in dBm (0 if not available) - RxBytes uint64 // Received bytes (0 if not available) - TxBytes uint64 // Transmitted bytes (0 if not available) - ConnectedAt time.Time // When station connected (best effort) -} - -// EventCallbacks defines callback functions for station events. -// Backends call these when stations connect, disconnect, or update. -type EventCallbacks struct { - // OnStationConnected is called when a new station connects - OnStationConnected func(station Station) - - // OnStationDisconnected is called when a station disconnects - OnStationDisconnected func(mac string) - - // OnStationUpdated is called when station information changes - // (e.g., IP discovered, signal strength changed) - OnStationUpdated func(station Station) -} - -// BackendConfig provides configuration for backend initialization -type BackendConfig struct { - // Common - InterfaceName string // Network interface (e.g., "wlan1") - - // ARP-specific - ARPTablePath string // Path to /proc/net/arp (default: "/proc/net/arp") - - // DHCP-specific - DHCPLeasesPath string // Path to DHCP leases file - - // Hostapd-specific - HostapdInterface string // Hostapd interface name for DBus -} - -// 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" -} diff --git a/internal/station/dhcp/backend.go b/internal/station/dhcp/backend.go deleted file mode 100644 index 54abf95..0000000 --- a/internal/station/dhcp/backend.go +++ /dev/null @@ -1,184 +0,0 @@ -package dhcp - -import ( - "sync" - "time" - - "github.com/nemunaire/repeater/internal/station/backend" -) - -// Backend implements StationBackend using DHCP lease discovery -type Backend struct { - dhcpLeasesPath string - lastStations map[string]backend.Station // Key: MAC address - callbacks backend.EventCallbacks - stopChan chan struct{} - mu sync.RWMutex - running bool -} - -// NewBackend creates a new DHCP backend -func NewBackend() *Backend { - return &Backend{ - lastStations: make(map[string]backend.Station), - stopChan: make(chan struct{}), - } -} - -// Initialize initializes the DHCP backend -func (b *Backend) Initialize(config backend.BackendConfig) error { - b.mu.Lock() - defer b.mu.Unlock() - - b.dhcpLeasesPath = config.DHCPLeasesPath - if b.dhcpLeasesPath == "" { - b.dhcpLeasesPath = "/var/lib/dhcp/dhcpd.leases" - } - - return nil -} - -// Close cleans up backend resources -func (b *Backend) Close() error { - b.StopEventMonitoring() - return nil -} - -// GetStations returns all connected stations from DHCP leases validated by ARP -func (b *Backend) GetStations() ([]backend.Station, error) { - b.mu.RLock() - dhcpLeasesPath := b.dhcpLeasesPath - b.mu.RUnlock() - - // Read DHCP leases - leases, err := parseDHCPLeases(dhcpLeasesPath) - if err != nil { - return nil, err - } - - // Get ARP information for validation - arpInfo, err := getARPInfo() - if err != nil { - return nil, err - } - - var stations []backend.Station - for _, lease := range leases { - // Check if the device is still connected via ARP - if _, exists := arpInfo[lease.IP]; exists { - st := backend.Station{ - MAC: lease.MAC, - IP: lease.IP, - Hostname: lease.Hostname, - Type: backend.GuessDeviceType(lease.Hostname, lease.MAC), - Signal: 0, // Not available from DHCP - RxBytes: 0, // Not available from DHCP - TxBytes: 0, // Not available from DHCP - ConnectedAt: time.Now(), - } - stations = append(stations, st) - } - } - - return stations, nil -} - -// StartEventMonitoring starts monitoring for station events via polling -func (b *Backend) StartEventMonitoring(callbacks backend.EventCallbacks) error { - b.mu.Lock() - defer b.mu.Unlock() - - if b.running { - return nil - } - - b.callbacks = callbacks - b.running = true - - // Start polling goroutine - go b.pollLoop() - - return nil -} - -// StopEventMonitoring stops event monitoring -func (b *Backend) StopEventMonitoring() { - b.mu.Lock() - if !b.running { - b.mu.Unlock() - return - } - b.running = false - b.mu.Unlock() - - close(b.stopChan) -} - -// SupportsRealTimeEvents returns false (DHCP is polling-based) -func (b *Backend) SupportsRealTimeEvents() bool { - return false -} - -// pollLoop polls DHCP leases and simulates events -func (b *Backend) pollLoop() { - ticker := time.NewTicker(10 * time.Second) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - b.checkForChanges() - case <-b.stopChan: - return - } - } -} - -// checkForChanges compares current state with last state and triggers callbacks -func (b *Backend) checkForChanges() { - // Get current stations - current, err := b.GetStations() - if err != nil { - return - } - - // Build map of current stations - currentMap := make(map[string]backend.Station) - for _, st := range current { - currentMap[st.MAC] = st - } - - b.mu.Lock() - defer b.mu.Unlock() - - // Check for new stations (connected) - for mac, st := range currentMap { - if _, exists := b.lastStations[mac]; !exists { - // New station connected - if b.callbacks.OnStationConnected != nil { - go b.callbacks.OnStationConnected(st) - } - } else { - // Check for updates (IP change, hostname change, etc.) - oldStation := b.lastStations[mac] - if oldStation.IP != st.IP || oldStation.Hostname != st.Hostname { - if b.callbacks.OnStationUpdated != nil { - go b.callbacks.OnStationUpdated(st) - } - } - } - } - - // Check for disconnected stations - for mac := range b.lastStations { - if _, exists := currentMap[mac]; !exists { - // Station disconnected - if b.callbacks.OnStationDisconnected != nil { - go b.callbacks.OnStationDisconnected(mac) - } - } - } - - // Update last state - b.lastStations = currentMap -} diff --git a/internal/station/dhcp/parser.go b/internal/station/dhcp/parser.go deleted file mode 100644 index efcd583..0000000 --- a/internal/station/dhcp/parser.go +++ /dev/null @@ -1,72 +0,0 @@ -package dhcp - -import ( - "bufio" - "os" - "os/exec" - "regexp" - "strings" - - "github.com/nemunaire/repeater/internal/models" -) - -// parseDHCPLeases reads and parses DHCP lease file -func parseDHCPLeases(path string) ([]models.DHCPLease, error) { - var leases []models.DHCPLease - - file, err := os.Open(path) - 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 using arp command -// Returns a map of IP -> MAC address -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 -} diff --git a/internal/station/factory.go b/internal/station/factory.go deleted file mode 100644 index 94bfc2b..0000000 --- a/internal/station/factory.go +++ /dev/null @@ -1,24 +0,0 @@ -package station - -import ( - "fmt" - - "github.com/nemunaire/repeater/internal/station/arp" - "github.com/nemunaire/repeater/internal/station/backend" - "github.com/nemunaire/repeater/internal/station/dhcp" - "github.com/nemunaire/repeater/internal/station/hostapd" -) - -// createBackend creates a station backend based on the backend name -func createBackend(backendName string) (backend.StationBackend, error) { - switch backendName { - case "arp": - return arp.NewBackend(), nil - case "dhcp": - return dhcp.NewBackend(), nil - case "hostapd": - return hostapd.NewBackend(), nil - default: - return nil, fmt.Errorf("invalid station backend: %s (must be 'arp', 'dhcp', or 'hostapd')", backendName) - } -} diff --git a/internal/station/hostapd/backend.go b/internal/station/hostapd/backend.go deleted file mode 100644 index ce5708a..0000000 --- a/internal/station/hostapd/backend.go +++ /dev/null @@ -1,345 +0,0 @@ -package hostapd - -import ( - "bufio" - "bytes" - "fmt" - "log" - "os/exec" - "strconv" - "strings" - "sync" - "time" - - "github.com/nemunaire/repeater/internal/station/backend" -) - -// Backend implements StationBackend using hostapd_cli -type Backend struct { - interfaceName string - hostapdCLI string // Path to hostapd_cli executable - - stations map[string]*HostapdStation // Key: MAC address - callbacks backend.EventCallbacks - - mu sync.RWMutex - running bool - stopCh chan struct{} - - // IP correlation - will be populated by periodic DHCP lease correlation - ipByMAC map[string]string // MAC -> IP mapping -} - -// NewBackend creates a new hostapd backend -func NewBackend() *Backend { - return &Backend{ - stations: make(map[string]*HostapdStation), - ipByMAC: make(map[string]string), - hostapdCLI: "hostapd_cli", - stopCh: make(chan struct{}), - } -} - -// Initialize initializes the hostapd backend -func (b *Backend) Initialize(config backend.BackendConfig) error { - b.mu.Lock() - defer b.mu.Unlock() - - b.interfaceName = config.InterfaceName - if b.interfaceName == "" { - b.interfaceName = "wlan1" // Default AP interface - } - - // Check if hostapd_cli is available - if _, err := exec.LookPath(b.hostapdCLI); err != nil { - return fmt.Errorf("hostapd_cli not found in PATH: %w", err) - } - - // Verify we can communicate with hostapd - if err := b.runCommand("ping"); err != nil { - return fmt.Errorf("failed to communicate with hostapd on interface %s: %w", b.interfaceName, err) - } - - log.Printf("Hostapd backend initialized for interface %s", b.interfaceName) - - // Load initial station list - if err := b.loadStations(); err != nil { - log.Printf("Warning: Failed to load initial stations: %v", err) - } - - return nil -} - -// Close cleans up backend resources -func (b *Backend) Close() error { - b.StopEventMonitoring() - return nil -} - -// runCommand executes a hostapd_cli command and returns the output -func (b *Backend) runCommand(args ...string) error { - cmdArgs := []string{"-i", b.interfaceName} - cmdArgs = append(cmdArgs, args...) - cmd := exec.Command(b.hostapdCLI, cmdArgs...) - return cmd.Run() -} - -// runCommandOutput executes a hostapd_cli command and returns the output -func (b *Backend) runCommandOutput(args ...string) (string, error) { - cmdArgs := []string{"-i", b.interfaceName} - cmdArgs = append(cmdArgs, args...) - cmd := exec.Command(b.hostapdCLI, cmdArgs...) - out, err := cmd.Output() - if err != nil { - return "", err - } - return string(out), nil -} - -// GetStations returns all connected stations -func (b *Backend) GetStations() ([]backend.Station, error) { - b.mu.RLock() - defer b.mu.RUnlock() - - stations := make([]backend.Station, 0, len(b.stations)) - for mac, hs := range b.stations { - station := b.convertStation(mac, hs) - stations = append(stations, station) - } - - return stations, nil -} - -// StartEventMonitoring starts monitoring for station events via polling -func (b *Backend) StartEventMonitoring(callbacks backend.EventCallbacks) error { - b.mu.Lock() - defer b.mu.Unlock() - - if b.running { - return nil - } - - b.callbacks = callbacks - b.running = true - - // Start polling goroutine - go b.pollStations() - - log.Printf("Hostapd event monitoring started (polling mode)") - return nil -} - -// StopEventMonitoring stops event monitoring -func (b *Backend) StopEventMonitoring() { - b.mu.Lock() - if !b.running { - b.mu.Unlock() - return - } - b.running = false - b.mu.Unlock() - - close(b.stopCh) -} - -// SupportsRealTimeEvents returns false (hostapd_cli uses polling) -func (b *Backend) SupportsRealTimeEvents() bool { - return false -} - -// pollStations periodically polls for station changes -func (b *Backend) pollStations() { - ticker := time.NewTicker(2 * time.Second) - defer ticker.Stop() - - for { - select { - case <-b.stopCh: - return - case <-ticker.C: - if err := b.checkStationChanges(); err != nil { - log.Printf("Error polling stations: %v", err) - } - } - } -} - -// checkStationChanges checks for station connect/disconnect events -func (b *Backend) checkStationChanges() error { - // Get current stations from hostapd - currentStations, err := b.fetchStations() - if err != nil { - return err - } - - b.mu.Lock() - defer b.mu.Unlock() - - // Build a map of current MACs - currentMACs := make(map[string]bool) - for mac := range currentStations { - currentMACs[mac] = true - } - - // Check for new stations - for mac, station := range currentStations { - if _, exists := b.stations[mac]; !exists { - // New station connected - b.stations[mac] = station - if b.callbacks.OnStationConnected != nil { - st := b.convertStation(mac, station) - go b.callbacks.OnStationConnected(st) - } - log.Printf("Station connected: %s", mac) - } - } - - // Check for removed stations - for mac := range b.stations { - if !currentMACs[mac] { - // Station disconnected - delete(b.stations, mac) - delete(b.ipByMAC, mac) - if b.callbacks.OnStationDisconnected != nil { - go b.callbacks.OnStationDisconnected(mac) - } - log.Printf("Station disconnected: %s", mac) - } - } - - return nil -} - -// loadStations loads the initial list of stations from hostapd -func (b *Backend) loadStations() error { - stations, err := b.fetchStations() - if err != nil { - return err - } - - b.stations = stations - log.Printf("Loaded %d initial stations from hostapd", len(b.stations)) - return nil -} - -// fetchStations fetches all stations using hostapd_cli all_sta command -func (b *Backend) fetchStations() (map[string]*HostapdStation, error) { - output, err := b.runCommandOutput("all_sta") - if err != nil { - return nil, fmt.Errorf("failed to get stations: %w", err) - } - - return b.parseAllStaOutput(output), nil -} - -// parseAllStaOutput parses the output of "hostapd_cli all_sta" -func (b *Backend) parseAllStaOutput(output string) map[string]*HostapdStation { - stations := make(map[string]*HostapdStation) - scanner := bufio.NewScanner(bytes.NewBufferString(output)) - - var currentMAC string - var currentStation *HostapdStation - - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if line == "" { - continue - } - - // Check if this is a MAC address line (starts the station block) - if !strings.Contains(line, "=") && len(line) == 17 && strings.Count(line, ":") == 5 { - // Save previous station if exists - if currentMAC != "" && currentStation != nil { - stations[currentMAC] = currentStation - } - // Start new station - currentMAC = strings.ToLower(line) - currentStation = &HostapdStation{} - continue - } - - // Parse key=value pairs - if currentStation != nil && strings.Contains(line, "=") { - parts := strings.SplitN(line, "=", 2) - if len(parts) != 2 { - continue - } - key := strings.TrimSpace(parts[0]) - value := strings.TrimSpace(parts[1]) - - switch key { - case "signal": - if v, err := strconv.Atoi(value); err == nil { - currentStation.Signal = int32(v) - } - case "rx_bytes": - if v, err := strconv.ParseUint(value, 10, 64); err == nil { - currentStation.RxBytes = v - } - case "tx_bytes": - if v, err := strconv.ParseUint(value, 10, 64); err == nil { - currentStation.TxBytes = v - } - } - } - } - - // Save last station - if currentMAC != "" && currentStation != nil { - stations[currentMAC] = currentStation - } - - return stations -} - -// convertStation converts HostapdStation to backend.Station -func (b *Backend) convertStation(mac string, hs *HostapdStation) backend.Station { - // Get IP address if available from correlation - ip := b.ipByMAC[mac] - - // Attempt hostname resolution if we have an IP - hostname := "" - // TODO: Could do reverse DNS lookup here if needed - - return backend.Station{ - MAC: mac, - IP: ip, - Hostname: hostname, - Type: backend.GuessDeviceType(hostname, mac), - Signal: hs.Signal, - RxBytes: hs.RxBytes, - TxBytes: hs.TxBytes, - ConnectedAt: time.Now(), // We don't have exact connection time - } -} - -// UpdateIPMapping updates the MAC -> IP mapping from external source (e.g., DHCP) -// This should be called periodically to correlate hostapd stations with IP addresses -func (b *Backend) UpdateIPMapping(macToIP map[string]string) { - b.mu.Lock() - defer b.mu.Unlock() - - // Track which stations got IP updates - updated := make(map[string]bool) - - for mac, ip := range macToIP { - if oldIP, exists := b.ipByMAC[mac]; exists && oldIP != ip { - // IP changed - updated[mac] = true - } else if !exists { - // New IP mapping - updated[mac] = true - } - b.ipByMAC[mac] = ip - } - - // Trigger update callbacks for stations that got new/changed IPs - for mac := range updated { - if station, exists := b.stations[mac]; exists { - if b.callbacks.OnStationUpdated != nil { - st := b.convertStation(mac, station) - go b.callbacks.OnStationUpdated(st) - } - } - } -} diff --git a/internal/station/hostapd/correlation.go b/internal/station/hostapd/correlation.go deleted file mode 100644 index fe4361a..0000000 --- a/internal/station/hostapd/correlation.go +++ /dev/null @@ -1,130 +0,0 @@ -package hostapd - -import ( - "bufio" - "log" - "os" - "strings" - "time" - - "github.com/nemunaire/repeater/internal/models" -) - -// DHCPCorrelator helps correlate hostapd stations with DHCP leases to discover IP addresses -type DHCPCorrelator struct { - backend *Backend - dhcpLeasesPath string - stopChan chan struct{} - running bool -} - -// NewDHCPCorrelator creates a new DHCP correlator -func NewDHCPCorrelator(backend *Backend, dhcpLeasesPath string) *DHCPCorrelator { - if dhcpLeasesPath == "" { - dhcpLeasesPath = "/var/lib/dhcp/dhcpd.leases" - } - - return &DHCPCorrelator{ - backend: backend, - dhcpLeasesPath: dhcpLeasesPath, - stopChan: make(chan struct{}), - } -} - -// Start begins periodic correlation of DHCP leases with hostapd stations -func (dc *DHCPCorrelator) Start() { - if dc.running { - return - } - - dc.running = true - go dc.correlationLoop() - log.Printf("DHCP correlation started for hostapd backend") -} - -// Stop stops the correlation loop -func (dc *DHCPCorrelator) Stop() { - if !dc.running { - return - } - - dc.running = false - close(dc.stopChan) - log.Printf("DHCP correlation stopped") -} - -// correlationLoop periodically correlates DHCP leases with stations -func (dc *DHCPCorrelator) correlationLoop() { - // Do an initial correlation immediately - dc.correlate() - - // Then correlate every 10 seconds - ticker := time.NewTicker(10 * time.Second) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - dc.correlate() - case <-dc.stopChan: - return - } - } -} - -// correlate performs one correlation cycle -func (dc *DHCPCorrelator) correlate() { - // Parse DHCP leases - leases, err := parseDHCPLeases(dc.dhcpLeasesPath) - if err != nil { - log.Printf("Warning: Failed to parse DHCP leases: %v", err) - return - } - - // Build MAC -> IP mapping - macToIP := make(map[string]string) - for _, lease := range leases { - macToIP[lease.MAC] = lease.IP - } - - // Update backend with IP mappings - dc.backend.UpdateIPMapping(macToIP) -} - -// parseDHCPLeases reads and parses DHCP lease file -func parseDHCPLeases(path string) ([]models.DHCPLease, error) { - var leases []models.DHCPLease - - file, err := os.Open(path) - 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, scanner.Err() -} diff --git a/internal/station/hostapd/types.go b/internal/station/hostapd/types.go deleted file mode 100644 index 2521c1f..0000000 --- a/internal/station/hostapd/types.go +++ /dev/null @@ -1,10 +0,0 @@ -package hostapd - -// HostapdStation represents station properties from hostapd_cli -type HostapdStation struct { - RxPackets uint64 - TxPackets uint64 - RxBytes uint64 - TxBytes uint64 - Signal int32 // Signal in dBm -} diff --git a/internal/station/station.go b/internal/station/station.go deleted file mode 100644 index e6acaf1..0000000 --- a/internal/station/station.go +++ /dev/null @@ -1,111 +0,0 @@ -package station - -import ( - "sync" - - "github.com/nemunaire/repeater/internal/models" - "github.com/nemunaire/repeater/internal/station/backend" -) - -var ( - currentBackend backend.StationBackend - mu sync.RWMutex -) - -// Initialize initializes the station discovery backend -func Initialize(backendName string, config backend.BackendConfig) error { - mu.Lock() - defer mu.Unlock() - - // Close existing backend if any - if currentBackend != nil { - currentBackend.Close() - } - - // Create new backend - b, err := createBackend(backendName) - if err != nil { - return err - } - - // Initialize the backend - if err := b.Initialize(config); err != nil { - return err - } - - currentBackend = b - return nil -} - -// GetStations returns all connected stations as ConnectedDevice models -func GetStations() ([]models.ConnectedDevice, error) { - mu.RLock() - defer mu.RUnlock() - - if currentBackend == nil { - return nil, nil - } - - stations, err := currentBackend.GetStations() - if err != nil { - return nil, err - } - - // Convert backend.Station to models.ConnectedDevice - devices := make([]models.ConnectedDevice, len(stations)) - for i, s := range stations { - devices[i] = models.ConnectedDevice{ - Name: s.Hostname, - Type: s.Type, - MAC: s.MAC, - IP: s.IP, - } - } - - return devices, nil -} - -// StartEventMonitoring starts monitoring for station events -func StartEventMonitoring(callbacks backend.EventCallbacks) error { - mu.RLock() - defer mu.RUnlock() - - if currentBackend == nil { - return nil - } - - return currentBackend.StartEventMonitoring(callbacks) -} - -// StopEventMonitoring stops monitoring for station events -func StopEventMonitoring() { - mu.RLock() - defer mu.RUnlock() - - if currentBackend != nil { - currentBackend.StopEventMonitoring() - } -} - -// Close closes the current backend -func Close() { - mu.Lock() - defer mu.Unlock() - - if currentBackend != nil { - currentBackend.Close() - currentBackend = nil - } -} - -// SupportsRealTimeEvents returns true if the current backend supports real-time events -func SupportsRealTimeEvents() bool { - mu.RLock() - defer mu.RUnlock() - - if currentBackend == nil { - return false - } - - return currentBackend.SupportsRealTimeEvents() -} diff --git a/internal/syslog/parser.go b/internal/syslog/parser.go deleted file mode 100644 index 10c07b6..0000000 --- a/internal/syslog/parser.go +++ /dev/null @@ -1,32 +0,0 @@ -package syslog - -import ( - "strings" -) - -// ParseSyslogLine extracts the message content from a syslog line. -// It looks for the daemon prefix in the line and returns the message after it. -// -// Example input: "Dec 2 02:01:33 tyet daemon.info iwd: Error loading /var/lib/iwd//nemuphone.psk" -// Example output: "Error loading /var/lib/iwd//nemuphone.psk", true -// -// Returns the message and a boolean indicating if the line was successfully parsed. -func ParseSyslogLine(line, daemonPrefix string) (string, bool) { - // Find the daemon prefix in the line (e.g., "iwd:") - idx := strings.Index(line, daemonPrefix) - if idx == -1 { - return "", false - } - - // Extract everything after the daemon prefix - message := line[idx+len(daemonPrefix):] - - // Trim leading/trailing whitespace - message = strings.TrimSpace(message) - - if message == "" { - return "", false - } - - return message, true -} diff --git a/internal/syslog/syslog.go b/internal/syslog/syslog.go deleted file mode 100644 index 605a1fe..0000000 --- a/internal/syslog/syslog.go +++ /dev/null @@ -1,225 +0,0 @@ -package syslog - -import ( - "bufio" - "io" - "log" - "os" - "strings" - "sync" - "time" - - "github.com/nemunaire/repeater/internal/logging" -) - -// SyslogTailer tails a syslog file and filters messages to the logging system. -type SyslogTailer struct { - path string - filters []string - source string - - file *os.File - done chan struct{} - wg sync.WaitGroup - mu sync.Mutex - running bool -} - -// NewSyslogTailer creates a new syslog tailer. -// path: Path to the syslog file (e.g., "/var/log/messages") -// filter: Filter string to match in lines (e.g., "daemon.info iwd:") -// source: Source name for logging (e.g., "iwd") -func NewSyslogTailer(path string, filters []string, source string) *SyslogTailer { - return &SyslogTailer{ - path: path, - filters: filters, - source: source, - done: make(chan struct{}), - } -} - -// Start opens the syslog file and begins tailing it. -func (t *SyslogTailer) Start() error { - t.mu.Lock() - defer t.mu.Unlock() - - if t.running { - return nil - } - - // Try to open the file - file, err := os.Open(t.path) - if err != nil { - // File might not exist yet, we'll retry in the goroutine - log.Printf("Warning: Cannot open syslog file %s: %v (will retry)", t.path, err) - } else { - // Seek to the end to only read new entries - _, err = file.Seek(0, io.SeekEnd) - if err != nil { - file.Close() - return err - } - t.file = file - } - - t.running = true - t.wg.Add(1) - go t.tail() - - return nil -} - -// Stop signals the tailer to stop and waits for it to finish. -func (t *SyslogTailer) Stop() { - t.mu.Lock() - if !t.running { - t.mu.Unlock() - return - } - t.mu.Unlock() - - close(t.done) - t.wg.Wait() - - t.mu.Lock() - if t.file != nil { - t.file.Close() - t.file = nil - } - t.running = false - t.mu.Unlock() -} - -// tail is the main loop that reads from the syslog file. -func (t *SyslogTailer) tail() { - defer t.wg.Done() - - retryDelay := 1 * time.Second - maxRetryDelay := 30 * time.Second - - for { - select { - case <-t.done: - return - default: - } - - // Check if we have a file open - t.mu.Lock() - file := t.file - t.mu.Unlock() - - if file == nil { - // Try to open the file - newFile, err := os.Open(t.path) - if err != nil { - // File doesn't exist or can't be opened, wait and retry - select { - case <-t.done: - return - case <-time.After(retryDelay): - // Exponential backoff - retryDelay *= 2 - if retryDelay > maxRetryDelay { - retryDelay = maxRetryDelay - } - continue - } - } - - // Seek to the end - _, err = newFile.Seek(0, io.SeekEnd) - if err != nil { - log.Printf("Error seeking syslog file: %v", err) - newFile.Close() - time.Sleep(retryDelay) - continue - } - - t.mu.Lock() - t.file = newFile - file = newFile - t.mu.Unlock() - - retryDelay = 1 * time.Second - log.Printf("Syslog tailer: opened %s", t.path) - } - - // Read lines from the file - if err := t.readLines(file); err != nil { - if err == io.EOF { - // End of file, wait a bit and try again - time.Sleep(100 * time.Millisecond) - continue - } - - // Other error, close the file and retry - log.Printf("Error reading syslog file: %v", err) - t.mu.Lock() - if t.file != nil { - t.file.Close() - t.file = nil - } - t.mu.Unlock() - - time.Sleep(retryDelay) - } - } -} - -// readLines reads and processes lines from the file. -func (t *SyslogTailer) readLines(file *os.File) error { - scanner := bufio.NewScanner(file) - - // Increase buffer size to handle long log lines - const maxCapacity = 512 * 1024 - buf := make([]byte, maxCapacity) - scanner.Buffer(buf, maxCapacity) - - for scanner.Scan() { - select { - case <-t.done: - return nil - default: - } - - line := scanner.Text() - - // Check if the line contains any of the filter strings - var matchedFilter string - for _, filter := range t.filters { - if strings.Contains(line, filter) { - matchedFilter = filter - break - } - } - if matchedFilter == "" { - continue - } - - // Parse the syslog line to extract the message - // We look for "iwd:" (or whatever comes after the filter) - // The filter is "daemon.info iwd:" so we want to extract text after "iwd:" - daemonPrefix := extractDaemonPrefix(matchedFilter) - message, ok := ParseSyslogLine(line, daemonPrefix) - if !ok { - // Couldn't parse the line, skip it - continue - } - - // Add to logging system - logging.AddLog(t.source, message) - } - - return scanner.Err() -} - -// extractDaemonPrefix extracts the daemon prefix from the filter string. -// For example, "daemon.info iwd:" returns "iwd:" -func extractDaemonPrefix(filter string) string { - parts := strings.Fields(filter) - if len(parts) > 0 { - return parts[len(parts)-1] - } - return filter -} diff --git a/internal/wifi/backend/types.go b/internal/wifi/backend/types.go deleted file mode 100644 index 1346e9b..0000000 --- a/internal/wifi/backend/types.go +++ /dev/null @@ -1,56 +0,0 @@ -package backend - -// WiFiBackend is the interface that must be implemented by all WiFi backends (iwd, wpa_supplicant, etc.) -type WiFiBackend interface { - // Lifecycle Management - Initialize(interfaceName string) error - Close() error - - // Network Discovery - ScanNetworks() error - GetOrderedNetworks() ([]BackendNetwork, error) - IsScanning() (bool, error) - - // Connection Management - Connect(ssid, password string) error - Disconnect() error - GetConnectionState() (ConnectionState, error) - GetConnectedSSID() string - - // Event Monitoring - StartEventMonitoring(callbacks EventCallbacks) error - StopEventMonitoring() -} - -// BackendNetwork represents a WiFi network in a backend-agnostic format. -// Both iwd and wpa_supplicant backends convert their native representations to this type. -type BackendNetwork struct { - SSID string - SignalDBm int16 // Signal strength in dBm (-100 to 0) - SecurityType string // "open", "wep", "psk", "8021x" - BSSID string // MAC address of the access point - Frequency uint32 // Frequency in MHz (e.g., 2412 for channel 1, 5180 for channel 36) -} - -// ConnectionState represents the WiFi connection state in a backend-agnostic way. -type ConnectionState string - -const ( - StateConnected ConnectionState = "connected" - StateDisconnected ConnectionState = "disconnected" - StateConnecting ConnectionState = "connecting" - StateDisconnecting ConnectionState = "disconnecting" -) - -// EventCallbacks defines callback functions that backends use to notify the wifi package of events. -// This allows the wifi package to remain backend-agnostic while still receiving real-time updates. -type EventCallbacks struct { - // OnStateChange is called when the connection state changes - OnStateChange func(state ConnectionState, ssid string) - - // OnScanComplete is called when a network scan completes - OnScanComplete func() - - // OnSignalUpdate is called when signal strength changes for the connected network - OnSignalUpdate func(ssid string, signalDBm int16) -} diff --git a/internal/wifi/broadcaster.go b/internal/wifi/broadcaster.go deleted file mode 100644 index 931ad3e..0000000 --- a/internal/wifi/broadcaster.go +++ /dev/null @@ -1,140 +0,0 @@ -package wifi - -import ( - "log" - "sync" - - "github.com/gorilla/websocket" - "github.com/nemunaire/repeater/internal/models" -) - -// WifiBroadcaster manages WebSocket clients and broadcasts WiFi events -type WifiBroadcaster struct { - clients map[*websocket.Conn]bool - clientsMu sync.RWMutex - - // State deduplication - lastState string - lastSSID string - lastNetworks []models.WiFiNetwork - stateMu sync.RWMutex -} - -// NewWifiBroadcaster creates a new WiFi broadcaster -func NewWifiBroadcaster() *WifiBroadcaster { - return &WifiBroadcaster{ - clients: make(map[*websocket.Conn]bool), - } -} - -// RegisterClient registers a new WebSocket client -func (wb *WifiBroadcaster) RegisterClient(conn *websocket.Conn) { - wb.clientsMu.Lock() - wb.clients[conn] = true - wb.clientsMu.Unlock() - - // Send initial state to the new client - wb.sendInitialState(conn) -} - -// UnregisterClient removes a WebSocket client -func (wb *WifiBroadcaster) UnregisterClient(conn *websocket.Conn) { - wb.clientsMu.Lock() - delete(wb.clients, conn) - wb.clientsMu.Unlock() -} - -// sendInitialState sends the current WiFi state to a newly connected client -func (wb *WifiBroadcaster) sendInitialState(conn *websocket.Conn) { - wb.stateMu.RLock() - lastState := wb.lastState - lastSSID := wb.lastSSID - lastNetworks := make([]models.WiFiNetwork, len(wb.lastNetworks)) - copy(lastNetworks, wb.lastNetworks) - wb.stateMu.RUnlock() - - // Send last known state if available - if lastState != "" { - event := NewStateChangeEvent(lastState, lastSSID, "") - conn.WriteJSON(event) - } - - // Send last known network list if available - if len(lastNetworks) > 0 { - event := NewScanUpdateEvent(lastNetworks) - conn.WriteJSON(event) - } -} - -// BroadcastScanUpdate broadcasts a scan update event to all clients -func (wb *WifiBroadcaster) BroadcastScanUpdate(networks []models.WiFiNetwork) { - // Check for changes to avoid duplicate broadcasts - wb.stateMu.Lock() - if networksEqual(wb.lastNetworks, networks) { - wb.stateMu.Unlock() - return - } - wb.lastNetworks = make([]models.WiFiNetwork, len(networks)) - copy(wb.lastNetworks, networks) - wb.stateMu.Unlock() - - event := NewScanUpdateEvent(networks) - wb.broadcast(event) -} - -// BroadcastStateChange broadcasts a state change event to all clients -func (wb *WifiBroadcaster) BroadcastStateChange(state, ssid string) { - // Check for changes to avoid duplicate broadcasts - wb.stateMu.Lock() - if wb.lastState == state && wb.lastSSID == ssid { - wb.stateMu.Unlock() - return - } - previousState := wb.lastState - wb.lastState = state - wb.lastSSID = ssid - wb.stateMu.Unlock() - - event := NewStateChangeEvent(state, ssid, previousState) - wb.broadcast(event) -} - -// BroadcastSignalUpdate broadcasts a signal update event to all clients -func (wb *WifiBroadcaster) BroadcastSignalUpdate(ssid string, signal, dbm int) { - event := NewSignalUpdateEvent(ssid, signal, dbm) - wb.broadcast(event) -} - -// broadcast sends an event to all connected clients -func (wb *WifiBroadcaster) broadcast(event WifiEvent) { - // Get list of clients with read lock - wb.clientsMu.RLock() - clients := make([]*websocket.Conn, 0, len(wb.clients)) - for client := range wb.clients { - clients = append(clients, client) - } - wb.clientsMu.RUnlock() - - // Broadcast to all clients - for _, client := range clients { - err := client.WriteJSON(event) - if err != nil { - log.Printf("Erreur lors de l'envoi WebSocket WiFi: %v", err) - client.Close() - wb.UnregisterClient(client) - } - } -} - -// networksEqual compares two network slices for equality -func networksEqual(a, b []models.WiFiNetwork) bool { - if len(a) != len(b) { - return false - } - for i := range a { - if a[i].SSID != b[i].SSID || a[i].Signal != b[i].Signal { - return false - } - } - return true -} diff --git a/internal/wifi/events.go b/internal/wifi/events.go deleted file mode 100644 index c8f3672..0000000 --- a/internal/wifi/events.go +++ /dev/null @@ -1,70 +0,0 @@ -package wifi - -import ( - "time" - - "github.com/nemunaire/repeater/internal/models" -) - -// WifiEvent represents a WiFi event to be sent over WebSocket -type WifiEvent struct { - Type string `json:"type"` - Timestamp time.Time `json:"timestamp"` - Data interface{} `json:"data"` -} - -// ScanUpdateData contains network list update information -type ScanUpdateData struct { - Networks []models.WiFiNetwork `json:"networks"` -} - -// StateChangeData contains connection state change information -type StateChangeData struct { - State string `json:"state"` - SSID string `json:"ssid,omitempty"` - PreviousState string `json:"previous_state,omitempty"` -} - -// SignalUpdateData contains signal strength update information -type SignalUpdateData struct { - SSID string `json:"ssid"` - Signal int `json:"signal"` // 1-5 scale - DBm int `json:"dbm"` // Raw dBm value -} - -// NewScanUpdateEvent creates a new scan update event -func NewScanUpdateEvent(networks []models.WiFiNetwork) WifiEvent { - return WifiEvent{ - Type: "scan_update", - Timestamp: time.Now(), - Data: ScanUpdateData{ - Networks: networks, - }, - } -} - -// NewStateChangeEvent creates a new state change event -func NewStateChangeEvent(state, ssid, previousState string) WifiEvent { - return WifiEvent{ - Type: "state_change", - Timestamp: time.Now(), - Data: StateChangeData{ - State: state, - SSID: ssid, - PreviousState: previousState, - }, - } -} - -// NewSignalUpdateEvent creates a new signal update event -func NewSignalUpdateEvent(ssid string, signal, dbm int) WifiEvent { - return WifiEvent{ - Type: "signal_update", - Timestamp: time.Now(), - Data: SignalUpdateData{ - SSID: ssid, - Signal: signal, - DBm: dbm, - }, - } -} diff --git a/internal/wifi/factory.go b/internal/wifi/factory.go deleted file mode 100644 index fdb63cc..0000000 --- a/internal/wifi/factory.go +++ /dev/null @@ -1,21 +0,0 @@ -package wifi - -import ( - "fmt" - - "github.com/nemunaire/repeater/internal/wifi/backend" - "github.com/nemunaire/repeater/internal/wifi/iwd" - "github.com/nemunaire/repeater/internal/wifi/wpasupplicant" -) - -// createBackend creates the appropriate WiFi backend based on the backend name -func createBackend(backendName string) (backend.WiFiBackend, error) { - switch backendName { - case "iwd": - return iwd.NewIWDBackend(), nil - case "wpasupplicant": - return wpasupplicant.NewWPABackend(), nil - default: - return nil, fmt.Errorf("invalid wifi backend: %s (must be 'iwd' or 'wpasupplicant')", backendName) - } -} diff --git a/internal/wifi/iwd/agent.go b/internal/wifi/iwd/agent.go deleted file mode 100644 index 3a53ba6..0000000 --- a/internal/wifi/iwd/agent.go +++ /dev/null @@ -1,170 +0,0 @@ -package iwd - -import ( - "fmt" - "sync" - - "github.com/godbus/dbus/v5" - "github.com/godbus/dbus/v5/introspect" -) - -const agentIntrospectXML = ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -` - -// Agent implements the net.connman.iwd.Agent interface for credential callbacks -type Agent struct { - conn *dbus.Conn - path dbus.ObjectPath - passphraseStore map[string]string - mu sync.RWMutex -} - -// NewAgent creates a new Agent instance -func NewAgent(conn *dbus.Conn, path dbus.ObjectPath) *Agent { - return &Agent{ - conn: conn, - path: path, - passphraseStore: make(map[string]string), - } -} - -// SetPassphrase stores a passphrase for a given network SSID -func (a *Agent) SetPassphrase(ssid, passphrase string) { - a.mu.Lock() - defer a.mu.Unlock() - a.passphraseStore[ssid] = passphrase -} - -// ClearPassphrase removes the stored passphrase for a network -func (a *Agent) ClearPassphrase(ssid string) { - a.mu.Lock() - defer a.mu.Unlock() - delete(a.passphraseStore, ssid) -} - -// Export registers the agent object on D-Bus -func (a *Agent) Export() error { - err := a.conn.Export(a, a.path, AgentInterface) - if err != nil { - return fmt.Errorf("failed to export agent: %v", err) - } - - err = a.conn.Export(introspect.Introspectable(agentIntrospectXML), a.path, "org.freedesktop.DBus.Introspectable") - if err != nil { - a.conn.Export(nil, a.path, AgentInterface) - return fmt.Errorf("failed to export introspection: %v", err) - } - - return nil -} - -// Unexport unregisters the agent from D-Bus -func (a *Agent) Unexport() { - a.conn.Export(nil, a.path, AgentInterface) - a.conn.Export(nil, a.path, "org.freedesktop.DBus.Introspectable") -} - -// getNetworkSSID queries the network object to get its SSID (Name property) -func (a *Agent) getNetworkSSID(networkPath dbus.ObjectPath) (string, error) { - obj := a.conn.Object(Service, networkPath) - variant, err := obj.GetProperty(NetworkInterface + ".Name") - if err != nil { - return "", fmt.Errorf("failed to get network name: %v", err) - } - - name, ok := variant.Value().(string) - if !ok { - return "", fmt.Errorf("network name is not a string") - } - - return name, nil -} - -// RequestPassphrase is called by iwd when connecting to PSK networks -func (a *Agent) RequestPassphrase(network dbus.ObjectPath) (string, *dbus.Error) { - fmt.Printf("[Agent] RequestPassphrase called for network: %s\n", network) - - ssid, err := a.getNetworkSSID(network) - if err != nil { - fmt.Printf("[Agent] Failed to get SSID: %v\n", err) - return "", dbus.MakeFailedError(fmt.Errorf("failed to get network SSID: %v", err)) - } - - fmt.Printf("[Agent] Network SSID: %s\n", ssid) - - a.mu.RLock() - passphrase, ok := a.passphraseStore[ssid] - a.mu.RUnlock() - - if !ok { - fmt.Printf("[Agent] No passphrase stored for SSID: %s\n", ssid) - return "", dbus.MakeFailedError(fmt.Errorf("no passphrase stored for network '%s'", ssid)) - } - - fmt.Printf("[Agent] Returning passphrase for SSID: %s\n", ssid) - return passphrase, nil -} - -// RequestPrivateKeyPassphrase is called for encrypted private keys -func (a *Agent) RequestPrivateKeyPassphrase(network dbus.ObjectPath) (string, *dbus.Error) { - // Not implemented for now - return "", dbus.MakeFailedError(fmt.Errorf("RequestPrivateKeyPassphrase not implemented")) -} - -// RequestUserNameAndPassword is called for enterprise networks -func (a *Agent) RequestUserNameAndPassword(network dbus.ObjectPath) (string, string, *dbus.Error) { - // Not implemented for now - return "", "", dbus.MakeFailedError(fmt.Errorf("RequestUserNameAndPassword not implemented")) -} - -// RequestUserPassword is called for enterprise networks with known username -func (a *Agent) RequestUserPassword(network dbus.ObjectPath, user string) (string, *dbus.Error) { - // Not implemented for now - return "", dbus.MakeFailedError(fmt.Errorf("RequestUserPassword not implemented")) -} - -// Cancel is called when a request is canceled -func (a *Agent) Cancel(reason string) *dbus.Error { - // Nothing to do, just acknowledge - return nil -} - -// Release is called when the agent is unregistered -func (a *Agent) Release() *dbus.Error { - // Cleanup if needed - a.mu.Lock() - a.passphraseStore = make(map[string]string) - a.mu.Unlock() - return nil -} diff --git a/internal/wifi/iwd/agentmanager.go b/internal/wifi/iwd/agentmanager.go deleted file mode 100644 index 7ea6c77..0000000 --- a/internal/wifi/iwd/agentmanager.go +++ /dev/null @@ -1,39 +0,0 @@ -package iwd - -import ( - "fmt" - - "github.com/godbus/dbus/v5" -) - -// AgentManager handles agent registration with iwd -type AgentManager struct { - conn *dbus.Conn - obj dbus.BusObject -} - -// NewAgentManager creates a new AgentManager instance -func NewAgentManager(conn *dbus.Conn) *AgentManager { - return &AgentManager{ - conn: conn, - obj: conn.Object(Service, "/net/connman/iwd"), - } -} - -// RegisterAgent registers an agent with iwd -func (am *AgentManager) RegisterAgent(agentPath dbus.ObjectPath) error { - err := am.obj.Call(AgentManagerInterface+".RegisterAgent", 0, agentPath).Err - if err != nil { - return fmt.Errorf("failed to register agent: %v", err) - } - return nil -} - -// UnregisterAgent unregisters an agent from iwd -func (am *AgentManager) UnregisterAgent(agentPath dbus.ObjectPath) error { - err := am.obj.Call(AgentManagerInterface+".UnregisterAgent", 0, agentPath).Err - if err != nil { - return fmt.Errorf("failed to unregister agent: %v", err) - } - return nil -} diff --git a/internal/wifi/iwd/backend.go b/internal/wifi/iwd/backend.go deleted file mode 100644 index 615a280..0000000 --- a/internal/wifi/iwd/backend.go +++ /dev/null @@ -1,255 +0,0 @@ -package iwd - -import ( - "fmt" - - "github.com/godbus/dbus/v5" - "github.com/nemunaire/repeater/internal/wifi/backend" -) - -const ( - AgentPath = "/com/github/nemunaire/repeater/agent" -) - -// IWDBackend implements the WiFiBackend interface for iwd (Intel Wireless Daemon) -type IWDBackend struct { - conn *dbus.Conn - manager *Manager - station *Station - agent *Agent - agentManager *AgentManager - signalMonitor *SignalMonitor - interfaceName string - callbacks backend.EventCallbacks -} - -// NewIWDBackend creates a new IWD backend instance -func NewIWDBackend() *IWDBackend { - return &IWDBackend{} -} - -// Initialize initializes the iwd backend with the given interface name -func (b *IWDBackend) Initialize(interfaceName string) error { - b.interfaceName = interfaceName - var err error - - // Connect to D-Bus - b.conn, err = dbus.SystemBus() - if err != nil { - return fmt.Errorf("échec de connexion à D-Bus: %v", err) - } - - // Find station for interface - b.manager = NewManager(b.conn) - b.station, err = b.manager.FindStation(interfaceName) - if err != nil { - return fmt.Errorf("impossible de trouver la station pour %s: %v", interfaceName, err) - } - - // Create and register agent for credential callbacks - b.agent = NewAgent(b.conn, dbus.ObjectPath(AgentPath)) - if err := b.agent.Export(); err != nil { - return fmt.Errorf("échec de l'export de l'agent: %v", err) - } - - b.agentManager = NewAgentManager(b.conn) - if err := b.agentManager.RegisterAgent(dbus.ObjectPath(AgentPath)); err != nil { - b.agent.Unexport() - return fmt.Errorf("échec de l'enregistrement de l'agent: %v", err) - } - - return nil -} - -// Close closes the D-Bus connection and unregisters the agent -func (b *IWDBackend) Close() error { - if b.agentManager != nil && b.agent != nil { - b.agentManager.UnregisterAgent(dbus.ObjectPath(AgentPath)) - b.agent.Unexport() - } - if b.conn != nil { - b.conn.Close() - } - return nil -} - -// ScanNetworks triggers a network scan -func (b *IWDBackend) ScanNetworks() error { - err := b.station.Scan() - if err != nil { - return fmt.Errorf("erreur lors du scan: %v", err) - } - return nil -} - -// GetOrderedNetworks returns networks sorted by signal strength in backend-agnostic format -func (b *IWDBackend) GetOrderedNetworks() ([]backend.BackendNetwork, error) { - networkInfos, err := b.station.GetOrderedNetworks() - if err != nil { - return nil, fmt.Errorf("erreur lors de la récupération des réseaux: %v", err) - } - - var networks []backend.BackendNetwork - seenSSIDs := make(map[string]bool) - - for _, netInfo := range networkInfos { - network := NewNetwork(b.conn, netInfo.Path) - props, err := network.GetProperties() - if err != nil { - continue - } - - if props.Name == "" || seenSSIDs[props.Name] { - continue - } - seenSSIDs[props.Name] = true - - // Convert iwd network to backend-agnostic format - backendNet := backend.BackendNetwork{ - SSID: props.Name, - SignalDBm: netInfo.Signal / 100, // iwd provides 100*dBm, convert to dBm - SecurityType: props.Type, - BSSID: generateSyntheticBSSID(props.Name), // iwd doesn't expose BSSID - Frequency: 0, // iwd doesn't expose frequency in GetOrderedNetworks - } - - networks = append(networks, backendNet) - } - - return networks, nil -} - -// IsScanning checks if a scan is currently in progress -func (b *IWDBackend) IsScanning() (bool, error) { - return b.station.IsScanning() -} - -// Connect connects to a WiFi network -func (b *IWDBackend) Connect(ssid, password string) error { - // Store passphrase in agent for callback - if password != "" { - b.agent.SetPassphrase(ssid, password) - } - - // Ensure passphrase is cleared after connection attempt - defer func() { - if password != "" { - b.agent.ClearPassphrase(ssid) - } - }() - - // Get network object - network, err := b.station.GetNetwork(ssid) - if err != nil { - return fmt.Errorf("réseau '%s' non trouvé: %v", ssid, err) - } - - // Connect - iwd will call agent.RequestPassphrase() if needed - if err := network.Connect(); err != nil { - return fmt.Errorf("erreur lors de la connexion: %v", err) - } - - return nil -} - -// Disconnect disconnects from the current WiFi network -func (b *IWDBackend) Disconnect() error { - if err := b.station.Disconnect(); err != nil { - return fmt.Errorf("erreur lors de la déconnexion: %v", err) - } - return nil -} - -// GetConnectionState returns the current WiFi connection state -func (b *IWDBackend) GetConnectionState() (backend.ConnectionState, error) { - state, err := b.station.GetState() - if err != nil { - return backend.StateDisconnected, err - } - return mapIWDState(state), nil -} - -// GetConnectedSSID returns the SSID of the currently connected network -func (b *IWDBackend) GetConnectedSSID() string { - network, err := b.station.GetConnectedNetwork() - if err != nil { - return "" - } - - props, err := network.GetProperties() - if err != nil { - return "" - } - - return props.Name -} - -// StartEventMonitoring starts monitoring WiFi events -func (b *IWDBackend) StartEventMonitoring(callbacks backend.EventCallbacks) error { - b.callbacks = callbacks - - // Create signal monitor - b.signalMonitor = NewSignalMonitor(b.conn, b.station) - - // Register callbacks - wrap to convert iwd types to backend types - b.signalMonitor.OnStateChange(func(state StationState, ssid string) { - if b.callbacks.OnStateChange != nil { - b.callbacks.OnStateChange(mapIWDState(state), ssid) - } - }) - - b.signalMonitor.OnScanComplete(func() { - if b.callbacks.OnScanComplete != nil { - b.callbacks.OnScanComplete() - } - }) - - // Start monitoring - return b.signalMonitor.Start() -} - -// StopEventMonitoring stops monitoring WiFi events -func (b *IWDBackend) StopEventMonitoring() { - if b.signalMonitor != nil { - b.signalMonitor.Stop() - } -} - -// mapIWDState maps iwd-specific states to backend-agnostic states -func mapIWDState(state StationState) backend.ConnectionState { - switch state { - case StateConnected: - return backend.StateConnected - case StateConnecting: - return backend.StateConnecting - case StateDisconnecting: - return backend.StateDisconnecting - case StateDisconnected: - return backend.StateDisconnected - case StateRoaming: - // Map roaming to connected since we're still connected during roaming - return backend.StateConnected - default: - return backend.StateDisconnected - } -} - -// generateSyntheticBSSID generates a consistent fake BSSID from SSID -// (iwd doesn't expose real BSSID) -func generateSyntheticBSSID(ssid string) string { - // Use a simple hash approach - consistent per SSID - hash := 0 - for _, c := range ssid { - hash = ((hash << 5) - hash) + int(c) - } - - // Generate 6 bytes for MAC address - b1 := byte((hash >> 0) & 0xff) - b2 := byte((hash >> 8) & 0xff) - b3 := byte((hash >> 16) & 0xff) - b4 := byte((hash >> 24) & 0xff) - b5 := byte(len(ssid) & 0xff) - b6 := byte((len(ssid) >> 8) & 0xff) - - return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", b1, b2, b3, b4, b5, b6) -} diff --git a/internal/wifi/iwd/manager.go b/internal/wifi/iwd/manager.go deleted file mode 100644 index f636664..0000000 --- a/internal/wifi/iwd/manager.go +++ /dev/null @@ -1,71 +0,0 @@ -package iwd - -import ( - "fmt" - "strings" - - "github.com/godbus/dbus/v5" -) - -// Manager handles iwd object discovery via ObjectManager -type Manager struct { - conn *dbus.Conn - obj dbus.BusObject -} - -// NewManager creates a new Manager instance -func NewManager(conn *dbus.Conn) *Manager { - return &Manager{ - conn: conn, - obj: conn.Object(Service, dbus.ObjectPath(ManagerPath)), - } -} - -// GetManagedObjects returns all iwd managed objects -func (m *Manager) GetManagedObjects() (map[dbus.ObjectPath]map[string]map[string]dbus.Variant, error) { - var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant - err := m.obj.Call("org.freedesktop.DBus.ObjectManager.GetManagedObjects", 0).Store(&objects) - if err != nil { - return nil, fmt.Errorf("failed to get managed objects: %v", err) - } - return objects, nil -} - -// FindStation finds the Station object for the given interface name -func (m *Manager) FindStation(interfaceName string) (*Station, error) { - objects, err := m.GetManagedObjects() - if err != nil { - return nil, err - } - - // First, find the device with matching interface name - var devicePath dbus.ObjectPath - for path, interfaces := range objects { - if deviceProps, ok := interfaces[DeviceInterface]; ok { - if nameVariant, ok := deviceProps["Name"]; ok { - if name, ok := nameVariant.Value().(string); ok && name == interfaceName { - devicePath = path - break - } - } - } - } - - if devicePath == "" { - return nil, fmt.Errorf("device with interface '%s' not found", interfaceName) - } - - // Now find the station object under this device - // Station path is typically the same as device path or a child of it - for path, interfaces := range objects { - if _, ok := interfaces[StationInterface]; ok { - // Check if this station belongs to our device - // Station path should be the device path or start with it - if path == devicePath || strings.HasPrefix(string(path), string(devicePath)+"/") { - return NewStation(m.conn, path), nil - } - } - } - - return nil, fmt.Errorf("station for device '%s' not found", interfaceName) -} diff --git a/internal/wifi/iwd/network.go b/internal/wifi/iwd/network.go deleted file mode 100644 index 1563a95..0000000 --- a/internal/wifi/iwd/network.go +++ /dev/null @@ -1,64 +0,0 @@ -package iwd - -import ( - "fmt" - - "github.com/godbus/dbus/v5" -) - -// Network represents an iwd Network interface -type Network struct { - path dbus.ObjectPath - conn *dbus.Conn - obj dbus.BusObject -} - -// NewNetwork creates a new Network instance -func NewNetwork(conn *dbus.Conn, path dbus.ObjectPath) *Network { - return &Network{ - path: path, - conn: conn, - obj: conn.Object(Service, path), - } -} - -// GetProperties retrieves all network properties -func (n *Network) GetProperties() (*NetworkProperties, error) { - var props map[string]dbus.Variant - err := n.obj.Call("org.freedesktop.DBus.Properties.GetAll", 0, NetworkInterface).Store(&props) - if err != nil { - return nil, fmt.Errorf("failed to get network properties: %v", err) - } - - netProps := &NetworkProperties{} - - if nameVariant, ok := props["Name"]; ok { - if name, ok := nameVariant.Value().(string); ok { - netProps.Name = name - } - } - - if typeVariant, ok := props["Type"]; ok { - if netType, ok := typeVariant.Value().(string); ok { - netProps.Type = netType - } - } - - if connectedVariant, ok := props["Connected"]; ok { - if connected, ok := connectedVariant.Value().(bool); ok { - netProps.Connected = connected - } - } - - return netProps, nil -} - -// Connect initiates a connection to this network -// Credentials are provided via the registered agent's RequestPassphrase callback -func (n *Network) Connect() error { - err := n.obj.Call(NetworkInterface+".Connect", 0).Err - if err != nil { - return fmt.Errorf("connect failed: %v", err) - } - return nil -} diff --git a/internal/wifi/iwd/signals.go b/internal/wifi/iwd/signals.go deleted file mode 100644 index 90ed32e..0000000 --- a/internal/wifi/iwd/signals.go +++ /dev/null @@ -1,223 +0,0 @@ -package iwd - -import ( - "log" - "sync" - - "github.com/godbus/dbus/v5" -) - -// SignalMonitor monitors D-Bus signals from iwd -type SignalMonitor struct { - conn *dbus.Conn - station *Station - - // Signal channel - signalChan chan *dbus.Signal - - // Callbacks - onStateChange func(state StationState, ssid string) - onScanComplete func() - - // Control - stopChan chan struct{} - mu sync.RWMutex - running bool - - // State tracking - lastScanning bool -} - -// NewSignalMonitor creates a new signal monitor -func NewSignalMonitor(conn *dbus.Conn, station *Station) *SignalMonitor { - return &SignalMonitor{ - conn: conn, - station: station, - signalChan: make(chan *dbus.Signal, 100), - stopChan: make(chan struct{}), - } -} - -// OnStateChange registers a callback for state changes -func (sm *SignalMonitor) OnStateChange(callback func(state StationState, ssid string)) { - sm.mu.Lock() - defer sm.mu.Unlock() - sm.onStateChange = callback -} - -// OnScanComplete registers a callback for scan completion -func (sm *SignalMonitor) OnScanComplete(callback func()) { - sm.mu.Lock() - defer sm.mu.Unlock() - sm.onScanComplete = callback -} - -// Start begins monitoring D-Bus signals -func (sm *SignalMonitor) Start() error { - sm.mu.Lock() - if sm.running { - sm.mu.Unlock() - return nil - } - sm.running = true - sm.mu.Unlock() - - // Subscribe to PropertiesChanged signals for Station interface - stationPath := sm.station.GetPath() - - // Add signal match for PropertiesChanged on Station interface - matchOptions := []dbus.MatchOption{ - dbus.WithMatchObjectPath(stationPath), - dbus.WithMatchInterface("org.freedesktop.DBus.Properties"), - dbus.WithMatchMember("PropertiesChanged"), - } - - if err := sm.conn.AddMatchSignal(matchOptions...); err != nil { - sm.mu.Lock() - sm.running = false - sm.mu.Unlock() - return err - } - - // Register signal channel - sm.conn.Signal(sm.signalChan) - - // Get initial scanning state - scanning, err := sm.station.IsScanning() - if err == nil { - sm.lastScanning = scanning - } - - // Start monitoring goroutine - go sm.monitor() - - log.Printf("D-Bus signal monitoring started for station %s", stationPath) - return nil -} - -// Stop stops monitoring D-Bus signals -func (sm *SignalMonitor) Stop() { - sm.mu.Lock() - if !sm.running { - sm.mu.Unlock() - return - } - sm.running = false - sm.mu.Unlock() - - // Signal stop - close(sm.stopChan) - - // Remove signal channel - sm.conn.RemoveSignal(sm.signalChan) - - log.Printf("D-Bus signal monitoring stopped") -} - -// monitor is the main signal processing loop -func (sm *SignalMonitor) monitor() { - for { - select { - case sig := <-sm.signalChan: - sm.handleSignal(sig) - case <-sm.stopChan: - return - } - } -} - -// handleSignal processes a D-Bus signal -func (sm *SignalMonitor) handleSignal(sig *dbus.Signal) { - // Only process PropertiesChanged signals - if sig.Name != "org.freedesktop.DBus.Properties.PropertiesChanged" { - return - } - - // Verify signal is from Station interface - if len(sig.Body) < 2 { - return - } - - interfaceName, ok := sig.Body[0].(string) - if !ok || interfaceName != StationInterface { - return - } - - // Parse changed properties - changedProps, ok := sig.Body[1].(map[string]dbus.Variant) - if !ok { - return - } - - // Check for State property change - if stateVariant, ok := changedProps["State"]; ok { - if state, ok := stateVariant.Value().(string); ok { - sm.handleStateChange(StationState(state)) - } - } - - // Check for Scanning property change - if scanningVariant, ok := changedProps["Scanning"]; ok { - if scanning, ok := scanningVariant.Value().(bool); ok { - sm.handleScanningChange(scanning) - } - } - - // Check for ConnectedNetwork property change - if _, ok := changedProps["ConnectedNetwork"]; ok { - // Network connection changed, trigger state update - sm.handleConnectionChange() - } -} - -// handleStateChange processes a state change -func (sm *SignalMonitor) handleStateChange(state StationState) { - sm.mu.RLock() - callback := sm.onStateChange - sm.mu.RUnlock() - - if callback == nil { - return - } - - // Get connected SSID if connected - ssid := "" - if state == StateConnected { - network, err := sm.station.GetConnectedNetwork() - if err == nil { - props, err := network.GetProperties() - if err == nil { - ssid = props.Name - } - } - } - - callback(state, ssid) -} - -// handleScanningChange processes scanning state changes -func (sm *SignalMonitor) handleScanningChange(scanning bool) { - // Detect scan completion (transition from true to false) - if sm.lastScanning && !scanning { - sm.mu.RLock() - callback := sm.onScanComplete - sm.mu.RUnlock() - - if callback != nil { - callback() - } - } - - sm.lastScanning = scanning -} - -// handleConnectionChange processes connection changes -func (sm *SignalMonitor) handleConnectionChange() { - // Get current state and trigger state change callback - state, err := sm.station.GetState() - if err != nil { - return - } - - sm.handleStateChange(state) -} diff --git a/internal/wifi/iwd/station.go b/internal/wifi/iwd/station.go deleted file mode 100644 index 54014fd..0000000 --- a/internal/wifi/iwd/station.go +++ /dev/null @@ -1,142 +0,0 @@ -package iwd - -import ( - "fmt" - - "github.com/godbus/dbus/v5" -) - -// Station represents an iwd Station interface -type Station struct { - path dbus.ObjectPath - conn *dbus.Conn - obj dbus.BusObject -} - -// NewStation creates a new Station instance -func NewStation(conn *dbus.Conn, path dbus.ObjectPath) *Station { - return &Station{ - path: path, - conn: conn, - obj: conn.Object(Service, path), - } -} - -// Scan triggers a network scan -func (s *Station) Scan() error { - err := s.obj.Call(StationInterface+".Scan", 0).Err - if err != nil { - return fmt.Errorf("scan failed: %v", err) - } - return nil -} - -// IsScanning checks if a scan is currently in progress -func (s *Station) IsScanning() (bool, error) { - prop, err := s.obj.GetProperty(StationInterface + ".Scanning") - if err != nil { - return false, fmt.Errorf("failed to get Scanning property: %v", err) - } - - scanning, ok := prop.Value().(bool) - if !ok { - return false, fmt.Errorf("Scanning property is not a boolean") - } - - return scanning, nil -} - -// GetOrderedNetworks returns networks sorted by signal strength -func (s *Station) GetOrderedNetworks() ([]NetworkInfo, error) { - var result []struct { - Path dbus.ObjectPath - Signal int16 - } - - err := s.obj.Call(StationInterface+".GetOrderedNetworks", 0).Store(&result) - if err != nil { - return nil, fmt.Errorf("failed to get ordered networks: %v", err) - } - - networks := make([]NetworkInfo, len(result)) - for i, r := range result { - networks[i] = NetworkInfo{ - Path: r.Path, - Signal: r.Signal, - } - } - - return networks, nil -} - -// GetState returns the current connection state -func (s *Station) GetState() (StationState, error) { - prop, err := s.obj.GetProperty(StationInterface + ".State") - if err != nil { - return "", fmt.Errorf("failed to get State property: %v", err) - } - - state, ok := prop.Value().(string) - if !ok { - return "", fmt.Errorf("State property is not a string") - } - - return StationState(state), nil -} - -// Disconnect disconnects from the current network -func (s *Station) Disconnect() error { - err := s.obj.Call(StationInterface+".Disconnect", 0).Err - if err != nil { - return fmt.Errorf("disconnect failed: %v", err) - } - return nil -} - -// GetNetwork finds and returns a Network object by SSID -func (s *Station) GetNetwork(ssid string) (*Network, error) { - networks, err := s.GetOrderedNetworks() - if err != nil { - return nil, err - } - - // Find the network with matching SSID - for _, netInfo := range networks { - network := NewNetwork(s.conn, netInfo.Path) - props, err := network.GetProperties() - if err != nil { - continue - } - - if props.Name == ssid { - return network, nil - } - } - - return nil, fmt.Errorf("network '%s' not found", ssid) -} - -// GetConnectedNetwork returns the currently connected network -func (s *Station) GetConnectedNetwork() (*Network, error) { - prop, err := s.obj.GetProperty(StationInterface + ".ConnectedNetwork") - if err != nil { - return nil, fmt.Errorf("failed to get ConnectedNetwork property: %v", err) - } - - path, ok := prop.Value().(dbus.ObjectPath) - if !ok { - return nil, fmt.Errorf("ConnectedNetwork property is not an ObjectPath") - } - - // Check if path is empty (not connected) - if path == "/" || path == "" { - return nil, fmt.Errorf("not connected to any network") - } - - return NewNetwork(s.conn, path), nil -} - -// GetPath returns the D-Bus object path for this station -func (s *Station) GetPath() dbus.ObjectPath { - return s.path -} diff --git a/internal/wifi/iwd/types.go b/internal/wifi/iwd/types.go deleted file mode 100644 index 76624c4..0000000 --- a/internal/wifi/iwd/types.go +++ /dev/null @@ -1,38 +0,0 @@ -package iwd - -import "github.com/godbus/dbus/v5" - -const ( - // D-Bus service and interfaces - Service = "net.connman.iwd" - ManagerPath = "/" - DeviceInterface = "net.connman.iwd.Device" - StationInterface = "net.connman.iwd.Station" - NetworkInterface = "net.connman.iwd.Network" - AgentInterface = "net.connman.iwd.Agent" - AgentManagerInterface = "net.connman.iwd.AgentManager" -) - -// NetworkInfo represents a network with its signal strength -type NetworkInfo struct { - Path dbus.ObjectPath - Signal int16 // 100 * dBm (0 to -10000) -} - -// NetworkProperties holds network properties -type NetworkProperties struct { - Name string // SSID - Type string // "open", "wep", "psk", "8021x" - Connected bool -} - -// StationState represents the connection state -type StationState string - -const ( - StateConnected StationState = "connected" - StateDisconnected StationState = "disconnected" - StateConnecting StationState = "connecting" - StateDisconnecting StationState = "disconnecting" - StateRoaming StationState = "roaming" -) diff --git a/internal/wifi/wifi.go b/internal/wifi/wifi.go deleted file mode 100644 index dac04c3..0000000 --- a/internal/wifi/wifi.go +++ /dev/null @@ -1,242 +0,0 @@ -package wifi - -import ( - "fmt" - "sort" - "strings" - "time" - - "github.com/gorilla/websocket" - "github.com/nemunaire/repeater/internal/models" - "github.com/nemunaire/repeater/internal/wifi/backend" -) - -var ( - wifiBackend backend.WiFiBackend - wifiBroadcaster *WifiBroadcaster -) - -// Initialize initializes the WiFi service with the specified backend -func Initialize(interfaceName string, backendName string) error { - // Create the appropriate backend using the factory - var err error - wifiBackend, err = createBackend(backendName) - if err != nil { - return err - } - - // Initialize the backend - return wifiBackend.Initialize(interfaceName) -} - -// Close closes the backend connection -func Close() { - if wifiBackend != nil { - wifiBackend.Close() - } -} - -// GetCachedNetworks returns previously discovered networks without triggering a scan -func GetCachedNetworks() ([]models.WiFiNetwork, error) { - // Get ordered networks from backend - backendNetworks, err := wifiBackend.GetOrderedNetworks() - if err != nil { - return nil, fmt.Errorf("erreur lors de la récupération des réseaux: %v", err) - } - - // Convert backend networks to models - networks := make([]models.WiFiNetwork, 0, len(backendNetworks)) - for _, backendNet := range backendNetworks { - wifiNet := models.WiFiNetwork{ - SSID: backendNet.SSID, - Signal: signalToStrength(int(backendNet.SignalDBm)), - Security: mapSecurityType(backendNet.SecurityType), - BSSID: backendNet.BSSID, - Channel: 0, // Not yet exposed by backends - } - networks = append(networks, wifiNet) - } - - // Sort by signal strength (descending) - sort.Slice(networks, func(i, j int) bool { - return networks[i].Signal > networks[j].Signal - }) - - return networks, nil -} - -// ScanNetworks scans for available WiFi networks -func ScanNetworks() ([]models.WiFiNetwork, error) { - // Check if already scanning - scanning, err := wifiBackend.IsScanning() - if err == nil && scanning { - time.Sleep(3 * time.Second) - } else { - // Trigger scan - err := wifiBackend.ScanNetworks() - if err != nil && !strings.Contains(err.Error(), "rejected") { - return nil, fmt.Errorf("erreur lors du scan: %v", err) - } - time.Sleep(2 * time.Second) - } - - // Get ordered networks from backend - backendNetworks, err := wifiBackend.GetOrderedNetworks() - if err != nil { - return nil, fmt.Errorf("erreur lors de la récupération des réseaux: %v", err) - } - - // Convert backend networks to models - networks := make([]models.WiFiNetwork, 0, len(backendNetworks)) - for _, backendNet := range backendNetworks { - wifiNet := models.WiFiNetwork{ - SSID: backendNet.SSID, - Signal: signalToStrength(int(backendNet.SignalDBm)), - Security: mapSecurityType(backendNet.SecurityType), - BSSID: backendNet.BSSID, - Channel: 0, // Not yet exposed by backends - } - networks = append(networks, wifiNet) - } - - // Sort by signal strength (descending) - sort.Slice(networks, func(i, j int) bool { - return networks[i].Signal > networks[j].Signal - }) - - // Broadcast to WebSocket clients if available - if wifiBroadcaster != nil { - wifiBroadcaster.BroadcastScanUpdate(networks) - } - - return networks, nil -} - -// Connect connects to a WiFi network -func Connect(ssid, password string) error { - // Use backend to connect - if err := wifiBackend.Connect(ssid, password); err != nil { - return err - } - - // Poll 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 { - return wifiBackend.Disconnect() -} - -// IsConnected checks if WiFi is connected -func IsConnected() bool { - state, err := wifiBackend.GetConnectionState() - if err != nil { - return false - } - return state == backend.StateConnected -} - -// GetConnectedSSID returns the SSID of the currently connected network -func GetConnectedSSID() string { - return wifiBackend.GetConnectedSSID() -} - -// GetConnectionState returns the current WiFi connection state -func GetConnectionState() string { - state, err := wifiBackend.GetConnectionState() - if err != nil { - return string(backend.StateDisconnected) - } - return string(state) -} - -// StartEventMonitoring initializes signal monitoring and WebSocket broadcasting -func StartEventMonitoring() error { - // Initialize broadcaster - wifiBroadcaster = NewWifiBroadcaster() - - // Set up callbacks - callbacks := backend.EventCallbacks{ - OnStateChange: handleStateChange, - OnScanComplete: handleScanComplete, - } - - // Start backend monitoring - return wifiBackend.StartEventMonitoring(callbacks) -} - -// StopEventMonitoring stops signal monitoring -func StopEventMonitoring() { - if wifiBackend != nil { - wifiBackend.StopEventMonitoring() - } -} - -// RegisterWebSocketClient registers a new WebSocket client for WiFi events -func RegisterWebSocketClient(conn *websocket.Conn) { - if wifiBroadcaster != nil { - wifiBroadcaster.RegisterClient(conn) - } -} - -// UnregisterWebSocketClient removes a WebSocket client -func UnregisterWebSocketClient(conn *websocket.Conn) { - if wifiBroadcaster != nil { - wifiBroadcaster.UnregisterClient(conn) - } -} - -// handleStateChange is called when WiFi connection state changes -func handleStateChange(newState backend.ConnectionState, connectedSSID string) { - if wifiBroadcaster != nil { - wifiBroadcaster.BroadcastStateChange(string(newState), connectedSSID) - } -} - -// handleScanComplete is called when a WiFi scan completes -func handleScanComplete() { - // Get updated network list - networks, err := GetCachedNetworks() - if err == nil && wifiBroadcaster != nil { - wifiBroadcaster.BroadcastScanUpdate(networks) - } -} - -// mapSecurityType maps backend security types to display format -func mapSecurityType(securityType string) string { - switch securityType { - case "open": - return "Open" - case "wep": - return "WEP" - case "psk": - return "WPA2" - case "8021x": - return "WPA2" - default: - return "WPA2" - } -} - -// 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 - } -} diff --git a/internal/wifi/wpasupplicant/backend.go b/internal/wifi/wpasupplicant/backend.go deleted file mode 100644 index 406b8f5..0000000 --- a/internal/wifi/wpasupplicant/backend.go +++ /dev/null @@ -1,265 +0,0 @@ -package wpasupplicant - -import ( - "fmt" - "time" - - "github.com/godbus/dbus/v5" - "github.com/nemunaire/repeater/internal/wifi/backend" -) - -// WPABackend implements the WiFiBackend interface for wpa_supplicant -type WPABackend struct { - conn *dbus.Conn - wpasupplicant dbus.BusObject - iface *WPAInterface - signalMonitor *SignalMonitor - interfaceName string - currentNetwork dbus.ObjectPath -} - -// NewWPABackend creates a new wpa_supplicant backend instance -func NewWPABackend() *WPABackend { - return &WPABackend{} -} - -// Initialize initializes the wpa_supplicant backend with the given interface name -func (b *WPABackend) Initialize(interfaceName string) error { - b.interfaceName = interfaceName - var err error - - // Connect to D-Bus - b.conn, err = dbus.SystemBus() - if err != nil { - return fmt.Errorf("failed to connect to D-Bus: %v", err) - } - - // Get wpa_supplicant root object - b.wpasupplicant = b.conn.Object(Service, dbus.ObjectPath(RootPath)) - - // Get interface path for the given interface name - interfacePath, err := b.getInterfacePath(interfaceName) - if err != nil { - return fmt.Errorf("failed to get interface for %s: %v", interfaceName, err) - } - - b.iface = NewWPAInterface(b.conn, interfacePath) - - return nil -} - -// getInterfacePath gets or creates the wpa_supplicant Interface object path -func (b *WPABackend) getInterfacePath(interfaceName string) (dbus.ObjectPath, error) { - var interfacePath dbus.ObjectPath - - // Try to get existing interface - err := b.wpasupplicant.Call(Service+".GetInterface", 0, interfaceName).Store(&interfacePath) - if err == nil { - return interfacePath, nil - } - - // Interface doesn't exist, create it - args := map[string]dbus.Variant{ - "Ifname": dbus.MakeVariant(interfaceName), - } - - err = b.wpasupplicant.Call(Service+".CreateInterface", 0, args).Store(&interfacePath) - if err != nil { - return "", fmt.Errorf("failed to create interface: %v", err) - } - - return interfacePath, nil -} - -// Close closes the D-Bus connection -func (b *WPABackend) Close() error { - if b.conn != nil { - b.conn.Close() - } - return nil -} - -// ScanNetworks triggers a network scan -func (b *WPABackend) ScanNetworks() error { - err := b.iface.Scan("active") - if err != nil { - return fmt.Errorf("failed to trigger scan: %v", err) - } - return nil -} - -// GetOrderedNetworks returns networks sorted by signal strength in backend-agnostic format -func (b *WPABackend) GetOrderedNetworks() ([]backend.BackendNetwork, error) { - // Get BSS list - bssPaths, err := b.iface.GetBSSs() - if err != nil { - return nil, fmt.Errorf("failed to get BSSs: %v", err) - } - - var networks []backend.BackendNetwork - seenSSIDs := make(map[string]bool) - - // Iterate through BSSs and collect network info - for _, bssPath := range bssPaths { - bss := NewBSS(b.conn, bssPath) - props, err := bss.GetProperties() - if err != nil { - continue - } - - ssid := string(props.SSID) - if ssid == "" || seenSSIDs[ssid] { - continue - } - seenSSIDs[ssid] = true - - // Get BSSID string - bssidStr, err := bss.GetBSSIDString() - if err != nil { - bssidStr = "" - } - - // Convert to backend-agnostic format - backendNet := backend.BackendNetwork{ - SSID: ssid, - SignalDBm: props.Signal, - SecurityType: props.DetermineSecurityType(), - BSSID: bssidStr, - Frequency: props.Frequency, - } - - networks = append(networks, backendNet) - } - - // Sort by signal strength (descending) - // Note: This is a simple bubble sort for demonstration - // In production, use sort.Slice - for i := 0; i < len(networks); i++ { - for j := i + 1; j < len(networks); j++ { - if networks[j].SignalDBm > networks[i].SignalDBm { - networks[i], networks[j] = networks[j], networks[i] - } - } - } - - return networks, nil -} - -// IsScanning checks if a scan is currently in progress -func (b *WPABackend) IsScanning() (bool, error) { - return b.iface.GetScanning() -} - -// Connect connects to a WiFi network -func (b *WPABackend) Connect(ssid, password string) error { - // Create network configuration - config := make(map[string]interface{}) - config["ssid"] = fmt.Sprintf("\"%s\"", ssid) // wpa_supplicant expects quoted SSID - - if password != "" { - // For WPA/WPA2-PSK networks - config["psk"] = fmt.Sprintf("\"%s\"", password) - } else { - // For open networks - config["key_mgmt"] = "NONE" - } - - // Add network - networkPath, err := b.iface.AddNetwork(config) - if err != nil { - return fmt.Errorf("failed to add network: %v", err) - } - - // Store current network path for cleanup - b.currentNetwork = networkPath - - // Select (connect to) the network - err = b.iface.SelectNetwork(networkPath) - if err != nil { - // Clean up network on failure - b.iface.RemoveNetwork(networkPath) - return fmt.Errorf("failed to select network: %v", err) - } - - return nil -} - -// Disconnect disconnects from the current WiFi network -func (b *WPABackend) Disconnect() error { - // Disconnect from current network - if err := b.iface.Disconnect(); err != nil { - return fmt.Errorf("failed to disconnect: %v", err) - } - - // Remove the network configuration if we have one - if b.currentNetwork != "" && b.currentNetwork != "/" { - b.iface.RemoveNetwork(b.currentNetwork) - b.currentNetwork = "" - } - - return nil -} - -// GetConnectionState returns the current WiFi connection state -func (b *WPABackend) GetConnectionState() (backend.ConnectionState, error) { - state, err := b.iface.GetState() - if err != nil { - return backend.StateDisconnected, err - } - return mapWPAState(state), nil -} - -// GetConnectedSSID returns the SSID of the currently connected network -func (b *WPABackend) GetConnectedSSID() string { - // Get current BSS - bssPath, err := b.iface.GetCurrentBSS() - if err != nil || bssPath == "/" { - return "" - } - - // Get BSS object - bss := NewBSS(b.conn, bssPath) - ssid, err := bss.GetSSIDString() - if err != nil { - return "" - } - - return ssid -} - -// StartEventMonitoring starts monitoring WiFi events -func (b *WPABackend) StartEventMonitoring(callbacks backend.EventCallbacks) error { - // Create signal monitor - b.signalMonitor = NewSignalMonitor(b.conn, b.iface) - - // Start monitoring - return b.signalMonitor.Start(callbacks) -} - -// StopEventMonitoring stops monitoring WiFi events -func (b *WPABackend) StopEventMonitoring() { - if b.signalMonitor != nil { - b.signalMonitor.Stop() - } -} - -// Wait for scan to complete (helper method) -func (b *WPABackend) waitForScanComplete(timeout time.Duration) error { - start := time.Now() - for { - if time.Since(start) > timeout { - return fmt.Errorf("scan timeout") - } - - scanning, err := b.iface.GetScanning() - if err != nil { - return err - } - - if !scanning { - return nil - } - - time.Sleep(100 * time.Millisecond) - } -} diff --git a/internal/wifi/wpasupplicant/bss.go b/internal/wifi/wpasupplicant/bss.go deleted file mode 100644 index 799e9f9..0000000 --- a/internal/wifi/wpasupplicant/bss.go +++ /dev/null @@ -1,157 +0,0 @@ -package wpasupplicant - -import ( - "fmt" - - "github.com/godbus/dbus/v5" -) - -// BSS represents a wpa_supplicant BSS (Basic Service Set) object -type BSS struct { - path dbus.ObjectPath - conn *dbus.Conn - obj dbus.BusObject -} - -// BSSProperties holds the properties of a BSS -type BSSProperties struct { - SSID []byte - BSSID []byte - Signal int16 // Signal strength in dBm - Frequency uint32 // Frequency in MHz - Privacy bool // Whether encryption is enabled - RSN map[string]dbus.Variant - WPA map[string]dbus.Variant -} - -// NewBSS creates a new BSS instance -func NewBSS(conn *dbus.Conn, path dbus.ObjectPath) *BSS { - return &BSS{ - path: path, - conn: conn, - obj: conn.Object(Service, path), - } -} - -// GetProperties returns all properties of the BSS -func (b *BSS) GetProperties() (*BSSProperties, error) { - props := &BSSProperties{} - - // Get SSID - if ssidProp, err := b.obj.GetProperty(BSSInterface + ".SSID"); err == nil { - if ssid, ok := ssidProp.Value().([]byte); ok { - props.SSID = ssid - } - } - - // Get BSSID - if bssidProp, err := b.obj.GetProperty(BSSInterface + ".BSSID"); err == nil { - if bssid, ok := bssidProp.Value().([]byte); ok { - props.BSSID = bssid - } - } - - // Get Signal - if signalProp, err := b.obj.GetProperty(BSSInterface + ".Signal"); err == nil { - if signal, ok := signalProp.Value().(int16); ok { - props.Signal = signal - } - } - - // Get Frequency - if freqProp, err := b.obj.GetProperty(BSSInterface + ".Frequency"); err == nil { - if freq, ok := freqProp.Value().(uint16); ok { - props.Frequency = uint32(freq) - } - } - - // Get Privacy - if privacyProp, err := b.obj.GetProperty(BSSInterface + ".Privacy"); err == nil { - if privacy, ok := privacyProp.Value().(bool); ok { - props.Privacy = privacy - } - } - - // Get RSN (WPA2) information - if rsnProp, err := b.obj.GetProperty(BSSInterface + ".RSN"); err == nil { - if rsn, ok := rsnProp.Value().(map[string]dbus.Variant); ok { - props.RSN = rsn - } - } - - // Get WPA information - if wpaProp, err := b.obj.GetProperty(BSSInterface + ".WPA"); err == nil { - if wpa, ok := wpaProp.Value().(map[string]dbus.Variant); ok { - props.WPA = wpa - } - } - - return props, nil -} - -// GetSSIDString returns the SSID as a string -func (b *BSS) GetSSIDString() (string, error) { - prop, err := b.obj.GetProperty(BSSInterface + ".SSID") - if err != nil { - return "", fmt.Errorf("failed to get SSID property: %v", err) - } - - ssid, ok := prop.Value().([]byte) - if !ok { - return "", fmt.Errorf("SSID property is not a byte array") - } - - return string(ssid), nil -} - -// GetBSSIDString returns the BSSID as a formatted MAC address string -func (b *BSS) GetBSSIDString() (string, error) { - prop, err := b.obj.GetProperty(BSSInterface + ".BSSID") - if err != nil { - return "", fmt.Errorf("failed to get BSSID property: %v", err) - } - - bssid, ok := prop.Value().([]byte) - if !ok || len(bssid) != 6 { - return "", fmt.Errorf("BSSID property is not a valid MAC address") - } - - return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", - bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]), nil -} - -// GetSignal returns the signal strength in dBm -func (b *BSS) GetSignal() (int16, error) { - prop, err := b.obj.GetProperty(BSSInterface + ".Signal") - if err != nil { - return 0, fmt.Errorf("failed to get Signal property: %v", err) - } - - signal, ok := prop.Value().(int16) - if !ok { - return 0, fmt.Errorf("Signal property is not an int16") - } - - return signal, nil -} - -// DetermineSecurityType determines the security type based on BSS properties -func (p *BSSProperties) DetermineSecurityType() string { - // Check for WPA2 (RSN) - if len(p.RSN) > 0 { - return "psk" - } - - // Check for WPA - if len(p.WPA) > 0 { - return "psk" - } - - // Check for WEP (privacy but no WPA/RSN) - if p.Privacy { - return "wep" - } - - // Open network - return "open" -} diff --git a/internal/wifi/wpasupplicant/interface.go b/internal/wifi/wpasupplicant/interface.go deleted file mode 100644 index ecc9fbb..0000000 --- a/internal/wifi/wpasupplicant/interface.go +++ /dev/null @@ -1,146 +0,0 @@ -package wpasupplicant - -import ( - "fmt" - - "github.com/godbus/dbus/v5" -) - -// WPAInterface represents a wpa_supplicant Interface object -type WPAInterface struct { - path dbus.ObjectPath - conn *dbus.Conn - obj dbus.BusObject -} - -// NewWPAInterface creates a new WPAInterface instance -func NewWPAInterface(conn *dbus.Conn, path dbus.ObjectPath) *WPAInterface { - return &WPAInterface{ - path: path, - conn: conn, - obj: conn.Object(Service, path), - } -} - -// Scan triggers a network scan -func (i *WPAInterface) Scan(scanType string) error { - args := map[string]interface{}{ - "Type": scanType, // "active" or "passive" - } - - err := i.obj.Call(InterfaceInterface+".Scan", 0, args).Err - if err != nil { - return fmt.Errorf("scan failed: %v", err) - } - return nil -} - -// GetBSSs returns a list of BSS (Basic Service Set) object paths -func (i *WPAInterface) GetBSSs() ([]dbus.ObjectPath, error) { - prop, err := i.obj.GetProperty(InterfaceInterface + ".BSSs") - if err != nil { - return nil, fmt.Errorf("failed to get BSSs property: %v", err) - } - - bsss, ok := prop.Value().([]dbus.ObjectPath) - if !ok { - return nil, fmt.Errorf("BSSs property is not an array of ObjectPath") - } - - return bsss, nil -} - -// GetState returns the current connection state -func (i *WPAInterface) GetState() (WPAState, error) { - prop, err := i.obj.GetProperty(InterfaceInterface + ".State") - if err != nil { - return "", fmt.Errorf("failed to get State property: %v", err) - } - - state, ok := prop.Value().(string) - if !ok { - return "", fmt.Errorf("State property is not a string") - } - - return WPAState(state), nil -} - -// GetCurrentBSS returns the currently connected BSS object path -func (i *WPAInterface) GetCurrentBSS() (dbus.ObjectPath, error) { - prop, err := i.obj.GetProperty(InterfaceInterface + ".CurrentBSS") - if err != nil { - return "", fmt.Errorf("failed to get CurrentBSS property: %v", err) - } - - bss, ok := prop.Value().(dbus.ObjectPath) - if !ok { - return "", fmt.Errorf("CurrentBSS property is not an ObjectPath") - } - - return bss, nil -} - -// AddNetwork creates a new network configuration -func (i *WPAInterface) AddNetwork(config map[string]interface{}) (dbus.ObjectPath, error) { - var networkPath dbus.ObjectPath - - // Convert config to proper DBus variant format - dbusConfig := make(map[string]dbus.Variant) - for key, value := range config { - dbusConfig[key] = dbus.MakeVariant(value) - } - - err := i.obj.Call(InterfaceInterface+".AddNetwork", 0, dbusConfig).Store(&networkPath) - if err != nil { - return "", fmt.Errorf("failed to add network: %v", err) - } - - return networkPath, nil -} - -// SelectNetwork connects to a network -func (i *WPAInterface) SelectNetwork(networkPath dbus.ObjectPath) error { - err := i.obj.Call(InterfaceInterface+".SelectNetwork", 0, networkPath).Err - if err != nil { - return fmt.Errorf("failed to select network: %v", err) - } - return nil -} - -// RemoveNetwork removes a network configuration -func (i *WPAInterface) RemoveNetwork(networkPath dbus.ObjectPath) error { - err := i.obj.Call(InterfaceInterface+".RemoveNetwork", 0, networkPath).Err - if err != nil { - return fmt.Errorf("failed to remove network: %v", err) - } - return nil -} - -// Disconnect disconnects from the current network -func (i *WPAInterface) Disconnect() error { - err := i.obj.Call(InterfaceInterface+".Disconnect", 0).Err - if err != nil { - return fmt.Errorf("disconnect failed: %v", err) - } - return nil -} - -// GetPath returns the D-Bus object path for this interface -func (i *WPAInterface) GetPath() dbus.ObjectPath { - return i.path -} - -// GetScanning returns whether a scan is currently in progress -func (i *WPAInterface) GetScanning() (bool, error) { - prop, err := i.obj.GetProperty(InterfaceInterface + ".Scanning") - if err != nil { - return false, fmt.Errorf("failed to get Scanning property: %v", err) - } - - scanning, ok := prop.Value().(bool) - if !ok { - return false, fmt.Errorf("Scanning property is not a boolean") - } - - return scanning, nil -} diff --git a/internal/wifi/wpasupplicant/network.go b/internal/wifi/wpasupplicant/network.go deleted file mode 100644 index 3152a6b..0000000 --- a/internal/wifi/wpasupplicant/network.go +++ /dev/null @@ -1,41 +0,0 @@ -package wpasupplicant - -import ( - "github.com/godbus/dbus/v5" -) - -// Network represents a wpa_supplicant Network configuration object -type Network struct { - path dbus.ObjectPath - conn *dbus.Conn - obj dbus.BusObject -} - -// NewNetwork creates a new Network instance -func NewNetwork(conn *dbus.Conn, path dbus.ObjectPath) *Network { - return &Network{ - path: path, - conn: conn, - obj: conn.Object(Service, path), - } -} - -// GetPath returns the D-Bus object path for this network -func (n *Network) GetPath() dbus.ObjectPath { - return n.path -} - -// GetProperties returns properties of the network configuration -func (n *Network) GetProperties() (map[string]dbus.Variant, error) { - prop, err := n.obj.GetProperty(NetworkInterface + ".Properties") - if err != nil { - return nil, err - } - - props, ok := prop.Value().(map[string]dbus.Variant) - if !ok { - return nil, nil - } - - return props, nil -} diff --git a/internal/wifi/wpasupplicant/signals.go b/internal/wifi/wpasupplicant/signals.go deleted file mode 100644 index a17a292..0000000 --- a/internal/wifi/wpasupplicant/signals.go +++ /dev/null @@ -1,236 +0,0 @@ -package wpasupplicant - -import ( - "log" - "sync" - - "github.com/godbus/dbus/v5" - "github.com/nemunaire/repeater/internal/wifi/backend" -) - -// SignalMonitor monitors D-Bus signals from wpa_supplicant -type SignalMonitor struct { - conn *dbus.Conn - iface *WPAInterface - callbacks backend.EventCallbacks - - // Signal channel - signalChan chan *dbus.Signal - - // Control - stopChan chan struct{} - mu sync.RWMutex - running bool - - // State tracking - lastState WPAState -} - -// NewSignalMonitor creates a new signal monitor -func NewSignalMonitor(conn *dbus.Conn, iface *WPAInterface) *SignalMonitor { - return &SignalMonitor{ - conn: conn, - iface: iface, - signalChan: make(chan *dbus.Signal, 100), - stopChan: make(chan struct{}), - } -} - -// Start begins monitoring D-Bus signals -func (sm *SignalMonitor) Start(callbacks backend.EventCallbacks) error { - sm.mu.Lock() - if sm.running { - sm.mu.Unlock() - return nil - } - sm.running = true - sm.callbacks = callbacks - sm.mu.Unlock() - - interfacePath := sm.iface.GetPath() - - // Add signal match for PropertiesChanged on Interface - matchOptions := []dbus.MatchOption{ - dbus.WithMatchObjectPath(interfacePath), - dbus.WithMatchInterface("org.freedesktop.DBus.Properties"), - dbus.WithMatchMember("PropertiesChanged"), - } - - if err := sm.conn.AddMatchSignal(matchOptions...); err != nil { - sm.mu.Lock() - sm.running = false - sm.mu.Unlock() - return err - } - - // Add signal match for ScanDone - scanDoneOptions := []dbus.MatchOption{ - dbus.WithMatchObjectPath(interfacePath), - dbus.WithMatchInterface(InterfaceInterface), - dbus.WithMatchMember("ScanDone"), - } - - if err := sm.conn.AddMatchSignal(scanDoneOptions...); err != nil { - sm.mu.Lock() - sm.running = false - sm.mu.Unlock() - return err - } - - // Register signal channel - sm.conn.Signal(sm.signalChan) - - // Get initial state - state, err := sm.iface.GetState() - if err == nil { - sm.lastState = state - } - - // Start monitoring goroutine - go sm.monitor() - - log.Printf("D-Bus signal monitoring started for wpa_supplicant interface %s", interfacePath) - return nil -} - -// Stop stops monitoring D-Bus signals -func (sm *SignalMonitor) Stop() { - sm.mu.Lock() - if !sm.running { - sm.mu.Unlock() - return - } - sm.running = false - sm.mu.Unlock() - - // Signal stop - close(sm.stopChan) - - // Remove signal channel - sm.conn.RemoveSignal(sm.signalChan) - - log.Printf("D-Bus signal monitoring stopped for wpa_supplicant") -} - -// monitor is the main signal processing loop -func (sm *SignalMonitor) monitor() { - for { - select { - case sig := <-sm.signalChan: - sm.handleSignal(sig) - case <-sm.stopChan: - return - } - } -} - -// handleSignal processes a D-Bus signal -func (sm *SignalMonitor) handleSignal(sig *dbus.Signal) { - // Handle ScanDone signal - if sig.Name == InterfaceInterface+".ScanDone" { - sm.handleScanDone(sig) - return - } - - // Handle PropertiesChanged signals - if sig.Name != "org.freedesktop.DBus.Properties.PropertiesChanged" { - return - } - - // Verify signal is from Interface - if len(sig.Body) < 2 { - return - } - - interfaceName, ok := sig.Body[0].(string) - if !ok || interfaceName != InterfaceInterface { - return - } - - // Parse changed properties - changedProps, ok := sig.Body[1].(map[string]dbus.Variant) - if !ok { - return - } - - // Check for State property change - if stateVariant, ok := changedProps["State"]; ok { - if state, ok := stateVariant.Value().(string); ok { - sm.handleStateChange(WPAState(state)) - } - } - - // Check for CurrentBSS property change (connection status) - if _, ok := changedProps["CurrentBSS"]; ok { - // BSS changed, trigger state update - sm.handleConnectionChange() - } -} - -// handleStateChange processes a state change -func (sm *SignalMonitor) handleStateChange(state WPAState) { - sm.lastState = state - - sm.mu.RLock() - callback := sm.callbacks.OnStateChange - sm.mu.RUnlock() - - if callback == nil { - return - } - - // Map wpa_supplicant state to backend state - backendState := mapWPAState(state) - - // Get connected SSID if connected - ssid := "" - if backendState == backend.StateConnected { - if bssPath, err := sm.iface.GetCurrentBSS(); err == nil && bssPath != "/" { - bss := NewBSS(sm.conn, bssPath) - if ssidStr, err := bss.GetSSIDString(); err == nil { - ssid = ssidStr - } - } - } - - callback(backendState, ssid) -} - -// handleConnectionChange processes connection changes -func (sm *SignalMonitor) handleConnectionChange() { - // Get current state and trigger state change callback - state, err := sm.iface.GetState() - if err != nil { - return - } - - sm.handleStateChange(state) -} - -// handleScanDone processes scan completion -func (sm *SignalMonitor) handleScanDone(sig *dbus.Signal) { - sm.mu.RLock() - callback := sm.callbacks.OnScanComplete - sm.mu.RUnlock() - - if callback != nil { - callback() - } -} - -// mapWPAState maps wpa_supplicant states to backend-agnostic states -func mapWPAState(state WPAState) backend.ConnectionState { - switch state { - case StateCompleted: - return backend.StateConnected - case StateAuthenticating, StateAssociating, StateAssociated, State4WayHandshake, StateGroupHandshake: - return backend.StateConnecting - case StateDisconnected, StateInactive, StateInterfaceDisabled: - return backend.StateDisconnected - case StateScanning: - // Keep as disconnected if just scanning - return backend.StateDisconnected - default: - return backend.StateDisconnected - } -} diff --git a/internal/wifi/wpasupplicant/types.go b/internal/wifi/wpasupplicant/types.go deleted file mode 100644 index 85fec0f..0000000 --- a/internal/wifi/wpasupplicant/types.go +++ /dev/null @@ -1,27 +0,0 @@ -package wpasupplicant - -const ( - // D-Bus service and interfaces - Service = "fi.w1.wpa_supplicant1" - RootPath = "/fi/w1/wpa_supplicant1" - InterfaceInterface = "fi.w1.wpa_supplicant1.Interface" - BSSInterface = "fi.w1.wpa_supplicant1.BSS" - NetworkInterface = "fi.w1.wpa_supplicant1.Network" -) - -// WPAState represents the wpa_supplicant connection state -type WPAState string - -const ( - // wpa_supplicant state strings - StateDisconnected WPAState = "disconnected" - StateInactive WPAState = "inactive" - StateScanning WPAState = "scanning" - StateAuthenticating WPAState = "authenticating" - StateAssociating WPAState = "associating" - StateAssociated WPAState = "associated" - State4WayHandshake WPAState = "4way_handshake" - StateGroupHandshake WPAState = "group_handshake" - StateCompleted WPAState = "completed" - StateInterfaceDisabled WPAState = "interface_disabled" -) diff --git a/main.go b/main.go new file mode 100644 index 0000000..c8b9fe7 --- /dev/null +++ b/main.go @@ -0,0 +1,826 @@ +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() + } +} diff --git a/oapi-gin.cfg.yaml b/oapi-gin.cfg.yaml deleted file mode 100644 index b30fef2..0000000 --- a/oapi-gin.cfg.yaml +++ /dev/null @@ -1,5 +0,0 @@ -package: api -generate: - - gin - - embedded-spec -output: internal/api/routes.gen.go diff --git a/oapi-types.cfg.yaml b/oapi-types.cfg.yaml deleted file mode 100644 index 57b44d2..0000000 --- a/oapi-types.cfg.yaml +++ /dev/null @@ -1,4 +0,0 @@ -package: api -generate: - - types -output: internal/api/types.gen.go diff --git a/openapi.yaml b/openapi.yaml deleted file mode 100644 index 4357d09..0000000 --- a/openapi.yaml +++ /dev/null @@ -1,552 +0,0 @@ -openapi: 3.0.3 -info: - title: Travel Router Control API - description: | - API for controlling a mini travel router with dual WiFi interfaces and Ethernet connectivity. - The router can operate as a WiFi repeater, connecting to upstream networks while providing - a hotspot for client devices. - version: 1.0.0 - contact: - name: API Support - license: - name: MIT - -servers: - - url: http://localhost:8080 - description: Local router API - -tags: - - name: WiFi - description: WiFi client operations (upstream network connection) - - name: Hotspot - description: Access point operations (client-facing hotspot) - - name: Devices - description: Connected devices management - - name: System - description: System status and monitoring - - name: Logs - description: System logs and real-time monitoring - -paths: - /api/wifi/networks: - get: - tags: - - WiFi - summary: Get discovered WiFi networks - description: | - Returns the list of WiFi networks from the last scan without triggering a new scan. - Returns an empty list if no scan has been performed yet. - operationId: getWiFiNetworks - responses: - '200': - description: List of discovered networks - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/WiFiNetwork' - example: - - ssid: "Hotel-Guest" - signal: 5 - security: "WPA2" - channel: 6 - bssid: "aa:bb:cc:dd:ee:ff" - - ssid: "Public-WiFi" - signal: 3 - security: "Open" - channel: 11 - bssid: "11:22:33:44:55:66" - '500': - description: Error retrieving networks - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - - /api/wifi/scan: - get: - tags: - - WiFi - summary: Scan for available WiFi networks - description: | - Triggers a WiFi scan using wpa_supplicant via D-Bus and returns all discovered networks - sorted by signal strength. The scan takes approximately 2 seconds to complete. - operationId: scanWiFi - responses: - '200': - description: Successfully scanned networks - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/WiFiNetwork' - example: - - ssid: "Hotel-Guest" - signal: 5 - security: "WPA2" - channel: 6 - bssid: "aa:bb:cc:dd:ee:ff" - - ssid: "Public-WiFi" - signal: 3 - security: "Open" - channel: 11 - bssid: "11:22:33:44:55:66" - '500': - description: Scan error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - - /api/wifi/connect: - post: - tags: - - WiFi - summary: Connect to a WiFi network - description: | - Connects the router to an upstream WiFi network using wpa_supplicant. - Supports both open and password-protected networks (WPA/WPA2). - operationId: connectWiFi - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/WiFiConnectRequest' - examples: - protected: - summary: WPA2 protected network - value: - ssid: "Hotel-Guest" - password: "guest1234" - open: - summary: Open network - value: - ssid: "Public-WiFi" - password: "" - responses: - '200': - description: Successfully connected - content: - application/json: - schema: - $ref: '#/components/schemas/SuccessResponse' - '400': - description: Invalid request data - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - '500': - description: Connection failed - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - - /api/wifi/disconnect: - post: - tags: - - WiFi - summary: Disconnect from WiFi network - description: | - Disconnects from the currently connected upstream WiFi network - and removes all saved network configurations. - operationId: disconnectWiFi - responses: - '200': - description: Successfully disconnected - content: - application/json: - schema: - $ref: '#/components/schemas/SuccessResponse' - '500': - description: Disconnection failed - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - - /api/hotspot/toggle: - post: - tags: - - Hotspot - summary: Toggle hotspot on/off - description: | - Enables or disables the hotspot (access point) by starting/stopping - the hostapd service. Returns the new enabled state and updates - the system status with current hostapd_cli information. - operationId: toggleHotspot - responses: - '200': - description: Hotspot state changed successfully - content: - application/json: - schema: - type: object - properties: - enabled: - type: boolean - description: Current hotspot state after toggle - required: - - enabled - example: - enabled: true - '500': - description: Failed to change hotspot state - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - - /api/devices: - get: - tags: - - Devices - summary: Get connected devices - description: | - Returns a list of all devices currently connected to the hotspot. - Device information is gathered from DHCP leases and ARP tables. - Only devices with active ARP entries are considered connected. - operationId: getDevices - responses: - '200': - description: List of connected devices - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/ConnectedDevice' - example: - - name: "iPhone-12" - type: "mobile" - mac: "aa:bb:cc:11:22:33" - ip: "192.168.1.100" - - name: "MacBook-Pro" - type: "laptop" - mac: "dd:ee:ff:44:55:66" - ip: "192.168.1.101" - '500': - description: Failed to retrieve device list - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - - /api/status: - get: - tags: - - System - summary: Get system status - description: | - Returns comprehensive system status including WiFi connection state, - detailed hotspot status from hostapd_cli, connected device count, - data usage, and uptime. - operationId: getStatus - responses: - '200': - description: Current system status - content: - application/json: - schema: - $ref: '#/components/schemas/SystemStatus' - - /api/logs: - get: - tags: - - Logs - summary: Get system logs - description: | - Returns the last 100 log entries from the system. - For real-time log streaming, use the WebSocket endpoint. - operationId: getLogs - responses: - '200': - description: List of log entries - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/LogEntry' - example: - - timestamp: "2025-10-28T14:32:10Z" - source: "WiFi" - message: "Scan terminé - 5 réseaux trouvés" - - timestamp: "2025-10-28T14:32:15Z" - source: "WiFi" - message: "Tentative de connexion à Hotel-Guest" - delete: - tags: - - Logs - summary: Clear system logs - description: Clears all stored log entries (keeps only the "logs cleared" entry) - operationId: clearLogs - responses: - '200': - description: Logs cleared successfully - content: - application/json: - schema: - $ref: '#/components/schemas/SuccessResponse' - - /ws/logs: - get: - tags: - - Logs - summary: WebSocket for real-time logs - description: | - WebSocket endpoint for receiving real-time log updates. - Upon connection, all existing logs are sent, followed by new logs as they occur. - - This is a WebSocket endpoint - upgrade the HTTP connection to WebSocket protocol. - operationId: logsWebSocket - responses: - '101': - description: WebSocket connection established - '400': - description: WebSocket upgrade failed - -components: - schemas: - WiFiNetwork: - type: object - description: Discovered WiFi network information - properties: - ssid: - type: string - description: Network SSID (name) - example: "Hotel-Guest" - signal: - type: integer - description: Signal strength (1-5 scale) - minimum: 1 - maximum: 5 - example: 4 - security: - type: string - description: Security type - enum: - - Open - - WEP - - WPA - - WPA2 - example: "WPA2" - channel: - type: integer - description: WiFi channel number - minimum: 1 - maximum: 165 - example: 6 - bssid: - type: string - description: Access point MAC address - pattern: '^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$' - example: "aa:bb:cc:dd:ee:ff" - required: - - ssid - - signal - - security - - channel - - bssid - - WiFiConnectRequest: - type: object - description: Request to connect to a WiFi network - properties: - ssid: - type: string - description: Network SSID to connect to - example: "Hotel-Guest" - password: - type: string - description: Network password (empty string for open networks) - example: "guest1234" - required: - - ssid - - password - - HotspotStatus: - type: object - description: Detailed hotspot status from hostapd_cli - properties: - state: - type: string - description: Hotspot state (ENABLED, DISABLED, etc.) - example: "ENABLED" - ssid: - type: string - description: Current SSID being broadcast - example: "TravelRouter" - bssid: - type: string - description: MAC address of the access point - pattern: '^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$' - example: "4a:e3:4e:09:57:f8" - channel: - type: integer - description: Current WiFi channel - minimum: 1 - maximum: 14 - example: 11 - frequency: - type: integer - description: Frequency in MHz - example: 2462 - numStations: - type: integer - description: Number of connected stations - minimum: 0 - example: 2 - hwMode: - type: string - description: Hardware mode (g, a, n, ac, etc.) - example: "g" - countryCode: - type: string - description: Country code - example: "VN" - required: - - state - - ConnectedDevice: - type: object - description: Device connected to the hotspot - properties: - name: - type: string - description: Device hostname - example: "iPhone-12" - type: - type: string - description: Detected device type - enum: - - mobile - - tablet - - laptop - - desktop - - unknown - example: "mobile" - mac: - type: string - description: Device MAC address - pattern: '^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$' - example: "aa:bb:cc:11:22:33" - ip: - type: string - description: Assigned IP address - pattern: '^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$' - example: "192.168.1.100" - required: - - name - - type - - mac - - ip - - SystemStatus: - type: object - description: Overall system status - properties: - connected: - type: boolean - description: Whether router is connected to upstream WiFi - example: true - connectionState: - type: string - description: Current WiFi connection state - enum: - - connected - - disconnected - - connecting - - disconnecting - - roaming - example: "connected" - connectedSSID: - type: string - description: SSID of connected upstream network (empty if not connected) - example: "Hotel-Guest" - hotspotStatus: - allOf: - - $ref: '#/components/schemas/HotspotStatus' - nullable: true - description: Detailed hotspot status (null if hotspot is not running) - connectedCount: - type: integer - description: Number of devices connected to hotspot - minimum: 0 - example: 3 - dataUsage: - type: number - format: double - description: Total data usage in MB - example: 145.7 - uptime: - type: integer - format: int64 - description: System uptime in seconds - example: 3600 - connectedDevices: - type: array - description: List of devices connected to hotspot - items: - $ref: '#/components/schemas/ConnectedDevice' - required: - - connected - - connectionState - - connectedSSID - - connectedCount - - dataUsage - - uptime - - connectedDevices - - LogEntry: - type: object - description: System log entry - properties: - timestamp: - type: string - format: date-time - description: When the log entry was created - example: "2025-10-28T14:32:10Z" - source: - type: string - description: Log source component - enum: - - Système - - WiFi - - Hotspot - example: "WiFi" - message: - type: string - description: Log message - example: "Scan terminé - 5 réseaux trouvés" - required: - - timestamp - - source - - message - - SuccessResponse: - type: object - description: Generic success response - properties: - status: - type: string - enum: - - success - example: "success" - required: - - status - - Error: - type: object - description: Error response - properties: - error: - type: string - description: Error message - example: "Erreur lors du scan WiFi" - required: - - error diff --git a/static/app.js b/static/app.js new file mode 100644 index 0000000..6fce809 --- /dev/null +++ b/static/app.js @@ -0,0 +1,239 @@ +// État global de l'application +let appState = { + selectedWifi: null, + hotspotEnabled: true, + connectedDevices: [], + wifiNetworks: [], + uptime: 0, + dataUsage: 0 +}; + +// Simulation de données +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"); +} + +async function updateWifiList() { + const wifiList = document.getElementById('wifiList'); + wifiList.innerHTML = ''; + + (await (await fetch('/api/wifi/scan')).json()).forEach((network, index) => { + const wifiItem = document.createElement('div'); + wifiItem.className = 'wifi-item'; + wifiItem.onclick = () => selectWifi(network, wifiItem); + + wifiItem.innerHTML = ` +
+ ${network.ssid} +
${network.security} • Canal ${network.channel}
+
+
+ ${generateSignalBars(network.signal)} +
+ `; + + 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(`
`); + } + return `
${bars.join('')}
`; +} + +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} +
${device.name}
+
${device.ip}
+ `; + + devicesList.appendChild(deviceCard); + }); + + document.getElementById('connectedDevices').textContent = mockDevices.length; +} + +function getDeviceIcon(type) { + const icons = { + mobile: '', + laptop: '', + tablet: '' + }; + 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 = `[${timestamp}] ${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 = '
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 = '
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 = ` +
+ Connecté à ${appState.selectedWifi.ssid} + `; + }, 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); +} diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..dbb7131 --- /dev/null +++ b/static/index.html @@ -0,0 +1,135 @@ + + + + + + WiFi Repeater Control + + + +
+
+

🌐 WiFi Repeater Control

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

+ + + + Connexion WiFi Externe +

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

+ + + + Configuration Hotspot +

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

+ + + + Appareils connectés +

+ +
+ +
+
+ +
+

+ + + + Logs système +

+ +
+ +
+ + +
+
+
+ +
+ + + + diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..3ea544a --- /dev/null +++ b/static/style.css @@ -0,0 +1,338 @@ +* { + 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; + } +}