feat: implement localhost URL handling and add comprehensive tests
This commit is contained in:
@@ -14,7 +14,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Full URL: `"http://192.168.1.100/onvif/device_service"` (backward compatible)
|
||||
- Automatically adds `http://` scheme and `/onvif/device_service` path when needed
|
||||
- See `docs/SIMPLIFIED_ENDPOINT.md` for details
|
||||
- **Localhost URL Fix**: Automatic handling of cameras that report localhost addresses
|
||||
- Detects and fixes localhost/127.0.0.1/0.0.0.0/::1 in GetCapabilities response
|
||||
- Replaces with actual camera IP address
|
||||
- Preserves service-specific ports when specified
|
||||
- Handles common camera firmware bugs transparently
|
||||
- Comprehensive test coverage for endpoint normalization (12 test cases)
|
||||
- Comprehensive test coverage for localhost URL handling (10 test cases)
|
||||
- New example: `examples/simplified-endpoint/` demonstrating all endpoint formats
|
||||
- Documentation: `docs/PROJECT_STRUCTURE.md` explaining project organization
|
||||
- Initial release of go-onvif library
|
||||
|
||||
@@ -116,6 +116,46 @@ func normalizeEndpoint(endpoint string) (string, error) {
|
||||
return fullURL, nil
|
||||
}
|
||||
|
||||
// fixLocalhostURL replaces localhost/loopback addresses in service URLs with the actual camera host
|
||||
// Some cameras incorrectly report localhost (127.0.0.1, 0.0.0.0, localhost) in their capability URLs
|
||||
func (c *Client) fixLocalhostURL(serviceURL string) string {
|
||||
if serviceURL == "" {
|
||||
return serviceURL
|
||||
}
|
||||
|
||||
// Parse the service URL
|
||||
parsedService, err := url.Parse(serviceURL)
|
||||
if err != nil {
|
||||
return serviceURL // Return original if parsing fails
|
||||
}
|
||||
|
||||
// Check if the service URL has a localhost/loopback address
|
||||
host := parsedService.Hostname()
|
||||
if host == "localhost" || host == "127.0.0.1" || host == "0.0.0.0" || host == "::1" {
|
||||
// Parse the client's endpoint to get the actual camera address
|
||||
parsedClient, err := url.Parse(c.endpoint)
|
||||
if err != nil {
|
||||
return serviceURL // Return original if parsing fails
|
||||
}
|
||||
|
||||
// Replace the host but keep the port from service URL if specified
|
||||
servicePort := parsedService.Port()
|
||||
if servicePort != "" {
|
||||
parsedService.Host = parsedClient.Hostname() + ":" + servicePort
|
||||
} else {
|
||||
parsedService.Host = parsedClient.Hostname()
|
||||
// Use client's port if service doesn't specify one
|
||||
if clientPort := parsedClient.Port(); clientPort != "" {
|
||||
parsedService.Host = parsedClient.Hostname() + ":" + clientPort
|
||||
}
|
||||
}
|
||||
|
||||
return parsedService.String()
|
||||
}
|
||||
|
||||
return serviceURL
|
||||
}
|
||||
|
||||
// Initialize discovers and initializes service endpoints
|
||||
func (c *Client) Initialize(ctx context.Context) error {
|
||||
// Get device information and capabilities
|
||||
@@ -124,18 +164,19 @@ func (c *Client) Initialize(ctx context.Context) error {
|
||||
return fmt.Errorf("failed to get capabilities: %w", err)
|
||||
}
|
||||
|
||||
// Extract service endpoints
|
||||
// Extract service endpoints and fix any localhost addresses
|
||||
// Some cameras incorrectly report localhost instead of their actual IP
|
||||
if capabilities.Media != nil && capabilities.Media.XAddr != "" {
|
||||
c.mediaEndpoint = capabilities.Media.XAddr
|
||||
c.mediaEndpoint = c.fixLocalhostURL(capabilities.Media.XAddr)
|
||||
}
|
||||
if capabilities.PTZ != nil && capabilities.PTZ.XAddr != "" {
|
||||
c.ptzEndpoint = capabilities.PTZ.XAddr
|
||||
c.ptzEndpoint = c.fixLocalhostURL(capabilities.PTZ.XAddr)
|
||||
}
|
||||
if capabilities.Imaging != nil && capabilities.Imaging.XAddr != "" {
|
||||
c.imagingEndpoint = capabilities.Imaging.XAddr
|
||||
c.imagingEndpoint = c.fixLocalhostURL(capabilities.Imaging.XAddr)
|
||||
}
|
||||
if capabilities.Events != nil && capabilities.Events.XAddr != "" {
|
||||
c.eventEndpoint = capabilities.Events.XAddr
|
||||
c.eventEndpoint = c.fixLocalhostURL(capabilities.Events.XAddr)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
+155
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -639,3 +640,157 @@ func ExampleClient_GetDeviceInformation() {
|
||||
fmt.Printf("Camera: %s %s\n", info.Manufacturer, info.Model)
|
||||
fmt.Printf("Firmware: %s\n", info.FirmwareVersion)
|
||||
}
|
||||
|
||||
func TestFixLocalhostURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
clientURL string
|
||||
serviceURL string
|
||||
expectedURL string
|
||||
}{
|
||||
{
|
||||
name: "localhost hostname",
|
||||
clientURL: "http://192.168.1.100/onvif/device_service",
|
||||
serviceURL: "http://localhost/onvif/media_service",
|
||||
expectedURL: "http://192.168.1.100/onvif/media_service",
|
||||
},
|
||||
{
|
||||
name: "127.0.0.1 loopback",
|
||||
clientURL: "http://192.168.1.100:8080/onvif/device_service",
|
||||
serviceURL: "http://127.0.0.1/onvif/ptz_service",
|
||||
expectedURL: "http://192.168.1.100:8080/onvif/ptz_service",
|
||||
},
|
||||
{
|
||||
name: "0.0.0.0 address",
|
||||
clientURL: "http://192.168.1.100/onvif/device_service",
|
||||
serviceURL: "http://0.0.0.0/onvif/imaging_service",
|
||||
expectedURL: "http://192.168.1.100/onvif/imaging_service",
|
||||
},
|
||||
{
|
||||
name: "IPv6 loopback",
|
||||
clientURL: "http://192.168.1.100/onvif/device_service",
|
||||
serviceURL: "http://[::1]/onvif/events_service",
|
||||
expectedURL: "http://192.168.1.100/onvif/events_service",
|
||||
},
|
||||
{
|
||||
name: "localhost with different port",
|
||||
clientURL: "http://192.168.1.100/onvif/device_service",
|
||||
serviceURL: "http://localhost:8080/onvif/media_service",
|
||||
expectedURL: "http://192.168.1.100:8080/onvif/media_service",
|
||||
},
|
||||
{
|
||||
name: "valid IP address unchanged",
|
||||
clientURL: "http://192.168.1.100/onvif/device_service",
|
||||
serviceURL: "http://192.168.1.100/onvif/media_service",
|
||||
expectedURL: "http://192.168.1.100/onvif/media_service",
|
||||
},
|
||||
{
|
||||
name: "different valid IP unchanged",
|
||||
clientURL: "http://192.168.1.100/onvif/device_service",
|
||||
serviceURL: "http://192.168.1.50/onvif/media_service",
|
||||
expectedURL: "http://192.168.1.50/onvif/media_service",
|
||||
},
|
||||
{
|
||||
name: "HTTPS localhost",
|
||||
clientURL: "https://192.168.1.100/onvif/device_service",
|
||||
serviceURL: "https://localhost/onvif/media_service",
|
||||
expectedURL: "https://192.168.1.100/onvif/media_service",
|
||||
},
|
||||
{
|
||||
name: "client with port, service localhost no port",
|
||||
clientURL: "http://192.168.1.100:80/onvif/device_service",
|
||||
serviceURL: "http://localhost/onvif/media_service",
|
||||
expectedURL: "http://192.168.1.100:80/onvif/media_service",
|
||||
},
|
||||
{
|
||||
name: "empty service URL",
|
||||
clientURL: "http://192.168.1.100/onvif/device_service",
|
||||
serviceURL: "",
|
||||
expectedURL: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client := &Client{
|
||||
endpoint: tt.clientURL,
|
||||
}
|
||||
|
||||
result := client.fixLocalhostURL(tt.serviceURL)
|
||||
if result != tt.expectedURL {
|
||||
t.Errorf("fixLocalhostURL() = %v, want %v", result, tt.expectedURL)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitializeWithLocalhostURLs(t *testing.T) {
|
||||
// Create a mock server
|
||||
mock := NewMockONVIFServer()
|
||||
defer mock.Close()
|
||||
|
||||
// Set a GetCapabilities response with localhost URLs
|
||||
capabilitiesResponse := `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope">
|
||||
<SOAP-ENV:Body>
|
||||
<tds:GetCapabilitiesResponse xmlns:tds="http://www.onvif.org/ver10/device/wsdl">
|
||||
<tds:Capabilities>
|
||||
<tt:Media xmlns:tt="http://www.onvif.org/ver10/schema">
|
||||
<tt:XAddr>http://localhost:8080/onvif/media_service</tt:XAddr>
|
||||
</tt:Media>
|
||||
<tt:PTZ xmlns:tt="http://www.onvif.org/ver10/schema">
|
||||
<tt:XAddr>http://127.0.0.1/onvif/ptz_service</tt:XAddr>
|
||||
</tt:PTZ>
|
||||
<tt:Imaging xmlns:tt="http://www.onvif.org/ver10/schema">
|
||||
<tt:XAddr>http://0.0.0.0/onvif/imaging_service</tt:XAddr>
|
||||
</tt:Imaging>
|
||||
</tds:Capabilities>
|
||||
</tds:GetCapabilitiesResponse>
|
||||
</SOAP-ENV:Body>
|
||||
</SOAP-ENV:Envelope>`
|
||||
|
||||
mock.SetResponse("GetCapabilities", capabilitiesResponse)
|
||||
|
||||
// Create client pointing to mock server
|
||||
client, err := NewClient(
|
||||
mock.URL()+"/onvif/device_service",
|
||||
WithCredentials("admin", "admin"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
|
||||
// Initialize should fix localhost URLs
|
||||
ctx := context.Background()
|
||||
err = client.Initialize(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Initialize() failed: %v", err)
|
||||
}
|
||||
|
||||
// Parse the mock server URL to get host
|
||||
mockURL, _ := url.Parse(mock.URL())
|
||||
expectedHost := mockURL.Host
|
||||
|
||||
// Verify media endpoint was fixed (localhost:8080 should be replaced with mock host)
|
||||
if strings.Contains(client.mediaEndpoint, "localhost") {
|
||||
t.Errorf("Media endpoint still contains localhost: %v", client.mediaEndpoint)
|
||||
}
|
||||
if !strings.Contains(client.mediaEndpoint, expectedHost) {
|
||||
t.Logf("Media endpoint: %v, Expected to contain: %v", client.mediaEndpoint, expectedHost)
|
||||
// The port 8080 from service URL should be preserved
|
||||
expectedMediaURL := "http://" + mockURL.Hostname() + ":8080/onvif/media_service"
|
||||
if client.mediaEndpoint != expectedMediaURL {
|
||||
t.Errorf("Media endpoint = %v, want %v", client.mediaEndpoint, expectedMediaURL)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify PTZ endpoint was fixed (127.0.0.1 should be replaced with mock host)
|
||||
if strings.Contains(client.ptzEndpoint, "127.0.0.1") && !strings.Contains(expectedHost, "127.0.0.1") {
|
||||
t.Errorf("PTZ endpoint still contains 127.0.0.1: %v", client.ptzEndpoint)
|
||||
}
|
||||
|
||||
// Verify Imaging endpoint was fixed (0.0.0.0 should be replaced with mock host)
|
||||
if strings.Contains(client.imagingEndpoint, "0.0.0.0") {
|
||||
t.Errorf("Imaging endpoint still contains 0.0.0.0: %v", client.imagingEndpoint)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user