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:
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user