api.go 28 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054
  1. package main
  2. import (
  3. "bufio"
  4. "context"
  5. "encoding/hex"
  6. "encoding/json"
  7. "fmt"
  8. "log"
  9. "net/http"
  10. "os"
  11. "os/exec"
  12. "strconv"
  13. "strings"
  14. "sync"
  15. "time"
  16. "github.com/gorilla/websocket"
  17. )
  18. // APIServer handles HTTP API requests
  19. type APIServer struct {
  20. daemon *Daemon
  21. upgrader websocket.Upgrader
  22. // HTTP server
  23. httpServer *http.Server
  24. serverMu sync.Mutex
  25. // Recent events (ring buffer)
  26. recentBLE []interface{}
  27. recentWiFi []interface{}
  28. recentMu sync.RWMutex
  29. // WebSocket clients
  30. wsClients map[*websocket.Conn]bool
  31. wsClientsMu sync.Mutex
  32. // Session management
  33. sessions map[string]time.Time
  34. sessionsMu sync.RWMutex
  35. // Cached CPU metrics (updated in background)
  36. cachedCPUPercent float64
  37. cpuMu sync.RWMutex
  38. }
  39. // StatusResponse is the response for /api/status
  40. type StatusResponse struct {
  41. DeviceID string `json:"device_id"`
  42. Registered bool `json:"registered"`
  43. Mode string `json:"mode"`
  44. Uptime int64 `json:"uptime_sec"`
  45. Network NetworkStatus `json:"network"`
  46. Scanners ScannerStatus `json:"scanners"`
  47. Counters CounterStatus `json:"counters"`
  48. ServerOK bool `json:"server_ok"`
  49. }
  50. type NetworkStatus struct {
  51. Eth0IP string `json:"eth0_ip,omitempty"`
  52. Eth0MAC string `json:"eth0_mac,omitempty"`
  53. Eth0RX int64 `json:"eth0_rx,omitempty"`
  54. Eth0TX int64 `json:"eth0_tx,omitempty"`
  55. Wlan0IP string `json:"wlan0_ip,omitempty"`
  56. Wlan0MAC string `json:"wlan0_mac,omitempty"`
  57. Wlan0SSID string `json:"wlan0_ssid,omitempty"`
  58. Wlan0Signal int `json:"wlan0_signal,omitempty"`
  59. Wlan0Channel int `json:"wlan0_channel,omitempty"`
  60. Wlan0Gateway string `json:"wlan0_gateway,omitempty"`
  61. Wlan0DNS string `json:"wlan0_dns,omitempty"`
  62. Wlan0RX int64 `json:"wlan0_rx,omitempty"`
  63. Wlan0TX int64 `json:"wlan0_tx,omitempty"`
  64. Gateway string `json:"gateway,omitempty"`
  65. DNS string `json:"dns,omitempty"`
  66. NTP string `json:"ntp,omitempty"`
  67. APActive bool `json:"ap_active"`
  68. }
  69. type ScannerStatus struct {
  70. BLERunning bool `json:"ble_running"`
  71. WiFiRunning bool `json:"wifi_running"`
  72. }
  73. type CounterStatus struct {
  74. BLEEvents uint64 `json:"ble_events"`
  75. WiFiEvents uint64 `json:"wifi_events"`
  76. Uploads uint64 `json:"uploads"`
  77. Errors uint64 `json:"errors"`
  78. }
  79. // MetricsResponse is the response for /api/metrics
  80. type MetricsResponse struct {
  81. CPUPercent float64 `json:"cpu_percent"`
  82. MemUsedMB float64 `json:"mem_used_mb"`
  83. MemTotalMB float64 `json:"mem_total_mb"`
  84. Temperature float64 `json:"temperature"`
  85. Load1m float64 `json:"load_1m"`
  86. Load5m float64 `json:"load_5m"`
  87. Load15m float64 `json:"load_15m"`
  88. }
  89. // SettingsRequest is the request for /api/settings
  90. type SettingsRequest struct {
  91. Password string `json:"password"`
  92. Settings SettingsPayload `json:"settings"`
  93. }
  94. // SettingsPayload contains the actual settings
  95. type SettingsPayload struct {
  96. Mode string `json:"mode"`
  97. // BLE Scanner (LAN mode only)
  98. BLEEnabled bool `json:"ble_enabled"`
  99. BLEBatchInterval int `json:"ble_batch_interval_ms"`
  100. BLEEndpoint string `json:"ble_endpoint"`
  101. // WiFi Scanner (LAN mode only)
  102. WiFiMonitorEnabled bool `json:"wifi_monitor_enabled"`
  103. WiFiMonitorBatchInterval int `json:"wifi_monitor_batch_interval_ms"`
  104. WiFiMonitorEndpoint string `json:"wifi_monitor_endpoint"`
  105. // WiFi Client
  106. WiFiClientEnabled bool `json:"wifi_client_enabled"`
  107. WifiSSID string `json:"wifi_ssid"`
  108. WifiPSK string `json:"wifi_psk"`
  109. // eth0
  110. Eth0Mode string `json:"eth0_mode"`
  111. Eth0IP string `json:"eth0_ip"`
  112. Eth0Gateway string `json:"eth0_gateway"`
  113. Eth0DNS string `json:"eth0_dns"`
  114. // NTP
  115. NTPServers string `json:"ntp_servers"`
  116. }
  117. // NewAPIServer creates a new API server
  118. func NewAPIServer(daemon *Daemon) *APIServer {
  119. return &APIServer{
  120. daemon: daemon,
  121. upgrader: websocket.Upgrader{
  122. CheckOrigin: func(r *http.Request) bool {
  123. return true // Allow all origins for local access
  124. },
  125. },
  126. recentBLE: make([]interface{}, 0, 100),
  127. recentWiFi: make([]interface{}, 0, 100),
  128. wsClients: make(map[*websocket.Conn]bool),
  129. sessions: make(map[string]time.Time),
  130. }
  131. }
  132. // Start starts the HTTP server
  133. func (s *APIServer) Start(addr string) error {
  134. s.serverMu.Lock()
  135. defer s.serverMu.Unlock()
  136. if s.httpServer != nil {
  137. return fmt.Errorf("HTTP server already running")
  138. }
  139. // Start CPU monitoring goroutine
  140. go s.updateCPUMetrics()
  141. mux := http.NewServeMux()
  142. // API endpoints
  143. mux.HandleFunc("/api/status", s.handleStatus)
  144. mux.HandleFunc("/api/metrics", s.handleMetrics)
  145. mux.HandleFunc("/api/ble/recent", s.handleBLERecent)
  146. mux.HandleFunc("/api/wifi/recent", s.handleWiFiRecent)
  147. mux.HandleFunc("/api/config", s.handleConfig)
  148. mux.HandleFunc("/api/settings", s.handleSettings)
  149. mux.HandleFunc("/api/unlock", s.handleUnlock)
  150. mux.HandleFunc("/api/logs", s.handleLogs)
  151. mux.HandleFunc("/api/kernel-logs", s.handleKernelLogs)
  152. mux.HandleFunc("/api/ws", s.handleWebSocket)
  153. // Serve static files for dashboard with SPA fallback
  154. mux.Handle("/", s.spaHandler("/opt/mybeacon/www"))
  155. s.httpServer = &http.Server{
  156. Addr: addr,
  157. Handler: s.corsMiddleware(mux),
  158. }
  159. log.Printf("[api] Starting HTTP server on %s", addr)
  160. return s.httpServer.ListenAndServe()
  161. }
  162. // Stop stops the HTTP server
  163. func (s *APIServer) Stop() error {
  164. s.serverMu.Lock()
  165. defer s.serverMu.Unlock()
  166. if s.httpServer == nil {
  167. return nil // Already stopped
  168. }
  169. log.Println("[api] Stopping HTTP server...")
  170. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  171. defer cancel()
  172. err := s.httpServer.Shutdown(ctx)
  173. s.httpServer = nil
  174. return err
  175. }
  176. // IsRunning returns true if HTTP server is running
  177. func (s *APIServer) IsRunning() bool {
  178. s.serverMu.Lock()
  179. defer s.serverMu.Unlock()
  180. return s.httpServer != nil
  181. }
  182. // spaHandler serves static files with SPA fallback
  183. func (s *APIServer) spaHandler(staticPath string) http.Handler {
  184. fileServer := http.FileServer(http.Dir(staticPath))
  185. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  186. // Check if file exists
  187. path := staticPath + r.URL.Path
  188. _, err := os.Stat(path)
  189. // If file doesn't exist and it's not the root, serve index.html (SPA routing)
  190. if os.IsNotExist(err) && r.URL.Path != "/" {
  191. // Serve index.html for SPA routing
  192. http.ServeFile(w, r, staticPath+"/index.html")
  193. return
  194. }
  195. // Serve the file normally
  196. fileServer.ServeHTTP(w, r)
  197. })
  198. }
  199. // corsMiddleware adds CORS headers
  200. func (s *APIServer) corsMiddleware(next http.Handler) http.Handler {
  201. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  202. w.Header().Set("Access-Control-Allow-Origin", "*")
  203. w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
  204. w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
  205. if r.Method == "OPTIONS" {
  206. w.WriteHeader(http.StatusOK)
  207. return
  208. }
  209. next.ServeHTTP(w, r)
  210. })
  211. }
  212. // handleStatus returns device status
  213. func (s *APIServer) handleStatus(w http.ResponseWriter, r *http.Request) {
  214. s.daemon.mu.Lock()
  215. defer s.daemon.mu.Unlock()
  216. eth0rx, eth0tx := getInterfaceStats("eth0")
  217. wlan0rx, wlan0tx := getInterfaceStats("wlan0")
  218. wlanInfo := getWlanInfo()
  219. // Gateway and DNS are shared, but wlan0 might have its own
  220. gateway := getDefaultGateway()
  221. dns := getDNS()
  222. status := StatusResponse{
  223. DeviceID: s.daemon.state.DeviceID,
  224. Registered: s.daemon.state.DeviceToken != "",
  225. Mode: "cloud", // TODO: implement mode switching
  226. Uptime: getUptime(),
  227. Network: NetworkStatus{
  228. Eth0IP: getInterfaceIP("eth0"),
  229. Eth0MAC: getMacAddress("eth0"),
  230. Eth0RX: eth0rx,
  231. Eth0TX: eth0tx,
  232. Wlan0IP: getInterfaceIP("wlan0"),
  233. Wlan0MAC: getMacAddress("wlan0"),
  234. Wlan0SSID: wlanInfo.ssid,
  235. Wlan0Signal: wlanInfo.signal,
  236. Wlan0Channel: wlanInfo.channel,
  237. Wlan0Gateway: gateway, // same gateway for now
  238. Wlan0DNS: dns, // same DNS for now
  239. Wlan0RX: wlan0rx,
  240. Wlan0TX: wlan0tx,
  241. Gateway: gateway,
  242. DNS: dns,
  243. NTP: "pool.ntp.org",
  244. },
  245. Scanners: ScannerStatus{
  246. BLERunning: s.daemon.scanners.IsBLERunning(),
  247. WiFiRunning: s.daemon.scanners.IsWiFiRunning(),
  248. },
  249. ServerOK: s.daemon.state.DeviceToken != "",
  250. }
  251. s.jsonResponse(w, status)
  252. }
  253. // updateCPUMetrics updates CPU metrics in background
  254. func (s *APIServer) updateCPUMetrics() {
  255. for {
  256. cpuPercent := getCPUPercent()
  257. s.cpuMu.Lock()
  258. s.cachedCPUPercent = cpuPercent
  259. s.cpuMu.Unlock()
  260. time.Sleep(2 * time.Second)
  261. }
  262. }
  263. // handleMetrics returns system metrics
  264. func (s *APIServer) handleMetrics(w http.ResponseWriter, r *http.Request) {
  265. load1m, load5m, load15m := getLoadAvg()
  266. s.cpuMu.RLock()
  267. cpuPercent := s.cachedCPUPercent
  268. s.cpuMu.RUnlock()
  269. metrics := MetricsResponse{
  270. CPUPercent: cpuPercent,
  271. MemUsedMB: getMemUsedMB(),
  272. MemTotalMB: getMemTotalMB(),
  273. Temperature: getTemperature(),
  274. Load1m: load1m,
  275. Load5m: load5m,
  276. Load15m: load15m,
  277. }
  278. s.jsonResponse(w, metrics)
  279. }
  280. // handleBLERecent returns recent BLE events
  281. func (s *APIServer) handleBLERecent(w http.ResponseWriter, r *http.Request) {
  282. s.recentMu.RLock()
  283. events := make([]interface{}, len(s.recentBLE))
  284. copy(events, s.recentBLE)
  285. s.recentMu.RUnlock()
  286. s.jsonResponse(w, events)
  287. }
  288. // handleWiFiRecent returns recent WiFi events
  289. func (s *APIServer) handleWiFiRecent(w http.ResponseWriter, r *http.Request) {
  290. s.recentMu.RLock()
  291. events := make([]interface{}, len(s.recentWiFi))
  292. copy(events, s.recentWiFi)
  293. s.recentMu.RUnlock()
  294. s.jsonResponse(w, events)
  295. }
  296. // handleLogs returns recent daemon log lines
  297. func (s *APIServer) handleLogs(w http.ResponseWriter, r *http.Request) {
  298. logFile := "/var/log/mybeacon.log"
  299. data, err := os.ReadFile(logFile)
  300. if err != nil {
  301. s.jsonResponse(w, []string{})
  302. return
  303. }
  304. lines := strings.Split(string(data), "\n")
  305. // Return last 500 lines
  306. if len(lines) > 500 {
  307. lines = lines[len(lines)-500:]
  308. }
  309. // Filter out empty lines
  310. var result []string
  311. for _, line := range lines {
  312. if strings.TrimSpace(line) != "" {
  313. result = append(result, line)
  314. }
  315. }
  316. s.jsonResponse(w, result)
  317. }
  318. // handleKernelLogs returns recent kernel log lines from dmesg
  319. func (s *APIServer) handleKernelLogs(w http.ResponseWriter, r *http.Request) {
  320. // Run dmesg command
  321. cmd := exec.Command("dmesg", "-T")
  322. output, err := cmd.CombinedOutput()
  323. if err != nil {
  324. // Try without -T flag (human-readable timestamps) if not supported
  325. cmd = exec.Command("dmesg")
  326. output, err = cmd.CombinedOutput()
  327. if err != nil {
  328. s.jsonResponse(w, []string{})
  329. return
  330. }
  331. }
  332. lines := strings.Split(string(output), "\n")
  333. // Return last 500 lines
  334. if len(lines) > 500 {
  335. lines = lines[len(lines)-500:]
  336. }
  337. // Filter out empty lines
  338. var result []string
  339. for _, line := range lines {
  340. if strings.TrimSpace(line) != "" {
  341. result = append(result, line)
  342. }
  343. }
  344. s.jsonResponse(w, result)
  345. }
  346. // handleConfig returns current config (without secrets)
  347. func (s *APIServer) handleConfig(w http.ResponseWriter, r *http.Request) {
  348. s.daemon.mu.Lock()
  349. cfg := *s.daemon.cfg
  350. s.daemon.mu.Unlock()
  351. // Remove secrets
  352. cfg.SSHTunnel.KeyPath = ""
  353. s.jsonResponse(w, cfg)
  354. }
  355. // handleSettings handles settings updates (requires password)
  356. func (s *APIServer) handleSettings(w http.ResponseWriter, r *http.Request) {
  357. if r.Method != "POST" {
  358. http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
  359. return
  360. }
  361. var req SettingsRequest
  362. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  363. http.Error(w, "Invalid JSON", http.StatusBadRequest)
  364. return
  365. }
  366. // Verify password
  367. if !s.verifyPassword(req.Password) {
  368. http.Error(w, "Invalid password", http.StatusUnauthorized)
  369. return
  370. }
  371. s.daemon.mu.Lock()
  372. // Apply mode change
  373. if req.Settings.Mode != "" && (req.Settings.Mode == "cloud" || req.Settings.Mode == "lan") {
  374. if s.daemon.cfg.Mode != req.Settings.Mode {
  375. log.Printf("[api] Mode changed: %s -> %s", s.daemon.cfg.Mode, req.Settings.Mode)
  376. s.daemon.cfg.Mode = req.Settings.Mode
  377. }
  378. }
  379. // In LAN mode: apply scanner settings
  380. if s.daemon.cfg.Mode == "lan" {
  381. // BLE Scanner
  382. s.daemon.cfg.BLE.Enabled = req.Settings.BLEEnabled
  383. if req.Settings.BLEBatchInterval > 0 {
  384. s.daemon.cfg.BLE.BatchIntervalMs = req.Settings.BLEBatchInterval
  385. }
  386. // Store custom BLE upload endpoint for LAN mode
  387. s.daemon.cfg.BLE.UploadEndpoint = req.Settings.BLEEndpoint
  388. log.Printf("[api] BLE scanner (LAN): enabled=%v, interval=%d, endpoint=%s", req.Settings.BLEEnabled, req.Settings.BLEBatchInterval, req.Settings.BLEEndpoint)
  389. // WiFi Scanner
  390. s.daemon.cfg.WiFi.MonitorEnabled = req.Settings.WiFiMonitorEnabled
  391. if req.Settings.WiFiMonitorBatchInterval > 0 {
  392. s.daemon.cfg.WiFi.BatchIntervalMs = req.Settings.WiFiMonitorBatchInterval
  393. }
  394. // Store custom WiFi upload endpoint for LAN mode
  395. s.daemon.cfg.WiFi.UploadEndpoint = req.Settings.WiFiMonitorEndpoint
  396. log.Printf("[api] WiFi scanner (LAN): enabled=%v, interval=%d, endpoint=%s", req.Settings.WiFiMonitorEnabled, req.Settings.WiFiMonitorBatchInterval, req.Settings.WiFiMonitorEndpoint)
  397. // WiFi Client enabled flag
  398. s.daemon.cfg.WiFi.ClientEnabled = req.Settings.WiFiClientEnabled
  399. }
  400. // Apply WiFi Client settings (both modes)
  401. if req.Settings.WifiSSID != "" {
  402. s.daemon.cfg.WiFi.SSID = req.Settings.WifiSSID
  403. s.daemon.cfg.WiFi.PSK = req.Settings.WifiPSK
  404. log.Printf("[api] WiFi client: ssid=%s", req.Settings.WifiSSID)
  405. }
  406. // Apply NTP settings
  407. if req.Settings.NTPServers != "" {
  408. servers := strings.Split(req.Settings.NTPServers, ",")
  409. for i := range servers {
  410. servers[i] = strings.TrimSpace(servers[i])
  411. }
  412. s.daemon.cfg.Network.NTPServers = servers
  413. log.Printf("[api] NTP servers updated: %v", servers)
  414. }
  415. // Apply eth0 settings (always local, never from server)
  416. if req.Settings.Eth0Mode == "static" {
  417. s.daemon.cfg.Network.Eth0.Static = true
  418. s.daemon.cfg.Network.Eth0.Address = req.Settings.Eth0IP
  419. s.daemon.cfg.Network.Eth0.Gateway = req.Settings.Eth0Gateway
  420. s.daemon.cfg.Network.Eth0.DNS = req.Settings.Eth0DNS
  421. log.Printf("[api] eth0 static IP configured: %s", req.Settings.Eth0IP)
  422. } else if req.Settings.Eth0Mode == "dhcp" {
  423. s.daemon.cfg.Network.Eth0.Static = false
  424. log.Printf("[api] eth0 set to DHCP")
  425. }
  426. // Save local config
  427. SaveConfig(s.daemon.configPath, s.daemon.cfg)
  428. // In Cloud Mode: send WiFi credentials to server for centralized management
  429. if s.daemon.cfg.Mode == "cloud" && req.Settings.WifiSSID != "" {
  430. s.daemon.mu.Unlock()
  431. if err := s.daemon.updateWiFiCredentials(req.Settings.WifiSSID, req.Settings.WifiPSK); err != nil {
  432. log.Printf("[api] Failed to update WiFi credentials on server: %v", err)
  433. // Don't fail the request - local settings are already saved
  434. } else {
  435. log.Printf("[api] WiFi credentials updated on server")
  436. }
  437. s.daemon.mu.Lock()
  438. }
  439. s.daemon.mu.Unlock()
  440. // Apply WiFi client connection if credentials provided
  441. if req.Settings.WifiSSID != "" {
  442. if err := applyWiFiSettings(req.Settings.WifiSSID, req.Settings.WifiPSK); err != nil {
  443. log.Printf("[api] Failed to apply WiFi settings: %v", err)
  444. http.Error(w, "Failed to apply WiFi settings: "+err.Error(), http.StatusInternalServerError)
  445. return
  446. }
  447. log.Printf("[api] WiFi client connection initiated: SSID=%s", req.Settings.WifiSSID)
  448. }
  449. s.jsonResponse(w, map[string]bool{"success": true})
  450. }
  451. // handleUnlock verifies password and creates session
  452. func (s *APIServer) handleUnlock(w http.ResponseWriter, r *http.Request) {
  453. if r.Method != "POST" {
  454. http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
  455. return
  456. }
  457. var req struct {
  458. Password string `json:"password"`
  459. }
  460. if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  461. http.Error(w, "Invalid JSON", http.StatusBadRequest)
  462. return
  463. }
  464. if !s.verifyPassword(req.Password) {
  465. http.Error(w, "Invalid password", http.StatusUnauthorized)
  466. return
  467. }
  468. // Create session token (simple implementation)
  469. token := generateToken(32)
  470. s.sessionsMu.Lock()
  471. s.sessions[token] = time.Now().Add(30 * time.Minute)
  472. s.sessionsMu.Unlock()
  473. s.jsonResponse(w, map[string]string{"token": token})
  474. }
  475. // handleWebSocket handles WebSocket connections for live updates
  476. func (s *APIServer) handleWebSocket(w http.ResponseWriter, r *http.Request) {
  477. conn, err := s.upgrader.Upgrade(w, r, nil)
  478. if err != nil {
  479. log.Printf("[api] WebSocket upgrade error: %v", err)
  480. return
  481. }
  482. defer conn.Close()
  483. s.wsClientsMu.Lock()
  484. s.wsClients[conn] = true
  485. s.wsClientsMu.Unlock()
  486. defer func() {
  487. s.wsClientsMu.Lock()
  488. delete(s.wsClients, conn)
  489. s.wsClientsMu.Unlock()
  490. }()
  491. // Keep connection alive and read messages
  492. for {
  493. _, _, err := conn.ReadMessage()
  494. if err != nil {
  495. break
  496. }
  497. }
  498. }
  499. // AddBLEEvent adds a BLE event to recent list and broadcasts to WebSocket
  500. func (s *APIServer) AddBLEEvent(event interface{}) {
  501. s.recentMu.Lock()
  502. s.recentBLE = append(s.recentBLE, event)
  503. if len(s.recentBLE) > 100 {
  504. // Copy to new slice to allow GC of old backing array
  505. newSlice := make([]interface{}, 100)
  506. copy(newSlice, s.recentBLE[len(s.recentBLE)-100:])
  507. s.recentBLE = newSlice
  508. }
  509. s.recentMu.Unlock()
  510. // Broadcast asynchronously to avoid blocking the event loop
  511. go s.broadcast(map[string]interface{}{
  512. "type": "ble",
  513. "event": event,
  514. })
  515. }
  516. // AddWiFiEvent adds a WiFi event to recent list and broadcasts to WebSocket
  517. func (s *APIServer) AddWiFiEvent(event interface{}) {
  518. s.recentMu.Lock()
  519. s.recentWiFi = append(s.recentWiFi, event)
  520. if len(s.recentWiFi) > 100 {
  521. // Copy to new slice to allow GC of old backing array
  522. newSlice := make([]interface{}, 100)
  523. copy(newSlice, s.recentWiFi[len(s.recentWiFi)-100:])
  524. s.recentWiFi = newSlice
  525. }
  526. s.recentMu.Unlock()
  527. // Broadcast asynchronously to avoid blocking the event loop
  528. go s.broadcast(map[string]interface{}{
  529. "type": "wifi",
  530. "event": event,
  531. })
  532. }
  533. // broadcast sends a message to all WebSocket clients
  534. func (s *APIServer) broadcast(msg interface{}) {
  535. data, err := json.Marshal(msg)
  536. if err != nil {
  537. return
  538. }
  539. s.wsClientsMu.Lock()
  540. defer s.wsClientsMu.Unlock()
  541. for conn := range s.wsClients {
  542. if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
  543. conn.Close()
  544. delete(s.wsClients, conn)
  545. }
  546. }
  547. }
  548. // verifyPassword checks if password matches device password
  549. func (s *APIServer) verifyPassword(password string) bool {
  550. s.daemon.mu.Lock()
  551. devicePassword := s.daemon.state.DevicePassword
  552. s.daemon.mu.Unlock()
  553. // Use default password "admin" if no password set (device not registered)
  554. if devicePassword == "" {
  555. devicePassword = "admin"
  556. }
  557. return password != "" && password == devicePassword
  558. }
  559. // jsonResponse sends a JSON response
  560. func (s *APIServer) jsonResponse(w http.ResponseWriter, data interface{}) {
  561. w.Header().Set("Content-Type", "application/json")
  562. json.NewEncoder(w).Encode(data)
  563. }
  564. // Helper functions for system metrics
  565. func getUptime() int64 {
  566. data, err := os.ReadFile("/proc/uptime")
  567. if err != nil {
  568. return 0
  569. }
  570. var uptime float64
  571. if err := parseFloat(string(data), &uptime); err != nil {
  572. return 0
  573. }
  574. return int64(uptime)
  575. }
  576. func parseFloat(s string, f *float64) error {
  577. parts := strings.Fields(s)
  578. if len(parts) == 0 {
  579. return nil
  580. }
  581. return json.Unmarshal([]byte(parts[0]), f)
  582. }
  583. func getCPUPercent() float64 {
  584. // Read CPU stats from /proc/stat twice with 100ms interval
  585. stat1 := readCPUStat()
  586. time.Sleep(100 * time.Millisecond)
  587. stat2 := readCPUStat()
  588. if stat1 == nil || stat2 == nil {
  589. return 0
  590. }
  591. // Calculate totals (sum all fields)
  592. var total1, total2, idle1, idle2 int64
  593. for i := 0; i < len(stat1) && i < 10; i++ {
  594. total1 += stat1[i]
  595. if i < len(stat2) {
  596. total2 += stat2[i]
  597. }
  598. }
  599. // Idle = idle + iowait (fields 3 and 4)
  600. if len(stat1) >= 5 {
  601. idle1 = stat1[3] + stat1[4]
  602. }
  603. if len(stat2) >= 5 {
  604. idle2 = stat2[3] + stat2[4]
  605. }
  606. totalDelta := total2 - total1
  607. idleDelta := idle2 - idle1
  608. if totalDelta == 0 {
  609. return 0
  610. }
  611. cpuPercent := 100.0 * (1.0 - float64(idleDelta)/float64(totalDelta))
  612. return cpuPercent
  613. }
  614. func readCPUStat() []int64 {
  615. data, err := os.ReadFile("/proc/stat")
  616. if err != nil {
  617. return nil
  618. }
  619. // First line: cpu user nice system idle iowait irq softirq steal guest guest_nice
  620. scanner := bufio.NewScanner(strings.NewReader(string(data)))
  621. if !scanner.Scan() {
  622. return nil
  623. }
  624. line := scanner.Text()
  625. if !strings.HasPrefix(line, "cpu ") {
  626. return nil
  627. }
  628. fields := strings.Fields(line)
  629. if len(fields) < 5 {
  630. return nil
  631. }
  632. var stats []int64
  633. // Read all available fields (up to 10)
  634. for i := 1; i < len(fields) && i < 11; i++ {
  635. var v int64
  636. json.Unmarshal([]byte(fields[i]), &v)
  637. stats = append(stats, v)
  638. }
  639. return stats
  640. }
  641. func getMemUsedMB() float64 {
  642. data, err := os.ReadFile("/proc/meminfo")
  643. if err != nil {
  644. return 0
  645. }
  646. var memTotal, memAvailable int64
  647. scanner := bufio.NewScanner(strings.NewReader(string(data)))
  648. for scanner.Scan() {
  649. line := scanner.Text()
  650. if strings.HasPrefix(line, "MemTotal:") {
  651. parseMemInfo(line, &memTotal)
  652. } else if strings.HasPrefix(line, "MemAvailable:") {
  653. parseMemInfo(line, &memAvailable)
  654. }
  655. }
  656. if memTotal == 0 {
  657. return 0
  658. }
  659. memUsed := memTotal - memAvailable
  660. return float64(memUsed) / 1024 // Convert KB to MB
  661. }
  662. func getMemTotalMB() float64 {
  663. data, err := os.ReadFile("/proc/meminfo")
  664. if err != nil {
  665. return 0
  666. }
  667. scanner := bufio.NewScanner(strings.NewReader(string(data)))
  668. for scanner.Scan() {
  669. line := scanner.Text()
  670. if strings.HasPrefix(line, "MemTotal:") {
  671. var total int64
  672. if err := parseMemInfo(line, &total); err == nil {
  673. return float64(total) / 1024
  674. }
  675. }
  676. }
  677. return 0
  678. }
  679. func parseMemInfo(line string, value *int64) error {
  680. parts := strings.Fields(line)
  681. if len(parts) >= 2 {
  682. return json.Unmarshal([]byte(parts[1]), value)
  683. }
  684. return nil
  685. }
  686. func getTemperature() float64 {
  687. data, err := os.ReadFile("/sys/class/thermal/thermal_zone0/temp")
  688. if err != nil {
  689. return 0
  690. }
  691. var temp int64
  692. if err := json.Unmarshal([]byte(strings.TrimSpace(string(data))), &temp); err != nil {
  693. return 0
  694. }
  695. return float64(temp) / 1000
  696. }
  697. func getLoadAvg() (float64, float64, float64) {
  698. data, err := os.ReadFile("/proc/loadavg")
  699. if err != nil {
  700. return 0, 0, 0
  701. }
  702. // /proc/loadavg format: "0.10 0.40 0.49 1/98 2452"
  703. parts := strings.Fields(string(data))
  704. if len(parts) < 3 {
  705. return 0, 0, 0
  706. }
  707. var load1m, load5m, load15m float64
  708. json.Unmarshal([]byte(parts[0]), &load1m)
  709. json.Unmarshal([]byte(parts[1]), &load5m)
  710. json.Unmarshal([]byte(parts[2]), &load15m)
  711. return load1m, load5m, load15m
  712. }
  713. func generateToken(length int) string {
  714. const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
  715. b := make([]byte, length)
  716. for i := range b {
  717. b[i] = chars[time.Now().UnixNano()%int64(len(chars))]
  718. time.Sleep(time.Nanosecond)
  719. }
  720. return string(b)
  721. }
  722. func getInterfaceStats(name string) (rx, tx int64) {
  723. rxPath := "/sys/class/net/" + name + "/statistics/rx_bytes"
  724. txPath := "/sys/class/net/" + name + "/statistics/tx_bytes"
  725. if data, err := os.ReadFile(rxPath); err == nil {
  726. json.Unmarshal([]byte(strings.TrimSpace(string(data))), &rx)
  727. }
  728. if data, err := os.ReadFile(txPath); err == nil {
  729. json.Unmarshal([]byte(strings.TrimSpace(string(data))), &tx)
  730. }
  731. return
  732. }
  733. func getDefaultGateway() string {
  734. data, err := os.ReadFile("/proc/net/route")
  735. if err != nil {
  736. return ""
  737. }
  738. scanner := bufio.NewScanner(strings.NewReader(string(data)))
  739. for scanner.Scan() {
  740. fields := strings.Fields(scanner.Text())
  741. if len(fields) >= 3 && fields[1] == "00000000" {
  742. gw := fields[2]
  743. if len(gw) == 8 {
  744. b, _ := hex.DecodeString(gw)
  745. if len(b) == 4 {
  746. // Little-endian: reverse bytes
  747. return fmt.Sprintf("%d.%d.%d.%d", b[3], b[2], b[1], b[0])
  748. }
  749. }
  750. }
  751. }
  752. return ""
  753. }
  754. func getDNS() string {
  755. data, err := os.ReadFile("/etc/resolv.conf")
  756. if err != nil {
  757. return ""
  758. }
  759. var servers []string
  760. scanner := bufio.NewScanner(strings.NewReader(string(data)))
  761. for scanner.Scan() {
  762. line := scanner.Text()
  763. if strings.HasPrefix(line, "nameserver ") {
  764. servers = append(servers, strings.TrimPrefix(line, "nameserver "))
  765. }
  766. }
  767. return strings.Join(servers, ", ")
  768. }
  769. type wlanInfoResult struct {
  770. ssid string
  771. signal int
  772. channel int
  773. }
  774. func getWlanInfo() wlanInfoResult {
  775. var info wlanInfoResult
  776. // Get SSID, signal, channel from iw dev wlan0 link
  777. out, err := exec.Command("iw", "dev", "wlan0", "link").Output()
  778. if err == nil {
  779. scanner := bufio.NewScanner(strings.NewReader(string(out)))
  780. for scanner.Scan() {
  781. line := strings.TrimSpace(scanner.Text())
  782. if strings.HasPrefix(line, "SSID:") {
  783. info.ssid = strings.TrimSpace(strings.TrimPrefix(line, "SSID:"))
  784. } else if strings.HasPrefix(line, "signal:") {
  785. // signal: -30 dBm
  786. parts := strings.Fields(line)
  787. if len(parts) >= 2 {
  788. if v, err := strconv.Atoi(parts[1]); err == nil {
  789. info.signal = v
  790. }
  791. }
  792. } else if strings.HasPrefix(line, "freq:") {
  793. // freq: 2462
  794. parts := strings.Fields(line)
  795. if len(parts) >= 2 {
  796. if f, err := strconv.Atoi(parts[1]); err == nil {
  797. info.channel = freqToChannel(f)
  798. }
  799. }
  800. }
  801. }
  802. }
  803. return info
  804. }
  805. // freqToChannel converts WiFi frequency (MHz) to channel number
  806. func freqToChannel(freq int) int {
  807. if freq >= 2412 && freq <= 2484 {
  808. if freq == 2484 {
  809. return 14
  810. }
  811. return (freq - 2407) / 5
  812. }
  813. if freq >= 5170 && freq <= 5825 {
  814. return (freq - 5000) / 5
  815. }
  816. return 0
  817. }
  818. // getMacAddress returns MAC address for given interface
  819. func getMacAddress(iface string) string {
  820. addressPath := "/sys/class/net/" + iface + "/address"
  821. data, err := os.ReadFile(addressPath)
  822. if err != nil {
  823. return ""
  824. }
  825. return strings.TrimSpace(string(data))
  826. }
  827. // applyWiFiSettings configures wpa_supplicant and connects to WiFi
  828. func applyWiFiSettings(ssid, psk string) error {
  829. // Retry the whole connection up to 3 times
  830. var lastErr error
  831. for attempt := 1; attempt <= 3; attempt++ {
  832. log.Printf("[wifi] Connection attempt %d/3 to SSID=%s", attempt, ssid)
  833. if err := tryWiFiConnect(ssid, psk); err != nil {
  834. lastErr = err
  835. log.Printf("[wifi] Attempt %d failed: %v", attempt, err)
  836. if attempt < 3 {
  837. log.Printf("[wifi] Waiting 5s before retry...")
  838. time.Sleep(5 * time.Second)
  839. }
  840. continue
  841. }
  842. log.Println("[wifi] WiFi client connected successfully")
  843. return nil
  844. }
  845. return fmt.Errorf("all connection attempts failed: %w", lastErr)
  846. }
  847. func tryWiFiConnect(ssid, psk string) error {
  848. // Use external shell script for WiFi connection (more reliable)
  849. scriptPath := "/opt/mybeacon/bin/wifi-connect.sh"
  850. cmd := exec.Command(scriptPath, ssid, psk)
  851. output, err := cmd.CombinedOutput()
  852. if err != nil {
  853. return fmt.Errorf("wifi-connect.sh failed: %w (output: %s)", err, string(output))
  854. }
  855. log.Printf("[wifi] %s", strings.TrimSpace(string(output)))
  856. // Ensure wlan0 is configured in /etc/network/interfaces (for manual use)
  857. ensureWlan0Interface()
  858. return nil
  859. }
  860. func ensureWlan0Interface() error {
  861. interfacesPath := "/etc/network/interfaces"
  862. data, err := os.ReadFile(interfacesPath)
  863. if err != nil {
  864. return err
  865. }
  866. content := string(data)
  867. // Check if wlan0 is already configured (not commented out)
  868. lines := strings.Split(content, "\n")
  869. for _, line := range lines {
  870. trimmed := strings.TrimSpace(line)
  871. if !strings.HasPrefix(trimmed, "#") && strings.Contains(trimmed, "iface wlan0 inet") {
  872. return nil // Already configured
  873. }
  874. }
  875. // Add wlan0 configuration
  876. wlan0Config := `
  877. # WiFi (managed by beacon-daemon)
  878. auto wlan0
  879. iface wlan0 inet dhcp
  880. pre-up wpa_supplicant -B -i wlan0 -c /etc/wpa_supplicant/wpa_supplicant.conf
  881. post-down killall wpa_supplicant 2>/dev/null || true
  882. `
  883. if err := os.WriteFile(interfacesPath, []byte(content+wlan0Config), 0644); err != nil {
  884. return err
  885. }
  886. log.Println("[wifi] Added wlan0 configuration to /etc/network/interfaces")
  887. return nil
  888. }
  889. // disconnectWiFiClient disconnects from WiFi and stops wpa_supplicant
  890. func disconnectWiFiClient() {
  891. log.Println("[wifi] Disconnecting WiFi client...")
  892. exec.Command("killall", "wpa_supplicant", "udhcpc", "dhcpcd").Run()
  893. exec.Command("ip", "addr", "flush", "dev", "wlan0").Run()
  894. os.RemoveAll("/var/run/wpa_supplicant/wlan0")
  895. log.Println("[wifi] WiFi client disconnected")
  896. }