From 6d87f75b61bb65051716d3b34017272e9f5fc8ff Mon Sep 17 00:00:00 2001 From: ehsan shariati Date: Mon, 9 Dec 2024 18:33:38 -0500 Subject: [PATCH] Ble hotspot (#223) * BLE * Update scan.go * added test server file to run wap server * Update server.go * Update server.go --- wap/pkg/server/server.go | 75 +++++++++--- wap/pkg/wifi/init.go | 2 +- wap/pkg/wifi/scan.go | 251 ++++++++++----------------------------- wap/tests/server/main.go | 28 +++++ 4 files changed, 152 insertions(+), 204 deletions(-) create mode 100644 wap/tests/server/main.go diff --git a/wap/pkg/server/server.go b/wap/pkg/server/server.go index 6517a62a..5ed17a27 100644 --- a/wap/pkg/server/server.go +++ b/wap/pkg/server/server.go @@ -45,6 +45,21 @@ type Config struct { IpniPublisherIdentity string `yaml:"ipniPublisherIdentity"` } +type multiCloser struct { + listeners []io.Closer +} + +// Implement Close method for multiCloser +func (mc *multiCloser) Close() error { + var err error + for _, l := range mc.listeners { + if cerr := l.Close(); cerr != nil { + err = cerr + } + } + return err +} + func checkPathExistAndFileNotExist(path string) string { dir := filepath.Dir(path) @@ -753,16 +768,21 @@ func Serve(peerFn func(clientPeerId string, bloxSeed string) (string, error), ip mux.HandleFunc("/account/id", accountIdHandler) mux.HandleFunc("/account/seed", accountSeedHandler) - listenAddr := "" + mc := &multiCloser{ + listeners: []io.Closer{}, + } + + // Track successful addresses + var successfulAddresses []string + // Try first listener + listenAddr := "" if ip == "" { ip = config.IPADDRESS } - if port == "" { port = config.API_PORT } - listenAddr = ip + ":" + port ln, err := net.Listen("tcp", listenAddr) @@ -771,11 +791,6 @@ func Serve(peerFn func(clientPeerId string, bloxSeed string) (string, error), ip ip, err = getIPFromSpecificNetwork(ctx, config.HOTSPOT_SSID) if err != nil { log.Errorw("Failed to use IP of hotspot", "err", err) - /*ip, err = getNonLoopbackIP() - if err != nil { - log.Errorw("Failed to get non-loopback IP address for serve", "err", err) - ip = "0.0.0.0" - }*/ ip = "0.0.0.0" } listenAddr = ip + ":" + port @@ -784,17 +799,41 @@ func Serve(peerFn func(clientPeerId string, bloxSeed string) (string, error), ip if err != nil { listenAddr = "0.0.0.0:" + port ln, err = net.Listen("tcp", listenAddr) - if err != nil { - log.Errorw("Listen could not initialize for serve", "err", err) - } } } - log.Info("Starting server at " + listenAddr) - go func() { - if err := http.Serve(ln, mux); err != nil { - log.Errorw("Serve could not initialize", "err", err) - } - }() - return ln + if err == nil { + mc.listeners = append(mc.listeners, ln) + successfulAddresses = append(successfulAddresses, listenAddr) + log.Info("Starting server at " + listenAddr) + go func() { + if err := http.Serve(ln, mux); err != nil && !strings.Contains(err.Error(), "use of closed network connection") { + log.Errorw("Serve could not initialize", "err", err) + } + }() + } + + // Try second listener (localhost) + localhostAddr := "127.0.0.1:" + port + ln1, err1 := net.Listen("tcp", localhostAddr) + if err1 == nil { + mc.listeners = append(mc.listeners, ln1) + successfulAddresses = append(successfulAddresses, localhostAddr) + go func() { + if err := http.Serve(ln1, mux); err != nil && !strings.Contains(err.Error(), "use of closed network connection") { + log.Errorw("Serve could not initialize on 127.0.0.1", "err", err) + } + }() + } else { + log.Errorw("Failed to use 127.0.0.1 for serve", "err", err1) + } + + // Print summary of successful listeners + if len(successfulAddresses) > 0 { + log.Infof("Server successfully listening on: %s", strings.Join(successfulAddresses, ", ")) + } else { + log.Error("Failed to start server on any address") + } + + return mc } diff --git a/wap/pkg/wifi/init.go b/wap/pkg/wifi/init.go index b8f9d9a1..b223a5c0 100644 --- a/wap/pkg/wifi/init.go +++ b/wap/pkg/wifi/init.go @@ -15,7 +15,7 @@ var log = logging.Logger("fula/wap/wifi") const maxRetries = 4 -var TimeLimit = 10 * time.Second +var TimeLimit = 25 * time.Second func init() { // Working Directory diff --git a/wap/pkg/wifi/scan.go b/wap/pkg/wifi/scan.go index 214e079e..cec06866 100644 --- a/wap/pkg/wifi/scan.go +++ b/wap/pkg/wifi/scan.go @@ -4,8 +4,7 @@ import ( "bufio" "context" "fmt" - "os/exec" - "runtime" + "sort" "strconv" "strings" ) @@ -89,120 +88,78 @@ func parseDarwin(output string) (wifis []Wifi, err error) { } func parseLinux(output string) (wifis []Wifi, err error) { - // Check which command is available - useIw := false - if _, err := exec.LookPath("iwlist"); err != nil { - if _, err := exec.LookPath("iw"); err != nil { - return nil, fmt.Errorf("neither iwlist nor iw commands found") - } - useIw = true - } + scanner := bufio.NewScanner(strings.NewReader(output)) - if output == "" { - return nil, fmt.Errorf("empty output received") + // Skip header line + if !scanner.Scan() { + return nil, fmt.Errorf("empty output") } - scanner := bufio.NewScanner(strings.NewReader(output)) - w := Wifi{} - wifis = []Wifi{} + // Use map to track strongest signal for each SSID + uniqueWifi := make(map[string]Wifi) - if useIw { - // Parse iw output - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if line == "" { - continue - } + for scanner.Scan() { + line := scanner.Text() + fields := strings.Fields(line) - if strings.Contains(line, "BSS") { - // If we have a complete wifi entry, append it - if w.SSID != "" && w.RSSI != 0 { - // Copy SSID to ESSID if ESSID is empty - if w.ESSID == "" { - w.ESSID = w.SSID - } - wifis = append(wifis, w) - w = Wifi{} // Reset for next entry - } - } else if strings.Contains(line, "SSID:") { - fs := strings.Fields(line) - if len(fs) > 1 { - ssid := strings.Join(fs[1:], " ") - if ssid != "" { - w.SSID = ssid - w.ESSID = ssid // In iw, SSID is what we want for both fields - } - } - } else if strings.Contains(line, "signal:") { - fs := strings.Fields(line) - if len(fs) > 1 { - levelStr := strings.TrimSpace(strings.TrimSuffix(fs[1], " dBm")) - level, errParse := strconv.ParseFloat(levelStr, 64) - if errParse == nil && level < 0 { // Signal strength should be negative - w.RSSI = int(level) // Already in dBm - } - } - } + if len(fields) < 8 { + continue } - // Don't forget the last entry - if w.SSID != "" && w.RSSI != 0 { - if w.ESSID == "" { - w.ESSID = w.SSID - } - wifis = append(wifis, w) + + var startIndex int + if fields[0] == "*" { + startIndex = 1 } - } else { - // Original iwlist parsing logic - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if line == "" { - continue - } - if w.SSID == "" { - if strings.Contains(line, "Address") { - fs := strings.Fields(line) - if len(fs) >= 5 { // Changed from == 5 to >= 5 for more flexibility - w.SSID = strings.ToLower(fs[4]) - } - } else { - continue - } - } else { - if strings.Contains(line, "ESSID") { - parts := strings.Split(line, ":") - if len(parts) > 1 { - essid := strings.Trim(parts[1], "\"") - if essid != "" { - w.ESSID = essid - } - } - } else if strings.Contains(line, "Signal level=") { - parts := strings.Split(line, "level=") - if len(parts) > 1 { - levelParts := strings.Split(parts[1], "/") - if len(levelParts) > 0 { - levelStr := strings.Split(levelParts[0], " dB")[0] - level, errParse := strconv.Atoi(strings.TrimSpace(levelStr)) - if errParse == nil { - if level > 0 { - level = (level / 2) - 100 - } - w.RSSI = level - } - } - } + // Skip networks with "--" as SSID + if fields[startIndex+1] == "--" { + continue + } + + ssid := fields[startIndex+1] + signal, err := strconv.Atoi(fields[startIndex+6]) + if err != nil { + continue + } + + rssi := (signal / 2) - 100 + + // If SSID exists, only update if new signal is stronger + if existing, exists := uniqueWifi[ssid]; exists { + if rssi > existing.RSSI { + uniqueWifi[ssid] = Wifi{ + SSID: ssid, + ESSID: ssid, + RSSI: rssi, } } - if w.SSID != "" && w.RSSI != 0 && w.ESSID != "" { - wifis = append(wifis, w) - w = Wifi{} + } else { + uniqueWifi[ssid] = Wifi{ + SSID: ssid, + ESSID: ssid, + RSSI: rssi, } } } if err := scanner.Err(); err != nil { - return wifis, fmt.Errorf("error scanning output: %v", err) + return nil, fmt.Errorf("error scanning output: %v", err) + } + + // Convert map to slice + wifis = make([]Wifi, 0, len(uniqueWifi)) + for _, wifi := range uniqueWifi { + wifis = append(wifis, wifi) + } + + // Sort by signal strength (RSSI), strongest first + sort.Slice(wifis, func(i, j int) bool { + return wifis[i].RSSI > wifis[j].RSSI + }) + + // Keep only the first 6 records + if len(wifis) > 6 { + wifis = wifis[:6] } return wifis, nil @@ -212,90 +169,14 @@ func parseLinux(output string) (wifis []Wifi, err error) { // If forceReload is set to true it resets the network adapter to make sure it fetches the latest list, otherwise it reads from cache // wifiInterface is the name of interface that it should look for in Linux. func Scan(forceReload bool, wifiInterface ...string) (wifilist []Wifi, err error) { - var command, stdout, stderr string - switch runtime.GOOS { - case "windows": - // Your windows related code - case "darwin": - // Your darwin related code - default: - // Get all available wireless network interfaces - ctx, cl := context.WithTimeout(context.Background(), TimeLimit) - defer cl() - - // Check which command is available - useIw := false - if _, err := exec.LookPath("iwlist"); err != nil { - if _, err := exec.LookPath("iw"); err != nil { - return nil, fmt.Errorf("neither iwlist nor iw commands found") - } - useIw = true - } - - var interfaces []string - if useIw { - // Get interfaces using iw - stdout, stderr, err = runCommand(ctx, "iw dev") - if err != nil { - log.Errorw("failed to run iw dev", "err", err, "stderr", stderr) - return nil, err - } - - // Parse iw output for interfaces - scanner := bufio.NewScanner(strings.NewReader(stdout)) - for scanner.Scan() { - line := scanner.Text() - if strings.Contains(line, "Interface") { - fields := strings.Fields(line) - if len(fields) > 1 { - interfaces = append(interfaces, fields[1]) - } - } - } - } else { - // Use existing logic for iwconfig/iwlist - stdout, stderr, err = runCommand(ctx, "iwconfig") - if err != nil { - log.Errorw("failed to run iwconfig", "err", err, "stderr", stderr) - return nil, err - } - - // Filter output with grep-like functionality - var filteredLines []string - scanner := bufio.NewScanner(strings.NewReader(stdout)) - for scanner.Scan() { - line := scanner.Text() - if len(line) > 0 && (line[0] >= 'a' && line[0] <= 'z' || line[0] >= 'A' && line[0] <= 'Z') { - filteredLines = append(filteredLines, line) - } - } - - // Run awk-like functionality to print the first field of each line - for _, line := range filteredLines { - fields := strings.Fields(line) - if len(fields) > 0 { - interfaces = append(interfaces, fields[0]) - } - } - } + ctx, cl := context.WithTimeout(context.Background(), TimeLimit) + defer cl() - // Loop over interfaces and perform scan - for _, iface := range interfaces { - if useIw { - command = fmt.Sprintf("iw dev %s scan", iface) - } else { - command = fmt.Sprintf("iwlist %s scan", iface) - } - - stdout, _, err = runCommand(ctx, command) - if err == nil { - // Break the loop when the scan command is successful - wifilist, err = parse(stdout, "linux") - if err == nil { - break - } - } - } + stdout, stderr, err := runCommand(ctx, "nmcli dev wifi list --rescan yes") + if err != nil { + log.Errorw("failed to run nmcli scan", "err", err, "stderr", stderr) + return nil, err } - return + + return parse(stdout, "linux") } diff --git a/wap/tests/server/main.go b/wap/tests/server/main.go new file mode 100644 index 00000000..cf7d4c0b --- /dev/null +++ b/wap/tests/server/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/functionland/go-fula/wap/pkg/server" +) + +func main() { + connectedCh := make(chan bool, 1) + + mockPeerFn := func(clientPeerId string, bloxSeed string) (string, error) { + return "test-peer-id", nil + } + + closer := server.Serve(mockPeerFn, "", "", connectedCh) + defer closer.Close() + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + fmt.Println("Server is running. Press Ctrl+C to stop.") + <-sigChan + fmt.Println("\nShutting down server...") +}