mirror of
https://github.com/eduard256/Strix
synced 2026-04-21 13:37:27 +00:00
Add ONVIF probe detector via unicast WS-Discovery
- Add ProbeONVIF() prober: sends unicast WS-Discovery to ip:3702, parses XAddrs, Name, Hardware from response (no auth needed) - Add ONVIFResult struct to probe models - Register ONVIF detector with highest priority (before HomeKit) - Fix homekit.html back-wrapper max-width to match design system
This commit is contained in:
parent
1291e6a5b6
commit
5be8d4aa00
4 changed files with 150 additions and 2 deletions
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
126
pkg/probe/onvif.go
Normal file
126
pkg/probe/onvif.go
Normal file
|
|
@ -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 := `<?xml version="1.0" ?>
|
||||
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
|
||||
<s:Header xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing">
|
||||
<a:Action>http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</a:Action>
|
||||
<a:MessageID>urn:uuid:` + randUUID() + `</a:MessageID>
|
||||
<a:To>urn:schemas-xmlsoap-org:ws:2005:04:discovery</a:To>
|
||||
</s:Header>
|
||||
<s:Body>
|
||||
<d:Probe xmlns:d="http://schemas.xmlsoap.org/ws/2005/04/discovery">
|
||||
<d:Types />
|
||||
<d:Scopes />
|
||||
</d:Probe>
|
||||
</s:Body>
|
||||
</s:Envelope>`
|
||||
|
||||
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. <wsdd:XAddrs>http://0.0.0.0:8080/onvif/device_service</wsdd:XAddrs>
|
||||
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:]
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue