| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054 |
- package main
- import (
- "bufio"
- "context"
- "encoding/hex"
- "encoding/json"
- "fmt"
- "log"
- "net/http"
- "os"
- "os/exec"
- "strconv"
- "strings"
- "sync"
- "time"
- "github.com/gorilla/websocket"
- )
- // APIServer handles HTTP API requests
- type APIServer struct {
- daemon *Daemon
- upgrader websocket.Upgrader
- // HTTP server
- httpServer *http.Server
- serverMu sync.Mutex
- // Recent events (ring buffer)
- recentBLE []interface{}
- recentWiFi []interface{}
- recentMu sync.RWMutex
- // WebSocket clients
- wsClients map[*websocket.Conn]bool
- wsClientsMu sync.Mutex
- // Session management
- sessions map[string]time.Time
- sessionsMu sync.RWMutex
- // Cached CPU metrics (updated in background)
- cachedCPUPercent float64
- cpuMu sync.RWMutex
- }
- // StatusResponse is the response for /api/status
- type StatusResponse struct {
- DeviceID string `json:"device_id"`
- Registered bool `json:"registered"`
- Mode string `json:"mode"`
- Uptime int64 `json:"uptime_sec"`
- Network NetworkStatus `json:"network"`
- Scanners ScannerStatus `json:"scanners"`
- Counters CounterStatus `json:"counters"`
- ServerOK bool `json:"server_ok"`
- }
- type NetworkStatus struct {
- Eth0IP string `json:"eth0_ip,omitempty"`
- Eth0MAC string `json:"eth0_mac,omitempty"`
- Eth0RX int64 `json:"eth0_rx,omitempty"`
- Eth0TX int64 `json:"eth0_tx,omitempty"`
- Wlan0IP string `json:"wlan0_ip,omitempty"`
- Wlan0MAC string `json:"wlan0_mac,omitempty"`
- Wlan0SSID string `json:"wlan0_ssid,omitempty"`
- Wlan0Signal int `json:"wlan0_signal,omitempty"`
- Wlan0Channel int `json:"wlan0_channel,omitempty"`
- Wlan0Gateway string `json:"wlan0_gateway,omitempty"`
- Wlan0DNS string `json:"wlan0_dns,omitempty"`
- Wlan0RX int64 `json:"wlan0_rx,omitempty"`
- Wlan0TX int64 `json:"wlan0_tx,omitempty"`
- Gateway string `json:"gateway,omitempty"`
- DNS string `json:"dns,omitempty"`
- NTP string `json:"ntp,omitempty"`
- APActive bool `json:"ap_active"`
- }
- type ScannerStatus struct {
- BLERunning bool `json:"ble_running"`
- WiFiRunning bool `json:"wifi_running"`
- }
- type CounterStatus struct {
- BLEEvents uint64 `json:"ble_events"`
- WiFiEvents uint64 `json:"wifi_events"`
- Uploads uint64 `json:"uploads"`
- Errors uint64 `json:"errors"`
- }
- // MetricsResponse is the response for /api/metrics
- type MetricsResponse struct {
- CPUPercent float64 `json:"cpu_percent"`
- MemUsedMB float64 `json:"mem_used_mb"`
- MemTotalMB float64 `json:"mem_total_mb"`
- Temperature float64 `json:"temperature"`
- Load1m float64 `json:"load_1m"`
- Load5m float64 `json:"load_5m"`
- Load15m float64 `json:"load_15m"`
- }
- // SettingsRequest is the request for /api/settings
- type SettingsRequest struct {
- Password string `json:"password"`
- Settings SettingsPayload `json:"settings"`
- }
- // SettingsPayload contains the actual settings
- type SettingsPayload struct {
- Mode string `json:"mode"`
- // BLE Scanner (LAN mode only)
- BLEEnabled bool `json:"ble_enabled"`
- BLEBatchInterval int `json:"ble_batch_interval_ms"`
- BLEEndpoint string `json:"ble_endpoint"`
- // WiFi Scanner (LAN mode only)
- WiFiMonitorEnabled bool `json:"wifi_monitor_enabled"`
- WiFiMonitorBatchInterval int `json:"wifi_monitor_batch_interval_ms"`
- WiFiMonitorEndpoint string `json:"wifi_monitor_endpoint"`
- // WiFi Client
- WiFiClientEnabled bool `json:"wifi_client_enabled"`
- WifiSSID string `json:"wifi_ssid"`
- WifiPSK string `json:"wifi_psk"`
- // eth0
- Eth0Mode string `json:"eth0_mode"`
- Eth0IP string `json:"eth0_ip"`
- Eth0Gateway string `json:"eth0_gateway"`
- Eth0DNS string `json:"eth0_dns"`
- // NTP
- NTPServers string `json:"ntp_servers"`
- }
- // NewAPIServer creates a new API server
- func NewAPIServer(daemon *Daemon) *APIServer {
- return &APIServer{
- daemon: daemon,
- upgrader: websocket.Upgrader{
- CheckOrigin: func(r *http.Request) bool {
- return true // Allow all origins for local access
- },
- },
- recentBLE: make([]interface{}, 0, 100),
- recentWiFi: make([]interface{}, 0, 100),
- wsClients: make(map[*websocket.Conn]bool),
- sessions: make(map[string]time.Time),
- }
- }
- // Start starts the HTTP server
- func (s *APIServer) Start(addr string) error {
- s.serverMu.Lock()
- defer s.serverMu.Unlock()
- if s.httpServer != nil {
- return fmt.Errorf("HTTP server already running")
- }
- // Start CPU monitoring goroutine
- go s.updateCPUMetrics()
- mux := http.NewServeMux()
- // API endpoints
- mux.HandleFunc("/api/status", s.handleStatus)
- mux.HandleFunc("/api/metrics", s.handleMetrics)
- mux.HandleFunc("/api/ble/recent", s.handleBLERecent)
- mux.HandleFunc("/api/wifi/recent", s.handleWiFiRecent)
- mux.HandleFunc("/api/config", s.handleConfig)
- mux.HandleFunc("/api/settings", s.handleSettings)
- mux.HandleFunc("/api/unlock", s.handleUnlock)
- mux.HandleFunc("/api/logs", s.handleLogs)
- mux.HandleFunc("/api/kernel-logs", s.handleKernelLogs)
- mux.HandleFunc("/api/ws", s.handleWebSocket)
- // Serve static files for dashboard with SPA fallback
- mux.Handle("/", s.spaHandler("/opt/mybeacon/www"))
- s.httpServer = &http.Server{
- Addr: addr,
- Handler: s.corsMiddleware(mux),
- }
- log.Printf("[api] Starting HTTP server on %s", addr)
- return s.httpServer.ListenAndServe()
- }
- // Stop stops the HTTP server
- func (s *APIServer) Stop() error {
- s.serverMu.Lock()
- defer s.serverMu.Unlock()
- if s.httpServer == nil {
- return nil // Already stopped
- }
- log.Println("[api] Stopping HTTP server...")
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
- err := s.httpServer.Shutdown(ctx)
- s.httpServer = nil
- return err
- }
- // IsRunning returns true if HTTP server is running
- func (s *APIServer) IsRunning() bool {
- s.serverMu.Lock()
- defer s.serverMu.Unlock()
- return s.httpServer != nil
- }
- // spaHandler serves static files with SPA fallback
- func (s *APIServer) spaHandler(staticPath string) http.Handler {
- fileServer := http.FileServer(http.Dir(staticPath))
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- // Check if file exists
- path := staticPath + r.URL.Path
- _, err := os.Stat(path)
- // If file doesn't exist and it's not the root, serve index.html (SPA routing)
- if os.IsNotExist(err) && r.URL.Path != "/" {
- // Serve index.html for SPA routing
- http.ServeFile(w, r, staticPath+"/index.html")
- return
- }
- // Serve the file normally
- fileServer.ServeHTTP(w, r)
- })
- }
- // corsMiddleware adds CORS headers
- func (s *APIServer) corsMiddleware(next http.Handler) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Access-Control-Allow-Origin", "*")
- w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
- w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
- if r.Method == "OPTIONS" {
- w.WriteHeader(http.StatusOK)
- return
- }
- next.ServeHTTP(w, r)
- })
- }
- // handleStatus returns device status
- func (s *APIServer) handleStatus(w http.ResponseWriter, r *http.Request) {
- s.daemon.mu.Lock()
- defer s.daemon.mu.Unlock()
- eth0rx, eth0tx := getInterfaceStats("eth0")
- wlan0rx, wlan0tx := getInterfaceStats("wlan0")
- wlanInfo := getWlanInfo()
- // Gateway and DNS are shared, but wlan0 might have its own
- gateway := getDefaultGateway()
- dns := getDNS()
- status := StatusResponse{
- DeviceID: s.daemon.state.DeviceID,
- Registered: s.daemon.state.DeviceToken != "",
- Mode: "cloud", // TODO: implement mode switching
- Uptime: getUptime(),
- Network: NetworkStatus{
- Eth0IP: getInterfaceIP("eth0"),
- Eth0MAC: getMacAddress("eth0"),
- Eth0RX: eth0rx,
- Eth0TX: eth0tx,
- Wlan0IP: getInterfaceIP("wlan0"),
- Wlan0MAC: getMacAddress("wlan0"),
- Wlan0SSID: wlanInfo.ssid,
- Wlan0Signal: wlanInfo.signal,
- Wlan0Channel: wlanInfo.channel,
- Wlan0Gateway: gateway, // same gateway for now
- Wlan0DNS: dns, // same DNS for now
- Wlan0RX: wlan0rx,
- Wlan0TX: wlan0tx,
- Gateway: gateway,
- DNS: dns,
- NTP: "pool.ntp.org",
- },
- Scanners: ScannerStatus{
- BLERunning: s.daemon.scanners.IsBLERunning(),
- WiFiRunning: s.daemon.scanners.IsWiFiRunning(),
- },
- ServerOK: s.daemon.state.DeviceToken != "",
- }
- s.jsonResponse(w, status)
- }
- // updateCPUMetrics updates CPU metrics in background
- func (s *APIServer) updateCPUMetrics() {
- for {
- cpuPercent := getCPUPercent()
- s.cpuMu.Lock()
- s.cachedCPUPercent = cpuPercent
- s.cpuMu.Unlock()
- time.Sleep(2 * time.Second)
- }
- }
- // handleMetrics returns system metrics
- func (s *APIServer) handleMetrics(w http.ResponseWriter, r *http.Request) {
- load1m, load5m, load15m := getLoadAvg()
- s.cpuMu.RLock()
- cpuPercent := s.cachedCPUPercent
- s.cpuMu.RUnlock()
- metrics := MetricsResponse{
- CPUPercent: cpuPercent,
- MemUsedMB: getMemUsedMB(),
- MemTotalMB: getMemTotalMB(),
- Temperature: getTemperature(),
- Load1m: load1m,
- Load5m: load5m,
- Load15m: load15m,
- }
- s.jsonResponse(w, metrics)
- }
- // handleBLERecent returns recent BLE events
- func (s *APIServer) handleBLERecent(w http.ResponseWriter, r *http.Request) {
- s.recentMu.RLock()
- events := make([]interface{}, len(s.recentBLE))
- copy(events, s.recentBLE)
- s.recentMu.RUnlock()
- s.jsonResponse(w, events)
- }
- // handleWiFiRecent returns recent WiFi events
- func (s *APIServer) handleWiFiRecent(w http.ResponseWriter, r *http.Request) {
- s.recentMu.RLock()
- events := make([]interface{}, len(s.recentWiFi))
- copy(events, s.recentWiFi)
- s.recentMu.RUnlock()
- s.jsonResponse(w, events)
- }
- // handleLogs returns recent daemon log lines
- func (s *APIServer) handleLogs(w http.ResponseWriter, r *http.Request) {
- logFile := "/var/log/mybeacon.log"
- data, err := os.ReadFile(logFile)
- if err != nil {
- s.jsonResponse(w, []string{})
- return
- }
- lines := strings.Split(string(data), "\n")
- // Return last 500 lines
- if len(lines) > 500 {
- lines = lines[len(lines)-500:]
- }
- // Filter out empty lines
- var result []string
- for _, line := range lines {
- if strings.TrimSpace(line) != "" {
- result = append(result, line)
- }
- }
- s.jsonResponse(w, result)
- }
- // handleKernelLogs returns recent kernel log lines from dmesg
- func (s *APIServer) handleKernelLogs(w http.ResponseWriter, r *http.Request) {
- // Run dmesg command
- cmd := exec.Command("dmesg", "-T")
- output, err := cmd.CombinedOutput()
- if err != nil {
- // Try without -T flag (human-readable timestamps) if not supported
- cmd = exec.Command("dmesg")
- output, err = cmd.CombinedOutput()
- if err != nil {
- s.jsonResponse(w, []string{})
- return
- }
- }
- lines := strings.Split(string(output), "\n")
- // Return last 500 lines
- if len(lines) > 500 {
- lines = lines[len(lines)-500:]
- }
- // Filter out empty lines
- var result []string
- for _, line := range lines {
- if strings.TrimSpace(line) != "" {
- result = append(result, line)
- }
- }
- s.jsonResponse(w, result)
- }
- // handleConfig returns current config (without secrets)
- func (s *APIServer) handleConfig(w http.ResponseWriter, r *http.Request) {
- s.daemon.mu.Lock()
- cfg := *s.daemon.cfg
- s.daemon.mu.Unlock()
- // Remove secrets
- cfg.SSHTunnel.KeyPath = ""
- s.jsonResponse(w, cfg)
- }
- // handleSettings handles settings updates (requires password)
- func (s *APIServer) handleSettings(w http.ResponseWriter, r *http.Request) {
- if r.Method != "POST" {
- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
- return
- }
- var req SettingsRequest
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- http.Error(w, "Invalid JSON", http.StatusBadRequest)
- return
- }
- // Verify password
- if !s.verifyPassword(req.Password) {
- http.Error(w, "Invalid password", http.StatusUnauthorized)
- return
- }
- s.daemon.mu.Lock()
- // Apply mode change
- if req.Settings.Mode != "" && (req.Settings.Mode == "cloud" || req.Settings.Mode == "lan") {
- if s.daemon.cfg.Mode != req.Settings.Mode {
- log.Printf("[api] Mode changed: %s -> %s", s.daemon.cfg.Mode, req.Settings.Mode)
- s.daemon.cfg.Mode = req.Settings.Mode
- }
- }
- // In LAN mode: apply scanner settings
- if s.daemon.cfg.Mode == "lan" {
- // BLE Scanner
- s.daemon.cfg.BLE.Enabled = req.Settings.BLEEnabled
- if req.Settings.BLEBatchInterval > 0 {
- s.daemon.cfg.BLE.BatchIntervalMs = req.Settings.BLEBatchInterval
- }
- // Store custom BLE upload endpoint for LAN mode
- s.daemon.cfg.BLE.UploadEndpoint = req.Settings.BLEEndpoint
- log.Printf("[api] BLE scanner (LAN): enabled=%v, interval=%d, endpoint=%s", req.Settings.BLEEnabled, req.Settings.BLEBatchInterval, req.Settings.BLEEndpoint)
- // WiFi Scanner
- s.daemon.cfg.WiFi.MonitorEnabled = req.Settings.WiFiMonitorEnabled
- if req.Settings.WiFiMonitorBatchInterval > 0 {
- s.daemon.cfg.WiFi.BatchIntervalMs = req.Settings.WiFiMonitorBatchInterval
- }
- // Store custom WiFi upload endpoint for LAN mode
- s.daemon.cfg.WiFi.UploadEndpoint = req.Settings.WiFiMonitorEndpoint
- log.Printf("[api] WiFi scanner (LAN): enabled=%v, interval=%d, endpoint=%s", req.Settings.WiFiMonitorEnabled, req.Settings.WiFiMonitorBatchInterval, req.Settings.WiFiMonitorEndpoint)
- // WiFi Client enabled flag
- s.daemon.cfg.WiFi.ClientEnabled = req.Settings.WiFiClientEnabled
- }
- // Apply WiFi Client settings (both modes)
- if req.Settings.WifiSSID != "" {
- s.daemon.cfg.WiFi.SSID = req.Settings.WifiSSID
- s.daemon.cfg.WiFi.PSK = req.Settings.WifiPSK
- log.Printf("[api] WiFi client: ssid=%s", req.Settings.WifiSSID)
- }
- // Apply NTP settings
- if req.Settings.NTPServers != "" {
- servers := strings.Split(req.Settings.NTPServers, ",")
- for i := range servers {
- servers[i] = strings.TrimSpace(servers[i])
- }
- s.daemon.cfg.Network.NTPServers = servers
- log.Printf("[api] NTP servers updated: %v", servers)
- }
- // Apply eth0 settings (always local, never from server)
- if req.Settings.Eth0Mode == "static" {
- s.daemon.cfg.Network.Eth0.Static = true
- s.daemon.cfg.Network.Eth0.Address = req.Settings.Eth0IP
- s.daemon.cfg.Network.Eth0.Gateway = req.Settings.Eth0Gateway
- s.daemon.cfg.Network.Eth0.DNS = req.Settings.Eth0DNS
- log.Printf("[api] eth0 static IP configured: %s", req.Settings.Eth0IP)
- } else if req.Settings.Eth0Mode == "dhcp" {
- s.daemon.cfg.Network.Eth0.Static = false
- log.Printf("[api] eth0 set to DHCP")
- }
- // Save local config
- SaveConfig(s.daemon.configPath, s.daemon.cfg)
- // In Cloud Mode: send WiFi credentials to server for centralized management
- if s.daemon.cfg.Mode == "cloud" && req.Settings.WifiSSID != "" {
- s.daemon.mu.Unlock()
- if err := s.daemon.updateWiFiCredentials(req.Settings.WifiSSID, req.Settings.WifiPSK); err != nil {
- log.Printf("[api] Failed to update WiFi credentials on server: %v", err)
- // Don't fail the request - local settings are already saved
- } else {
- log.Printf("[api] WiFi credentials updated on server")
- }
- s.daemon.mu.Lock()
- }
- s.daemon.mu.Unlock()
- // Apply WiFi client connection if credentials provided
- if req.Settings.WifiSSID != "" {
- if err := applyWiFiSettings(req.Settings.WifiSSID, req.Settings.WifiPSK); err != nil {
- log.Printf("[api] Failed to apply WiFi settings: %v", err)
- http.Error(w, "Failed to apply WiFi settings: "+err.Error(), http.StatusInternalServerError)
- return
- }
- log.Printf("[api] WiFi client connection initiated: SSID=%s", req.Settings.WifiSSID)
- }
- s.jsonResponse(w, map[string]bool{"success": true})
- }
- // handleUnlock verifies password and creates session
- func (s *APIServer) handleUnlock(w http.ResponseWriter, r *http.Request) {
- if r.Method != "POST" {
- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
- return
- }
- var req struct {
- Password string `json:"password"`
- }
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- http.Error(w, "Invalid JSON", http.StatusBadRequest)
- return
- }
- if !s.verifyPassword(req.Password) {
- http.Error(w, "Invalid password", http.StatusUnauthorized)
- return
- }
- // Create session token (simple implementation)
- token := generateToken(32)
- s.sessionsMu.Lock()
- s.sessions[token] = time.Now().Add(30 * time.Minute)
- s.sessionsMu.Unlock()
- s.jsonResponse(w, map[string]string{"token": token})
- }
- // handleWebSocket handles WebSocket connections for live updates
- func (s *APIServer) handleWebSocket(w http.ResponseWriter, r *http.Request) {
- conn, err := s.upgrader.Upgrade(w, r, nil)
- if err != nil {
- log.Printf("[api] WebSocket upgrade error: %v", err)
- return
- }
- defer conn.Close()
- s.wsClientsMu.Lock()
- s.wsClients[conn] = true
- s.wsClientsMu.Unlock()
- defer func() {
- s.wsClientsMu.Lock()
- delete(s.wsClients, conn)
- s.wsClientsMu.Unlock()
- }()
- // Keep connection alive and read messages
- for {
- _, _, err := conn.ReadMessage()
- if err != nil {
- break
- }
- }
- }
- // AddBLEEvent adds a BLE event to recent list and broadcasts to WebSocket
- func (s *APIServer) AddBLEEvent(event interface{}) {
- s.recentMu.Lock()
- s.recentBLE = append(s.recentBLE, event)
- if len(s.recentBLE) > 100 {
- // Copy to new slice to allow GC of old backing array
- newSlice := make([]interface{}, 100)
- copy(newSlice, s.recentBLE[len(s.recentBLE)-100:])
- s.recentBLE = newSlice
- }
- s.recentMu.Unlock()
- // Broadcast asynchronously to avoid blocking the event loop
- go s.broadcast(map[string]interface{}{
- "type": "ble",
- "event": event,
- })
- }
- // AddWiFiEvent adds a WiFi event to recent list and broadcasts to WebSocket
- func (s *APIServer) AddWiFiEvent(event interface{}) {
- s.recentMu.Lock()
- s.recentWiFi = append(s.recentWiFi, event)
- if len(s.recentWiFi) > 100 {
- // Copy to new slice to allow GC of old backing array
- newSlice := make([]interface{}, 100)
- copy(newSlice, s.recentWiFi[len(s.recentWiFi)-100:])
- s.recentWiFi = newSlice
- }
- s.recentMu.Unlock()
- // Broadcast asynchronously to avoid blocking the event loop
- go s.broadcast(map[string]interface{}{
- "type": "wifi",
- "event": event,
- })
- }
- // broadcast sends a message to all WebSocket clients
- func (s *APIServer) broadcast(msg interface{}) {
- data, err := json.Marshal(msg)
- if err != nil {
- return
- }
- s.wsClientsMu.Lock()
- defer s.wsClientsMu.Unlock()
- for conn := range s.wsClients {
- if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
- conn.Close()
- delete(s.wsClients, conn)
- }
- }
- }
- // verifyPassword checks if password matches device password
- func (s *APIServer) verifyPassword(password string) bool {
- s.daemon.mu.Lock()
- devicePassword := s.daemon.state.DevicePassword
- s.daemon.mu.Unlock()
- // Use default password "admin" if no password set (device not registered)
- if devicePassword == "" {
- devicePassword = "admin"
- }
- return password != "" && password == devicePassword
- }
- // jsonResponse sends a JSON response
- func (s *APIServer) jsonResponse(w http.ResponseWriter, data interface{}) {
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(data)
- }
- // Helper functions for system metrics
- func getUptime() int64 {
- data, err := os.ReadFile("/proc/uptime")
- if err != nil {
- return 0
- }
- var uptime float64
- if err := parseFloat(string(data), &uptime); err != nil {
- return 0
- }
- return int64(uptime)
- }
- func parseFloat(s string, f *float64) error {
- parts := strings.Fields(s)
- if len(parts) == 0 {
- return nil
- }
- return json.Unmarshal([]byte(parts[0]), f)
- }
- func getCPUPercent() float64 {
- // Read CPU stats from /proc/stat twice with 100ms interval
- stat1 := readCPUStat()
- time.Sleep(100 * time.Millisecond)
- stat2 := readCPUStat()
- if stat1 == nil || stat2 == nil {
- return 0
- }
- // Calculate totals (sum all fields)
- var total1, total2, idle1, idle2 int64
- for i := 0; i < len(stat1) && i < 10; i++ {
- total1 += stat1[i]
- if i < len(stat2) {
- total2 += stat2[i]
- }
- }
- // Idle = idle + iowait (fields 3 and 4)
- if len(stat1) >= 5 {
- idle1 = stat1[3] + stat1[4]
- }
- if len(stat2) >= 5 {
- idle2 = stat2[3] + stat2[4]
- }
- totalDelta := total2 - total1
- idleDelta := idle2 - idle1
- if totalDelta == 0 {
- return 0
- }
- cpuPercent := 100.0 * (1.0 - float64(idleDelta)/float64(totalDelta))
- return cpuPercent
- }
- func readCPUStat() []int64 {
- data, err := os.ReadFile("/proc/stat")
- if err != nil {
- return nil
- }
- // First line: cpu user nice system idle iowait irq softirq steal guest guest_nice
- scanner := bufio.NewScanner(strings.NewReader(string(data)))
- if !scanner.Scan() {
- return nil
- }
- line := scanner.Text()
- if !strings.HasPrefix(line, "cpu ") {
- return nil
- }
- fields := strings.Fields(line)
- if len(fields) < 5 {
- return nil
- }
- var stats []int64
- // Read all available fields (up to 10)
- for i := 1; i < len(fields) && i < 11; i++ {
- var v int64
- json.Unmarshal([]byte(fields[i]), &v)
- stats = append(stats, v)
- }
- return stats
- }
- func getMemUsedMB() float64 {
- data, err := os.ReadFile("/proc/meminfo")
- if err != nil {
- return 0
- }
- var memTotal, memAvailable int64
- scanner := bufio.NewScanner(strings.NewReader(string(data)))
- for scanner.Scan() {
- line := scanner.Text()
- if strings.HasPrefix(line, "MemTotal:") {
- parseMemInfo(line, &memTotal)
- } else if strings.HasPrefix(line, "MemAvailable:") {
- parseMemInfo(line, &memAvailable)
- }
- }
- if memTotal == 0 {
- return 0
- }
- memUsed := memTotal - memAvailable
- return float64(memUsed) / 1024 // Convert KB to MB
- }
- func getMemTotalMB() float64 {
- data, err := os.ReadFile("/proc/meminfo")
- if err != nil {
- return 0
- }
- scanner := bufio.NewScanner(strings.NewReader(string(data)))
- for scanner.Scan() {
- line := scanner.Text()
- if strings.HasPrefix(line, "MemTotal:") {
- var total int64
- if err := parseMemInfo(line, &total); err == nil {
- return float64(total) / 1024
- }
- }
- }
- return 0
- }
- func parseMemInfo(line string, value *int64) error {
- parts := strings.Fields(line)
- if len(parts) >= 2 {
- return json.Unmarshal([]byte(parts[1]), value)
- }
- return nil
- }
- func getTemperature() float64 {
- data, err := os.ReadFile("/sys/class/thermal/thermal_zone0/temp")
- if err != nil {
- return 0
- }
- var temp int64
- if err := json.Unmarshal([]byte(strings.TrimSpace(string(data))), &temp); err != nil {
- return 0
- }
- return float64(temp) / 1000
- }
- func getLoadAvg() (float64, float64, float64) {
- data, err := os.ReadFile("/proc/loadavg")
- if err != nil {
- return 0, 0, 0
- }
- // /proc/loadavg format: "0.10 0.40 0.49 1/98 2452"
- parts := strings.Fields(string(data))
- if len(parts) < 3 {
- return 0, 0, 0
- }
- var load1m, load5m, load15m float64
- json.Unmarshal([]byte(parts[0]), &load1m)
- json.Unmarshal([]byte(parts[1]), &load5m)
- json.Unmarshal([]byte(parts[2]), &load15m)
- return load1m, load5m, load15m
- }
- func generateToken(length int) string {
- const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
- b := make([]byte, length)
- for i := range b {
- b[i] = chars[time.Now().UnixNano()%int64(len(chars))]
- time.Sleep(time.Nanosecond)
- }
- return string(b)
- }
- func getInterfaceStats(name string) (rx, tx int64) {
- rxPath := "/sys/class/net/" + name + "/statistics/rx_bytes"
- txPath := "/sys/class/net/" + name + "/statistics/tx_bytes"
- if data, err := os.ReadFile(rxPath); err == nil {
- json.Unmarshal([]byte(strings.TrimSpace(string(data))), &rx)
- }
- if data, err := os.ReadFile(txPath); err == nil {
- json.Unmarshal([]byte(strings.TrimSpace(string(data))), &tx)
- }
- return
- }
- func getDefaultGateway() string {
- data, err := os.ReadFile("/proc/net/route")
- if err != nil {
- return ""
- }
- scanner := bufio.NewScanner(strings.NewReader(string(data)))
- for scanner.Scan() {
- fields := strings.Fields(scanner.Text())
- if len(fields) >= 3 && fields[1] == "00000000" {
- gw := fields[2]
- if len(gw) == 8 {
- b, _ := hex.DecodeString(gw)
- if len(b) == 4 {
- // Little-endian: reverse bytes
- return fmt.Sprintf("%d.%d.%d.%d", b[3], b[2], b[1], b[0])
- }
- }
- }
- }
- return ""
- }
- func getDNS() string {
- data, err := os.ReadFile("/etc/resolv.conf")
- if err != nil {
- return ""
- }
- var servers []string
- scanner := bufio.NewScanner(strings.NewReader(string(data)))
- for scanner.Scan() {
- line := scanner.Text()
- if strings.HasPrefix(line, "nameserver ") {
- servers = append(servers, strings.TrimPrefix(line, "nameserver "))
- }
- }
- return strings.Join(servers, ", ")
- }
- type wlanInfoResult struct {
- ssid string
- signal int
- channel int
- }
- func getWlanInfo() wlanInfoResult {
- var info wlanInfoResult
- // Get SSID, signal, channel from iw dev wlan0 link
- out, err := exec.Command("iw", "dev", "wlan0", "link").Output()
- if err == nil {
- scanner := bufio.NewScanner(strings.NewReader(string(out)))
- for scanner.Scan() {
- line := strings.TrimSpace(scanner.Text())
- if strings.HasPrefix(line, "SSID:") {
- info.ssid = strings.TrimSpace(strings.TrimPrefix(line, "SSID:"))
- } else if strings.HasPrefix(line, "signal:") {
- // signal: -30 dBm
- parts := strings.Fields(line)
- if len(parts) >= 2 {
- if v, err := strconv.Atoi(parts[1]); err == nil {
- info.signal = v
- }
- }
- } else if strings.HasPrefix(line, "freq:") {
- // freq: 2462
- parts := strings.Fields(line)
- if len(parts) >= 2 {
- if f, err := strconv.Atoi(parts[1]); err == nil {
- info.channel = freqToChannel(f)
- }
- }
- }
- }
- }
- return info
- }
- // freqToChannel converts WiFi frequency (MHz) to channel number
- func freqToChannel(freq int) int {
- if freq >= 2412 && freq <= 2484 {
- if freq == 2484 {
- return 14
- }
- return (freq - 2407) / 5
- }
- if freq >= 5170 && freq <= 5825 {
- return (freq - 5000) / 5
- }
- return 0
- }
- // getMacAddress returns MAC address for given interface
- func getMacAddress(iface string) string {
- addressPath := "/sys/class/net/" + iface + "/address"
- data, err := os.ReadFile(addressPath)
- if err != nil {
- return ""
- }
- return strings.TrimSpace(string(data))
- }
- // applyWiFiSettings configures wpa_supplicant and connects to WiFi
- func applyWiFiSettings(ssid, psk string) error {
- // Retry the whole connection up to 3 times
- var lastErr error
- for attempt := 1; attempt <= 3; attempt++ {
- log.Printf("[wifi] Connection attempt %d/3 to SSID=%s", attempt, ssid)
- if err := tryWiFiConnect(ssid, psk); err != nil {
- lastErr = err
- log.Printf("[wifi] Attempt %d failed: %v", attempt, err)
- if attempt < 3 {
- log.Printf("[wifi] Waiting 5s before retry...")
- time.Sleep(5 * time.Second)
- }
- continue
- }
- log.Println("[wifi] WiFi client connected successfully")
- return nil
- }
- return fmt.Errorf("all connection attempts failed: %w", lastErr)
- }
- func tryWiFiConnect(ssid, psk string) error {
- // Use external shell script for WiFi connection (more reliable)
- scriptPath := "/opt/mybeacon/bin/wifi-connect.sh"
- cmd := exec.Command(scriptPath, ssid, psk)
- output, err := cmd.CombinedOutput()
- if err != nil {
- return fmt.Errorf("wifi-connect.sh failed: %w (output: %s)", err, string(output))
- }
- log.Printf("[wifi] %s", strings.TrimSpace(string(output)))
- // Ensure wlan0 is configured in /etc/network/interfaces (for manual use)
- ensureWlan0Interface()
- return nil
- }
- func ensureWlan0Interface() error {
- interfacesPath := "/etc/network/interfaces"
- data, err := os.ReadFile(interfacesPath)
- if err != nil {
- return err
- }
- content := string(data)
- // Check if wlan0 is already configured (not commented out)
- lines := strings.Split(content, "\n")
- for _, line := range lines {
- trimmed := strings.TrimSpace(line)
- if !strings.HasPrefix(trimmed, "#") && strings.Contains(trimmed, "iface wlan0 inet") {
- return nil // Already configured
- }
- }
- // Add wlan0 configuration
- wlan0Config := `
- # WiFi (managed by beacon-daemon)
- auto wlan0
- iface wlan0 inet dhcp
- pre-up wpa_supplicant -B -i wlan0 -c /etc/wpa_supplicant/wpa_supplicant.conf
- post-down killall wpa_supplicant 2>/dev/null || true
- `
- if err := os.WriteFile(interfacesPath, []byte(content+wlan0Config), 0644); err != nil {
- return err
- }
- log.Println("[wifi] Added wlan0 configuration to /etc/network/interfaces")
- return nil
- }
- // disconnectWiFiClient disconnects from WiFi and stops wpa_supplicant
- func disconnectWiFiClient() {
- log.Println("[wifi] Disconnecting WiFi client...")
- exec.Command("killall", "wpa_supplicant", "udhcpc", "dhcpcd").Run()
- exec.Command("ip", "addr", "flush", "dev", "wlan0").Run()
- os.RemoveAll("/var/run/wpa_supplicant/wlan0")
- log.Println("[wifi] WiFi client disconnected")
- }
|