api.go 20 KB

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