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,76 @@
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// OUIDatabase provides MAC address prefix to vendor name lookup.
|
||||
// Data is loaded from a JSON file containing camera/surveillance vendor OUI prefixes.
|
||||
type OUIDatabase struct {
|
||||
data map[string]string // "C0:56:E3" -> "Hikvision"
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewOUIDatabase creates an empty OUI database.
|
||||
func NewOUIDatabase() *OUIDatabase {
|
||||
return &OUIDatabase{
|
||||
data: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
// LoadFromFile loads OUI data from a JSON file.
|
||||
// Expected format: {"C0:56:E3": "Hikvision", "54:EF:44": "Lumi/Aqara", ...}
|
||||
func (db *OUIDatabase) LoadFromFile(path string) error {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open OUI database: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var data map[string]string
|
||||
if err := json.NewDecoder(file).Decode(&data); err != nil {
|
||||
return fmt.Errorf("failed to decode OUI database: %w", err)
|
||||
}
|
||||
|
||||
// Normalize all keys to uppercase
|
||||
normalized := make(map[string]string, len(data))
|
||||
for k, v := range data {
|
||||
normalized[strings.ToUpper(k)] = v
|
||||
}
|
||||
|
||||
db.mu.Lock()
|
||||
db.data = normalized
|
||||
db.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LookupVendor returns the vendor name for a given MAC address.
|
||||
// MAC can be in any format: "C0:56:E3:AA:BB:CC", "c0:56:e3:aa:bb:cc", "C0-56-E3-AA-BB-CC".
|
||||
// Returns empty string if not found.
|
||||
func (db *OUIDatabase) LookupVendor(mac string) string {
|
||||
if len(mac) < 8 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Normalize: uppercase and replace dashes with colons
|
||||
prefix := strings.ToUpper(mac[:8])
|
||||
prefix = strings.ReplaceAll(prefix, "-", ":")
|
||||
|
||||
db.mu.RLock()
|
||||
vendor := db.data[prefix]
|
||||
db.mu.RUnlock()
|
||||
|
||||
return vendor
|
||||
}
|
||||
|
||||
// Size returns the number of entries in the database.
|
||||
func (db *OUIDatabase) Size() int {
|
||||
db.mu.RLock()
|
||||
defer db.mu.RUnlock()
|
||||
return len(db.data)
|
||||
}
|
||||
Reference in New Issue
Block a user