api.go 25 KB

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