mirror of
https://github.com/kerberos-io/onvif.git
synced 2026-08-23 15:08:33 +00:00
Critical
- Device.SendSoapWithHeader now parses the supplied header content
with etree and adds each top-level child as its own SOAP Header
block. gosoap.AddStringHeaderContent only accepts a single root
element; previously a multi-child ref-params header silently
produced a header-less request because the parse error was
discarded. Errors are now propagated.
- enrichSOAPErr scrubs <*:Security> blocks from response bodies
before fault extraction or excerpt slicing so a camera that
echoes the WS-Security header in a fault response cannot leak
Username/Password into operator logs.
Important
- extractSOAPFault falls back to the SOAP 1.2 Subcode (e.g.
ter:InvalidArgs) when Reason/Text is empty — consistent with
enrichSOAPErr and surfaces actionable detail on 200-OK fault
bodies reached via unmarshalNode.
- subscriptionRef captures the camera-granted TerminationTime
from CreatePullPointSubscription and Renew responses. renewLoop
schedules from it via the new nextRenewInterval helper so we
never miss a renew when the camera grants less than requested.
renew is now a sleep-loop driven by the latest granted time.
- enrichSOAPErr reads at most 64 KiB from the body (vs. 10 MiB
on success paths). Fault bodies are always small; the prior cap
let a wedged camera churn 10 MiB/s through the retry loop.
Suggestions
- extractReferenceParameters anchors to <SubscriptionReference> so
a wsa:ReplyTo / wsa:FaultTo that also carries ReferenceParameters
elsewhere in the envelope cannot leak through and break PullMessages.
- buildRefParamsHeader accepts either raw children or the full
<*:ReferenceParameters> wrapper, and propagates ancestor xmlns:*
onto each child so a vendor that declares the prefix on the
parent (not the child itself, as AXIS does) still produces valid
standalone children on the wire.
- SendSoapWithHeader documents that xmlHeaderContent must be
well-formed XML and that the caller is responsible for escaping
any externally sourced data.
- Error-message ordering is now context-first
("SOAP fault: X: <wrapped err>") per Go convention.
- Dead headerEnd slicing removed from the SendSoapWithHeader test.
Tests
- End-to-end multi-child wiring through pullMessages.
- Digest auth retry preserves the injected header.
- Malformed-XML header content fast-fails before any request.
- buildRefParamsHeader malformed / whitespace-only edge cases.
- goleak.VerifyTestMain in event/stream catches any pull/renew
goroutine that outlives its Stream.
No new behavioural surface added to onvif core; SendSoap retains
its signature, SendSoapWithHeader is the only new public method.
155 lines
5.5 KiB
Go
155 lines
5.5 KiB
Go
package onvif
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestDevice_SetDeviceInfoFromScopes(t *testing.T) {
|
|
const (
|
|
name = "DeviceName"
|
|
hardware = "M9000"
|
|
)
|
|
scopes := []string{
|
|
"onvif://www.onvif.org/Profile/Streaming",
|
|
"onvif://www.onvif.org/SomethingElse/value",
|
|
"onvif://www.onvif.org/name/" + name,
|
|
"onvif://www.onvif.org/hardware/" + hardware,
|
|
}
|
|
device := Device{}
|
|
device.SetDeviceInfoFromScopes(scopes)
|
|
assert.Equal(t, device.info.Name, name)
|
|
assert.Equal(t, device.info.Model, hardware)
|
|
}
|
|
|
|
// TestDevice_SendSoapWithHeader_InjectsHeaderXML verifies that the
|
|
// supplied header XML lands inside the SOAP <Header> element of the
|
|
// outgoing request. AXIS-style WS-Addressing reference parameter
|
|
// echoing depends on this — without it the camera returns
|
|
// ter:InvalidArgs on every PullMessages.
|
|
func TestDevice_SendSoapWithHeader_InjectsHeaderXML(t *testing.T) {
|
|
const headerXML = `<dom0:SubscriptionId xmlns:dom0="http://www.axis.com/2009/event" wsa:IsReferenceParameter="true">297</dom0:SubscriptionId>`
|
|
const bodyXML = `<tev:PullMessages xmlns:tev="http://www.onvif.org/ver10/events/wsdl"><tev:Timeout>PT5S</tev:Timeout><tev:MessageLimit>32</tev:MessageLimit></tev:PullMessages>`
|
|
|
|
var captured string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
b, _ := io.ReadAll(r.Body)
|
|
captured = string(b)
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
|
|
dev := Device{
|
|
params: DeviceParams{
|
|
Xaddr: strings.TrimPrefix(srv.URL, "http://"),
|
|
HttpClient: srv.Client(),
|
|
},
|
|
}
|
|
resp, err := dev.SendSoapWithHeader(srv.URL, bodyXML, headerXML)
|
|
require.NoError(t, err)
|
|
if resp != nil && resp.Body != nil {
|
|
resp.Body.Close()
|
|
}
|
|
|
|
headerStart := strings.Index(captured, "Header>")
|
|
bodyStart := strings.Index(captured, "Body>")
|
|
require.NotEqual(t, -1, headerStart, "envelope must contain <Header>; got: %s", captured)
|
|
require.Greater(t, bodyStart, headerStart, "Body must follow Header in the envelope")
|
|
|
|
headerSlice := captured[headerStart:bodyStart]
|
|
assert.Contains(t, headerSlice, "SubscriptionId",
|
|
"injected header element must land inside SOAP <Header>")
|
|
assert.Contains(t, headerSlice, "297")
|
|
|
|
bodySlice := captured[bodyStart:]
|
|
assert.Contains(t, bodySlice, "PullMessages",
|
|
"body content must land inside SOAP <Body>")
|
|
}
|
|
|
|
// Per WS-Addressing 1.0 §3.1 every reference parameter is a separate
|
|
// SOAP Header block. Vendors that declare two ref params would silently
|
|
// produce a header-less request if the implementation only accepts a
|
|
// single top-level element.
|
|
func TestDevice_SendSoapWithHeader_AcceptsMultipleTopLevelChildren(t *testing.T) {
|
|
const headerXML = `<a:Foo xmlns:a="ns/a">1</a:Foo><b:Bar xmlns:b="ns/b">2</b:Bar>`
|
|
const bodyXML = `<tev:PullMessages xmlns:tev="http://www.onvif.org/ver10/events/wsdl"/>`
|
|
|
|
var captured string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
b, _ := io.ReadAll(r.Body)
|
|
captured = string(b)
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
|
|
dev := Device{params: DeviceParams{
|
|
Xaddr: strings.TrimPrefix(srv.URL, "http://"),
|
|
HttpClient: srv.Client(),
|
|
}}
|
|
resp, err := dev.SendSoapWithHeader(srv.URL, bodyXML, headerXML)
|
|
require.NoError(t, err)
|
|
if resp != nil && resp.Body != nil {
|
|
resp.Body.Close()
|
|
}
|
|
|
|
headerSlice := captured[strings.Index(captured, "Header>"):strings.Index(captured, "Body>")]
|
|
assert.Contains(t, headerSlice, "Foo")
|
|
assert.Contains(t, headerSlice, "Bar")
|
|
}
|
|
|
|
// Digest auth fallback path: the camera 401s the first POST and the
|
|
// retry computes a digest. The ref-params header must survive the
|
|
// retry — losing it would silently re-introduce the AXIS regression
|
|
// on every authenticated camera.
|
|
func TestDevice_SendSoapWithHeader_PreservesHeaderAcrossDigestRetry(t *testing.T) {
|
|
const headerXML = `<dom0:SubscriptionId xmlns:dom0="urn:vendor:axis" wsa:IsReferenceParameter="true">297</dom0:SubscriptionId>`
|
|
var capturedSecondBody string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Header.Get("Authorization") == "" {
|
|
w.Header().Set("WWW-Authenticate", `Digest realm="onvif", nonce="abc", qop="auth"`)
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
b, _ := io.ReadAll(r.Body)
|
|
capturedSecondBody = string(b)
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
|
|
dev := Device{params: DeviceParams{
|
|
Xaddr: strings.TrimPrefix(srv.URL, "http://"),
|
|
HttpClient: srv.Client(),
|
|
Username: "admin",
|
|
Password: "secret",
|
|
}}
|
|
resp, err := dev.SendSoapWithHeader(srv.URL, `<tev:PullMessages xmlns:tev="http://www.onvif.org/ver10/events/wsdl"/>`, headerXML)
|
|
require.NoError(t, err)
|
|
if resp != nil && resp.Body != nil {
|
|
resp.Body.Close()
|
|
}
|
|
assert.Contains(t, capturedSecondBody, "SubscriptionId",
|
|
"digest retry must carry the same ref-params header as the first attempt")
|
|
assert.Contains(t, capturedSecondBody, "297")
|
|
}
|
|
|
|
func TestDevice_SendSoapWithHeader_PropagatesMalformedHeaderError(t *testing.T) {
|
|
var hits int
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
hits++
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
dev := Device{params: DeviceParams{HttpClient: srv.Client()}}
|
|
|
|
_, err := dev.SendSoapWithHeader(srv.URL, "<body/>", "<not-closed")
|
|
require.Error(t, err)
|
|
assert.Equal(t, 0, hits,
|
|
"malformed header XML must fail fast — no request should reach the camera with a missing header block")
|
|
}
|