Add GET /api/v1/probe endpoint for device inspection

Fast (~1-3s) endpoint that gathers network info about a device
before full stream discovery. Runs ping first, then parallel probes.

Features:
- Ping with ICMP + TCP fallback (works without root)
- Reverse DNS hostname lookup
- ARP table MAC address + OUI vendor identification (2403 entries, 51 camera vendors)
- mDNS HomeKit detection (camera/doorbell, paired status)
- Extensible Prober interface for adding new probe types
- 3-second overall timeout, parallel execution

Response includes "type" field:
- "unreachable" - device not responding
- "standard" - normal IP camera (RTSP/HTTP/ONVIF flow)
- "homekit" - Apple HomeKit camera (PIN pairing flow)
This commit is contained in:
eduard256
2026-03-16 13:57:41 +00:00
parent eb8cc546c8
commit 4d6c2fd878
13 changed files with 3164 additions and 12 deletions
+82
View File
@@ -0,0 +1,82 @@
package handlers
import (
"encoding/json"
"net"
"net/http"
"github.com/eduard256/Strix/internal/camera/discovery"
)
// ProbeHandler handles device probe requests.
// GET /api/v1/probe?ip=192.168.1.50
type ProbeHandler struct {
probeService *discovery.ProbeService
logger interface {
Debug(string, ...any)
Error(string, error, ...any)
Info(string, ...any)
}
}
// NewProbeHandler creates a new probe handler.
func NewProbeHandler(
probeService *discovery.ProbeService,
logger interface {
Debug(string, ...any)
Error(string, error, ...any)
Info(string, ...any)
},
) *ProbeHandler {
return &ProbeHandler{
probeService: probeService,
logger: logger,
}
}
// ServeHTTP handles probe requests.
func (h *ProbeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
ip := r.URL.Query().Get("ip")
if ip == "" {
h.sendError(w, "Missing required parameter: ip", http.StatusBadRequest)
return
}
// Validate IP format
if net.ParseIP(ip) == nil {
h.sendError(w, "Invalid IP address: "+ip, http.StatusBadRequest)
return
}
h.logger.Info("probe requested", "ip", ip, "remote_addr", r.RemoteAddr)
// Run probe
result := h.probeService.Probe(r.Context(), ip)
// Send response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(result); err != nil {
h.logger.Error("failed to encode probe response", err)
}
}
// sendError sends a JSON error response.
func (h *ProbeHandler) sendError(w http.ResponseWriter, message string, statusCode int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
response := map[string]interface{}{
"error": true,
"message": message,
"code": statusCode,
}
_ = json.NewEncoder(w).Encode(response)
}
+21
View File
@@ -20,6 +20,7 @@ type Server struct {
loader *database.Loader
searchEngine *database.SearchEngine
scanner *discovery.Scanner
probeService *discovery.ProbeService
sseServer *sse.Server
logger interface{ Debug(string, ...any); Error(string, error, ...any); Info(string, ...any) }
}
@@ -75,6 +76,22 @@ func NewServer(
// Initialize SSE server
sseServer := sse.NewServer(logger)
// Initialize OUI database for vendor identification
ouiDB := discovery.NewOUIDatabase()
if err := ouiDB.LoadFromFile(cfg.Database.OUIPath); err != nil {
logger.Error("failed to load OUI database, vendor lookup will be unavailable", err)
} else {
logger.Info("OUI database loaded", "entries", ouiDB.Size())
}
// Initialize ProbeService with all probers
probers := []discovery.Prober{
&discovery.DNSProber{},
discovery.NewARPProber(ouiDB),
&discovery.MDNSProber{},
}
probeService := discovery.NewProbeService(probers, logger)
// Create server
server := &Server{
router: chi.NewRouter(),
@@ -82,6 +99,7 @@ func NewServer(
loader: loader,
searchEngine: searchEngine,
scanner: scanner,
probeService: probeService,
sseServer: sseServer,
logger: logger,
}
@@ -127,6 +145,9 @@ func (s *Server) setupRoutes() {
// Stream discovery (SSE)
s.router.Post("/streams/discover", handlers.NewDiscoverHandler(s.scanner, s.sseServer, s.logger).ServeHTTP)
// Device probe (ping + DNS + ARP/OUI + mDNS)
s.router.Get("/probe", handlers.NewProbeHandler(s.probeService, s.logger).ServeHTTP)
}
// ServeHTTP implements http.Handler