diff --git a/internal/probe/probe.go b/internal/probe/probe.go
index 90e7838..63bd0d7 100644
--- a/internal/probe/probe.go
+++ b/internal/probe/probe.go
@@ -33,6 +33,14 @@ func Init() {
}
ports = loadPorts()
+ // ONVIF detector (highest priority -- auto-discovers all streams)
+ detectors = append(detectors, func(r *probe.Response) string {
+ if r.Probes.ONVIF != nil {
+ return "onvif"
+ }
+ return ""
+ })
+
// HomeKit detector
detectors = append(detectors, func(r *probe.Response) string {
if r.Probes.MDNS != nil {
@@ -115,6 +123,12 @@ func runProbe(parent context.Context, ip string) *probe.Response {
resp.Probes.HTTP = r
mu.Unlock()
})
+ run(func() {
+ r, _ := probe.ProbeONVIF(fastCtx, ip)
+ mu.Lock()
+ resp.Probes.ONVIF = r
+ mu.Unlock()
+ })
wg.Wait()
diff --git a/pkg/probe/models.go b/pkg/probe/models.go
index a67e6df..42afc12 100644
--- a/pkg/probe/models.go
+++ b/pkg/probe/models.go
@@ -14,6 +14,7 @@ type Probes struct {
ARP *ARPResult `json:"arp"`
MDNS *MDNSResult `json:"mdns"`
HTTP *HTTPResult `json:"http"`
+ ONVIF *ONVIFResult `json:"onvif"`
}
type PortsResult struct {
@@ -43,3 +44,10 @@ type HTTPResult struct {
StatusCode int `json:"status_code"`
Server string `json:"server"`
}
+
+type ONVIFResult struct {
+ URL string `json:"url"`
+ Port int `json:"port"`
+ Name string `json:"name,omitempty"`
+ Hardware string `json:"hardware,omitempty"`
+}
diff --git a/pkg/probe/onvif.go b/pkg/probe/onvif.go
new file mode 100644
index 0000000..e7c3517
--- /dev/null
+++ b/pkg/probe/onvif.go
@@ -0,0 +1,126 @@
+package probe
+
+import (
+ "context"
+ "crypto/rand"
+ "encoding/hex"
+ "fmt"
+ "net"
+ "net/url"
+ "regexp"
+ "strings"
+ "time"
+)
+
+// ProbeONVIF sends unicast WS-Discovery probe to ip:3702.
+// Returns nil, nil if the device does not support ONVIF.
+func ProbeONVIF(ctx context.Context, ip string) (*ONVIFResult, error) {
+ conn, err := net.ListenPacket("udp4", ":0")
+ if err != nil {
+ return nil, err
+ }
+ defer conn.Close()
+
+ deadline, ok := ctx.Deadline()
+ if !ok {
+ deadline = time.Now().Add(100 * time.Millisecond)
+ }
+ _ = conn.SetDeadline(deadline)
+
+ // WS-Discovery Probe message
+ // https://www.onvif.org/wp-content/uploads/2016/12/ONVIF_Feature_Discovery_Specification_16.07.pdf
+ msg := `
+
+
+ http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe
+ urn:uuid:` + randUUID() + `
+ urn:schemas-xmlsoap-org:ws:2005:04:discovery
+
+
+
+
+
+
+
+`
+
+ addr := &net.UDPAddr{IP: net.ParseIP(ip), Port: 3702}
+ if _, err = conn.WriteTo([]byte(msg), addr); err != nil {
+ return nil, err
+ }
+
+ buf := make([]byte, 8192)
+ for {
+ n, _, err := conn.ReadFrom(buf)
+ if err != nil {
+ return nil, nil // timeout -- device doesn't support ONVIF
+ }
+
+ body := string(buf[:n])
+ if !strings.Contains(body, "onvif") {
+ continue
+ }
+
+ xaddrs := findXMLTag(body, "XAddrs")
+ if xaddrs == "" {
+ continue
+ }
+
+ // fix buggy cameras reporting 0.0.0.0
+ // ex. http://0.0.0.0:8080/onvif/device_service
+ if s, ok := strings.CutPrefix(xaddrs, "http://0.0.0.0"); ok {
+ xaddrs = "http://" + ip + s
+ }
+
+ port := 80
+ if u, err := url.Parse(xaddrs); err == nil && u.Port() != "" {
+ fmt.Sscanf(u.Port(), "%d", &port)
+ }
+
+ scopes := findXMLTag(body, "Scopes")
+
+ return &ONVIFResult{
+ URL: xaddrs,
+ Port: port,
+ Name: findScope(scopes, "onvif://www.onvif.org/name/"),
+ Hardware: findScope(scopes, "onvif://www.onvif.org/hardware/"),
+ }, nil
+ }
+}
+
+// internals
+
+var reXMLTag = map[string]*regexp.Regexp{}
+
+func findXMLTag(s, tag string) string {
+ re, ok := reXMLTag[tag]
+ if !ok {
+ re = regexp.MustCompile(`(?s)<(?:\w+:)?` + tag + `\b[^>]*>([^<]+)`)
+ reXMLTag[tag] = re
+ }
+ m := re.FindStringSubmatch(s)
+ if len(m) != 2 {
+ return ""
+ }
+ return m[1]
+}
+
+func findScope(s, prefix string) string {
+ i := strings.Index(s, prefix)
+ if i < 0 {
+ return ""
+ }
+ s = s[i+len(prefix):]
+ if j := strings.IndexByte(s, ' '); j >= 0 {
+ s = s[:j]
+ }
+ s, _ = url.QueryUnescape(s)
+ return s
+}
+
+func randUUID() string {
+ b := make([]byte, 16)
+ rand.Read(b)
+ s := hex.EncodeToString(b)
+ return s[:8] + "-" + s[8:12] + "-" + s[12:16] + "-" + s[16:20] + "-" + s[20:]
+}
diff --git a/www/homekit.html b/www/homekit.html
index ba5c5b9..390ce77 100644
--- a/www/homekit.html
+++ b/www/homekit.html
@@ -60,13 +60,13 @@
.back-wrapper {
position: absolute; top: 1.5rem;
left: 50%; transform: translateX(-50%);
- width: 100%; max-width: 480px;
+ width: 100%; max-width: 600px;
padding: 0 1.5rem;
z-index: 10;
}
@media (min-width: 768px) {
- .back-wrapper { max-width: 540px; }
+ .back-wrapper { max-width: 660px; }
}
.btn-back {