package probe import ( "context" "database/sql" "net" "net/http" "sync" "time" "github.com/eduard256/strix/internal/api" "github.com/eduard256/strix/internal/app" "github.com/eduard256/strix/pkg/probe" "github.com/rs/zerolog" _ "modernc.org/sqlite" ) const probeTimeout = 100 * time.Millisecond var log zerolog.Logger var db *sql.DB var ports []int var hasICMP bool var detectors []func(*probe.Response) string func Init() { log = app.GetLogger("probe") var err error db, err = sql.Open("sqlite", "file:"+app.DB+"?mode=ro&immutable=1") if err != nil { log.Error().Err(err).Msg("[probe] db open") } ports = loadPorts() hasICMP = probe.CanICMP() if hasICMP { log.Info().Msg("[probe] ICMP available") } else { log.Info().Msg("[probe] ICMP not available, using port scan only") } // HomeKit detector detectors = append(detectors, func(r *probe.Response) string { if r.Probes.MDNS != nil && !r.Probes.MDNS.Paired { if r.Probes.MDNS.Category == "camera" || r.Probes.MDNS.Category == "doorbell" { return "homekit" } } return "" }) api.HandleFunc("api/probe", apiProbe) } func apiProbe(w http.ResponseWriter, r *http.Request) { ip := r.URL.Query().Get("ip") if ip == "" { http.Error(w, "missing ip parameter", http.StatusBadRequest) return } if net.ParseIP(ip) == nil { http.Error(w, "invalid ip: "+ip, http.StatusBadRequest) return } result := runProbe(r.Context(), ip) api.ResponseJSON(w, result) } func runProbe(parent context.Context, ip string) *probe.Response { ctx, cancel := context.WithTimeout(parent, probeTimeout) defer cancel() resp := &probe.Response{IP: ip} var mu sync.Mutex var wg sync.WaitGroup run := func(fn func()) { wg.Add(1) go func() { defer wg.Done() fn() }() } run(func() { r, _ := probe.ScanPorts(ctx, ip, ports) mu.Lock() resp.Probes.Ports = r mu.Unlock() }) run(func() { r, _ := probe.ReverseDNS(ctx, ip) mu.Lock() resp.Probes.DNS = r mu.Unlock() }) run(func() { mac := probe.LookupARP(ip) if mac == "" { return } vendor := probe.LookupOUI(db, mac) mu.Lock() resp.Probes.ARP = &probe.ARPResult{MAC: mac, Vendor: vendor} mu.Unlock() }) run(func() { r, _ := probe.QueryHAP(ctx, ip) mu.Lock() resp.Probes.MDNS = r mu.Unlock() }) run(func() { r, _ := probe.ProbeHTTP(ctx, ip, nil) mu.Lock() resp.Probes.HTTP = r mu.Unlock() }) if hasICMP { run(func() { r, _ := probe.Ping(ctx, ip) mu.Lock() resp.Probes.Ping = r mu.Unlock() }) } wg.Wait() // determine reachable resp.Reachable = resp.Probes.Ports != nil && len(resp.Probes.Ports.Open) > 0 if !resp.Reachable && resp.Probes.Ping != nil { resp.Reachable = true } if resp.Reachable && resp.Probes.Ping != nil { resp.LatencyMs = resp.Probes.Ping.LatencyMs } // determine type resp.Type = "standard" if !resp.Reachable { resp.Type = "unreachable" } else { for _, detect := range detectors { if t := detect(resp); t != "" { resp.Type = t break } } } return resp } func loadPorts() []int { if db == nil { return defaultPorts() } rows, err := db.Query("SELECT DISTINCT port FROM streams WHERE port > 0 UNION SELECT DISTINCT port FROM preset_streams WHERE port > 0") if err != nil { log.Warn().Err(err).Msg("[probe] failed to load ports from db, using defaults") return defaultPorts() } defer rows.Close() var result []int for rows.Next() { var port int if err = rows.Scan(&port); err == nil { result = append(result, port) } } if len(result) == 0 { return defaultPorts() } log.Info().Int("count", len(result)).Msg("[probe] loaded ports from db") return result } func defaultPorts() []int { return []int{554, 80, 8080, 443, 8554, 5544, 10554, 1935, 81, 88, 8090, 8001, 8081, 7070, 7447, 34567} }