Files
onvif-go/discovery/discovery.go
T
ProtoTess b4e4982876 Refactor XML response handling in device extended and security tests
- Adjusted formatting in XML response strings for consistency in device_extended_test.go and device_security_test.go.
- Improved readability by aligning XML declaration and body content.
- Updated mock server responses to ensure proper handling of various ONVIF operations.

Enhance device security and storage handling

- Refactored struct field declarations in device_security.go and device_storage_test.go for improved clarity.
- Ensured consistent formatting across struct definitions and XML tags.

Standardize whitespace and formatting across multiple files

- Removed unnecessary blank lines and adjusted indentation in discovery, imaging, media, and PTZ server files.
- Improved overall code readability and maintainability by ensuring consistent formatting.

Update example applications for better readability

- Cleaned up whitespace in example applications to enhance code clarity.
- Ensured consistent formatting in main.go files across various examples.

Refactor server and SOAP handler code for consistency

- Standardized struct field declarations and XML tag formatting in server and SOAP handler files.
- Improved readability by aligning struct fields and ensuring consistent use of whitespace.

General code cleanup and formatting adjustments

- Applied consistent formatting across various files, including types.go and test files.
- Enhanced readability by aligning struct fields and removing unnecessary blank lines.
2025-12-01 00:49:36 +00:00

357 lines
9.4 KiB
Go

package discovery
import (
"context"
"encoding/xml"
"fmt"
"net"
"strings"
"time"
)
const (
// WS-Discovery multicast address
multicastAddr = "239.255.255.250:3702"
// WS-Discovery probe message
probeTemplate = `<?xml version="1.0" encoding="UTF-8"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing">
<s:Header>
<a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</a:Action>
<a:MessageID>uuid:%s</a:MessageID>
<a:ReplyTo>
<a:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address>
</a:ReplyTo>
<a:To s:mustUnderstand="1">urn:schemas-xmlsoap-org:ws:2005:04:discovery</a:To>
</s:Header>
<s:Body>
<Probe xmlns="http://schemas.xmlsoap.org/ws/2005/04/discovery">
<d:Types xmlns:d="http://schemas.xmlsoap.org/ws/2005/04/discovery" xmlns:dp0="http://www.onvif.org/ver10/network/wsdl">dp0:NetworkVideoTransmitter</d:Types>
</Probe>
</s:Body>
</s:Envelope>`
)
// Device represents a discovered ONVIF device
type Device struct {
// Device endpoint address
EndpointRef string
// XAddrs contains the device service addresses
XAddrs []string
// Types contains the device types
Types []string
// Scopes contains the device scopes (name, location, etc.)
Scopes []string
// Metadata version
MetadataVersion int
}
// ProbeMatch represents a WS-Discovery probe match
type ProbeMatch struct {
XMLName xml.Name `xml:"ProbeMatch"`
EndpointRef string `xml:"EndpointReference>Address"`
Types string `xml:"Types"`
Scopes string `xml:"Scopes"`
XAddrs string `xml:"XAddrs"`
MetadataVersion int `xml:"MetadataVersion"`
}
// ProbeMatches represents WS-Discovery probe matches
type ProbeMatches struct {
XMLName xml.Name `xml:"ProbeMatches"`
ProbeMatch []ProbeMatch `xml:"ProbeMatch"`
}
// DiscoverOptions contains options for device discovery
type DiscoverOptions struct {
// NetworkInterface specifies the network interface to use for multicast.
// If empty, the system will choose the default interface.
// Examples: "eth0", "wlan0", "192.168.1.100"
NetworkInterface string
// Context and timeout are handled by the caller
}
// Discover discovers ONVIF devices on the network
// For advanced options like specifying a network interface, use DiscoverWithOptions
func Discover(ctx context.Context, timeout time.Duration) ([]*Device, error) {
return DiscoverWithOptions(ctx, timeout, &DiscoverOptions{})
}
// DiscoverWithOptions discovers ONVIF devices with custom options
func DiscoverWithOptions(ctx context.Context, timeout time.Duration, opts *DiscoverOptions) ([]*Device, error) {
if opts == nil {
opts = &DiscoverOptions{}
}
// Create UDP connection for multicast
addr, err := net.ResolveUDPAddr("udp", multicastAddr)
if err != nil {
return nil, fmt.Errorf("failed to resolve multicast address: %w", err)
}
// Get the network interface to use
var iface *net.Interface
if opts.NetworkInterface != "" {
iface, err = resolveNetworkInterface(opts.NetworkInterface)
if err != nil {
return nil, fmt.Errorf("failed to resolve network interface: %w", err)
}
}
conn, err := net.ListenMulticastUDP("udp", iface, addr)
if err != nil {
return nil, fmt.Errorf("failed to listen on multicast address: %w", err)
}
defer func() { _ = conn.Close() }()
// Set read deadline
if err := conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
return nil, fmt.Errorf("failed to set read deadline: %w", err)
}
// Generate message ID
messageID := generateUUID()
// Send probe message
probeMsg := fmt.Sprintf(probeTemplate, messageID)
if _, err := conn.WriteToUDP([]byte(probeMsg), addr); err != nil {
return nil, fmt.Errorf("failed to send probe message: %w", err)
}
// Collect responses
devices := make(map[string]*Device)
buffer := make([]byte, 8192)
// Read responses until timeout or context cancellation
for {
select {
case <-ctx.Done():
return deviceMapToSlice(devices), ctx.Err()
default:
n, _, err := conn.ReadFromUDP(buffer)
if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
// Timeout reached, return collected devices
return deviceMapToSlice(devices), nil
}
return deviceMapToSlice(devices), fmt.Errorf("failed to read UDP response: %w", err)
}
// Parse response
device, err := parseProbeResponse(buffer[:n])
if err != nil {
// Skip invalid responses
continue
}
// Add to devices map (deduplicate by endpoint)
if device != nil && device.EndpointRef != "" {
devices[device.EndpointRef] = device
}
}
}
}
// parseProbeResponse parses a WS-Discovery probe response
func parseProbeResponse(data []byte) (*Device, error) {
var envelope struct {
Body struct {
ProbeMatches ProbeMatches `xml:"ProbeMatches"`
} `xml:"Body"`
}
if err := xml.Unmarshal(data, &envelope); err != nil {
return nil, err
}
if len(envelope.Body.ProbeMatches.ProbeMatch) == 0 {
return nil, fmt.Errorf("no probe matches found")
}
// Take the first probe match
match := envelope.Body.ProbeMatches.ProbeMatch[0]
device := &Device{
EndpointRef: match.EndpointRef,
XAddrs: parseSpaceSeparated(match.XAddrs),
Types: parseSpaceSeparated(match.Types),
Scopes: parseSpaceSeparated(match.Scopes),
MetadataVersion: match.MetadataVersion,
}
return device, nil
}
// parseSpaceSeparated parses a space-separated string into a slice
func parseSpaceSeparated(s string) []string {
s = strings.TrimSpace(s)
if s == "" {
return []string{}
}
return strings.Fields(s)
}
// deviceMapToSlice converts a map of devices to a slice
func deviceMapToSlice(m map[string]*Device) []*Device {
devices := make([]*Device, 0, len(m))
for _, device := range m {
devices = append(devices, device)
}
return devices
}
// generateUUID generates a simple UUID (not cryptographically secure)
func generateUUID() string {
return fmt.Sprintf("%d-%d-%d-%d-%d",
time.Now().UnixNano(),
time.Now().Unix(),
time.Now().UnixNano()%1000,
time.Now().Unix()%1000,
time.Now().UnixNano()%10000)
}
// resolveNetworkInterface resolves a network interface by name or IP address
func resolveNetworkInterface(ifaceSpec string) (*net.Interface, error) {
// Try to get interface by name (e.g., "eth0", "wlan0")
if iface, err := net.InterfaceByName(ifaceSpec); err == nil {
return iface, nil
}
// Try to parse as IP address and find the interface
if ip := net.ParseIP(ifaceSpec); ip != nil {
interfaces, err := net.Interfaces()
if err != nil {
return nil, fmt.Errorf("failed to list network interfaces: %w", err)
}
for _, iface := range interfaces {
addrs, err := iface.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
switch v := addr.(type) {
case *net.IPNet:
if v.IP.Equal(ip) {
return &iface, nil
}
case *net.IPAddr:
if v.IP.Equal(ip) {
return &iface, nil
}
}
}
}
}
// List available interfaces for error message
interfaces, _ := net.Interfaces()
availableInterfaces := make([]string, 0)
for _, iface := range interfaces {
addrs, _ := iface.Addrs()
ifaceInfo := iface.Name
if len(addrs) > 0 {
var addrStrs []string
for _, addr := range addrs {
addrStrs = append(addrStrs, addr.String())
}
ifaceInfo += " [" + strings.Join(addrStrs, ", ") + "]"
}
availableInterfaces = append(availableInterfaces, ifaceInfo)
}
return nil, fmt.Errorf("network interface %q not found. Available interfaces: %v", ifaceSpec, availableInterfaces)
}
// ListNetworkInterfaces returns all available network interfaces with their addresses
func ListNetworkInterfaces() ([]NetworkInterface, error) {
interfaces, err := net.Interfaces()
if err != nil {
return nil, fmt.Errorf("failed to list network interfaces: %w", err)
}
var result []NetworkInterface
for _, iface := range interfaces {
addrs, err := iface.Addrs()
if err != nil {
continue
}
var ipAddrs []string
for _, addr := range addrs {
switch v := addr.(type) {
case *net.IPNet:
ipAddrs = append(ipAddrs, v.IP.String())
case *net.IPAddr:
ipAddrs = append(ipAddrs, v.IP.String())
}
}
result = append(result, NetworkInterface{
Name: iface.Name,
Addresses: ipAddrs,
Up: iface.Flags&net.FlagUp != 0,
Multicast: iface.Flags&net.FlagMulticast != 0,
})
}
return result, nil
}
// NetworkInterface represents a network interface
type NetworkInterface struct {
// Name of the interface (e.g., "eth0", "wlan0")
Name string
// IP addresses assigned to this interface
Addresses []string
// Up indicates if the interface is up
Up bool
// Multicast indicates if the interface supports multicast
Multicast bool
}
// GetDeviceEndpoint extracts the primary device endpoint from XAddrs
func (d *Device) GetDeviceEndpoint() string {
if len(d.XAddrs) == 0 {
return ""
}
// Return the first XAddr
return d.XAddrs[0]
}
// GetName extracts the device name from scopes
func (d *Device) GetName() string {
for _, scope := range d.Scopes {
if strings.Contains(scope, "name") {
parts := strings.Split(scope, "/")
if len(parts) > 0 {
return parts[len(parts)-1]
}
}
}
return ""
}
// GetLocation extracts the device location from scopes
func (d *Device) GetLocation() string {
for _, scope := range d.Scopes {
if strings.Contains(scope, "location") {
parts := strings.Split(scope, "/")
if len(parts) > 0 {
return parts[len(parts)-1]
}
}
}
return ""
}