2222#include "video/onvif_motion_recording.h"
2323#include "video/zone_filter.h"
2424#include "database/db_detections.h"
25+ #include "ezxml.h"
26+
27+ /* WS-Addressing action URIs for the ONVIF operations this file emits.
28+ * Strict ONVIF servers (Reolink, some Vivotek) reject requests that don't
29+ * carry a matching wsa:Action header and `action=` Content-Type parameter —
30+ * see #374. */
31+ #define ONVIF_ACTION_GET_SERVICES "http://www.onvif.org/ver10/device/wsdl/GetServices"
32+ #define ONVIF_ACTION_CREATE_PULLPOINT "http://www.onvif.org/ver10/events/wsdl/EventPortType/CreatePullPointSubscriptionRequest"
33+ #define ONVIF_ACTION_PULL_MESSAGES "http://www.onvif.org/ver10/events/wsdl/PullPointSubscription/PullMessagesRequest"
34+
35+ /* External UUID generator (used for wsa:MessageID). */
36+ extern void generate_uuid (char * uuid , size_t size );
2537
2638// Global variables
2739static bool initialized = false;
@@ -70,8 +82,15 @@ static size_t write_memory_callback(void *contents, size_t size, size_t nmemb, v
7082 return realsize ;
7183}
7284
73- // Create ONVIF SOAP request with WS-Security (if credentials provided)
74- static char * create_onvif_request (const char * username , const char * password , const char * request_body ) {
85+ /*
86+ * Create ONVIF SOAP request with WS-Security (if credentials provided) and
87+ * WS-Addressing headers. `action` and `to` are the WS-Addressing action URI
88+ * and destination URL respectively; both are required (strict ONVIF servers
89+ * such as Reolink reject requests missing either — #374).
90+ */
91+ static char * create_onvif_request (const char * username , const char * password ,
92+ const char * request_body ,
93+ const char * action , const char * to ) {
7594 bool has_credentials = (username && strlen (username ) > 0 && password && strlen (password ) > 0 );
7695 char * security_header = NULL ;
7796
@@ -87,8 +106,22 @@ static char *create_onvif_request(const char *username, const char *password, co
87106 }
88107 }
89108
90- // Allocate envelope dynamically to accommodate variable-length body and header
91- size_t envelope_size = strlen (request_body ) + strlen (security_header ) + 1024 ;
109+ /* Build a WS-Addressing fragment if an action was provided. Using
110+ * `urn:uuid:<v4>` for MessageID and leaving mustUnderstand off on To/Action
111+ * — cameras that ignore WS-Addressing shouldn't then fault because they
112+ * "didn't understand" an optional header. */
113+ char uuid [48 ] = {0 };
114+ generate_uuid (uuid , sizeof (uuid ));
115+
116+ const char * safe_action = action ? action : "" ;
117+ const char * safe_to = to ? to : "" ;
118+
119+ /* The WS-Addressing block inflates the envelope; reserve enough for
120+ * MessageID (urn:uuid:… = ~45 chars), the action URI, the destination URL,
121+ * the security header, and the body. 2 KB of slack above those sizes is
122+ * plenty. */
123+ size_t envelope_size = strlen (request_body ) + strlen (security_header )
124+ + strlen (safe_action ) + strlen (safe_to ) + 2048 ;
92125 char * soap_request = malloc (envelope_size );
93126 if (!soap_request ) {
94127 free (security_header );
@@ -97,28 +130,43 @@ static char *create_onvif_request(const char *username, const char *password, co
97130
98131 snprintf (soap_request , envelope_size ,
99132 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
100- "<s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\">"
101- "<s:Header>%s</s:Header>"
133+ "<s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\" "
134+ "xmlns:wsa=\"http://www.w3.org/2005/08/addressing\">"
135+ "<s:Header>"
136+ "<wsa:MessageID>urn:uuid:%s</wsa:MessageID>"
137+ "<wsa:To>%s</wsa:To>"
138+ "<wsa:Action>%s</wsa:Action>"
139+ "%s"
140+ "</s:Header>"
102141 "<s:Body xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "
103- "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">%s</s:Body>"
142+ "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">%s</s:Body>"
104143 "</s:Envelope>" ,
105- security_header , request_body );
144+ uuid , safe_to , safe_action , security_header , request_body );
106145
107146 free (security_header );
108147 return soap_request ;
109148}
110149
111- // Send ONVIF request to a full URL (bypassing the base URL + /onvif/ + service construction)
150+ /*
151+ * Send ONVIF request to a full URL (bypassing the base URL + /onvif/ + service
152+ * construction). `action` is the WS-Addressing action URI; it's also added as
153+ * the `action=` parameter on the Content-Type header per SOAP 1.2, which
154+ * strict ONVIF servers require (#374).
155+ */
112156static char * send_onvif_request_to_url (const char * full_url , const char * username ,
113- const char * password , const char * request_body ) {
157+ const char * password , const char * request_body ,
158+ const char * action ) {
114159 if (!initialized || !curl_handle ) {
115160 log_error ("ONVIF detection system not initialized" );
116161 return NULL ;
117162 }
118163
119164 log_info ("ONVIF Detection: Sending request to %s" , full_url );
120165
121- char * soap_request = create_onvif_request (username , password , request_body );
166+ /* Use full_url as the WS-Addressing To, which is exactly what the request
167+ * is being POSTed to. */
168+ char * soap_request = create_onvif_request (username , password , request_body ,
169+ action , full_url );
122170 if (!soap_request ) {
123171 log_error ("Failed to create ONVIF request" );
124172 return NULL ;
@@ -131,9 +179,20 @@ static char *send_onvif_request_to_url(const char *full_url, const char *usernam
131179 curl_easy_setopt (curl_handle , CURLOPT_POSTFIELDS , soap_request );
132180 curl_easy_setopt (curl_handle , CURLOPT_POSTFIELDSIZE , strlen (soap_request ));
133181
134- // Set headers
182+ /* Content-Type carries the SOAP 1.2 action parameter in addition to the
183+ * charset. Fall back to the plain type when no action is provided. */
184+ char content_type [512 ];
185+ if (action && action [0 ] != '\0' ) {
186+ snprintf (content_type , sizeof (content_type ),
187+ "Content-Type: application/soap+xml; charset=utf-8; action=\"%s\"" ,
188+ action );
189+ } else {
190+ snprintf (content_type , sizeof (content_type ),
191+ "Content-Type: application/soap+xml; charset=utf-8" );
192+ }
193+
135194 struct curl_slist * headers = NULL ;
136- headers = curl_slist_append (headers , "Content-Type: application/soap+xml; charset=utf-8" );
195+ headers = curl_slist_append (headers , content_type );
137196 curl_easy_setopt (curl_handle , CURLOPT_HTTPHEADER , headers );
138197
139198 // Set up response buffer
@@ -179,8 +238,9 @@ static char *send_onvif_request_to_url(const char *full_url, const char *usernam
179238}
180239
181240// Send ONVIF request and get response
182- static char * send_onvif_request (const char * url , const char * username , const char * password ,
183- const char * request_body , const char * service ) {
241+ static char * send_onvif_request (const char * url , const char * username , const char * password ,
242+ const char * request_body , const char * service ,
243+ const char * action ) {
184244 if (!initialized || !curl_handle ) {
185245 log_error ("ONVIF detection system not initialized" );
186246 return NULL ;
@@ -190,7 +250,7 @@ static char *send_onvif_request(const char *url, const char *username, const cha
190250 char full_url [512 ];
191251 snprintf (full_url , sizeof (full_url ), "%s/onvif/%s" , url , service );
192252
193- return send_onvif_request_to_url (full_url , username , password , request_body );
253+ return send_onvif_request_to_url (full_url , username , password , request_body , action );
194254}
195255
196256// Extract subscription address from response
@@ -219,45 +279,92 @@ static char *extract_subscription_address(const char *response) {
219279 return NULL ;
220280}
221281
222- // Query GetServices and return the event service URL, or NULL if not found.
223- // The returned string is heap-allocated; caller must free() it.
282+ /* Return the local name of an ezxml element (strips any "prefix:" prefix). */
283+ static const char * local_name (ezxml_t el ) {
284+ if (!el || !el -> name ) return "" ;
285+ const char * colon = strchr (el -> name , ':' );
286+ return colon ? colon + 1 : el -> name ;
287+ }
288+
289+ /* Find a direct child with the given local name, ignoring namespace prefix. */
290+ static ezxml_t child_by_local_name (ezxml_t parent , const char * local ) {
291+ if (!parent || !local ) return NULL ;
292+ for (ezxml_t c = parent -> child ; c ; c = c -> ordered ) {
293+ if (strcmp (local_name (c ), local ) == 0 ) return c ;
294+ }
295+ return NULL ;
296+ }
297+
298+ /*
299+ * Query GetServices and return the event service URL, or NULL if not found.
300+ * The returned string is heap-allocated; caller must free() it.
301+ *
302+ * Previous implementation used substring matching with a 512-byte window,
303+ * which misses the XAddr when a Service element carries a Capabilities
304+ * subtree (#374, Reolink Argus). We now parse the response with ezxml and
305+ * walk Service elements, comparing the Namespace text to the events WSDL.
306+ */
224307static char * discover_event_service_url (const char * url , const char * username , const char * password ) {
225308 const char * request_body =
226309 "<GetServices xmlns=\"http://www.onvif.org/ver10/device/wsdl\">"
227310 "<IncludeCapability>false</IncludeCapability>"
228311 "</GetServices>" ;
229312
230313 // GetServices is always sent to the standard device management endpoint
231- char * response = send_onvif_request (url , username , password , request_body , "device_service" );
314+ char * response = send_onvif_request (url , username , password , request_body ,
315+ "device_service" , ONVIF_ACTION_GET_SERVICES );
232316 if (!response ) {
233317 log_warn ("ONVIF: GetServices to device_service endpoint failed" );
234318 return NULL ;
235319 }
236320
237- // Find the events service namespace in the response
238- const char * events_ns = "http://www.onvif.org/ver10/events/wsdl" ;
239- const char * ns_pos = strstr (response , events_ns );
240- if (!ns_pos ) {
241- log_warn ("ONVIF: Events service namespace not found in GetServices response" );
242- free (response );
243- return NULL ;
244- }
245-
246- // Search forward from the namespace position for XAddr (prefixed and un-prefixed variants)
247- const char * xaddr_open [] = {"<tds:XAddr>" , "<XAddr>" , NULL };
248- const char * xaddr_close [] = {"</tds:XAddr>" , "</XAddr>" , NULL };
321+ /* ezxml_parse_str modifies the buffer in place, and the returned
322+ * tree references it, so keep `response` alive until we copy the
323+ * matched URL out. */
324+ ezxml_t xml = ezxml_parse_str (response , strlen (response ));
249325 char * event_url = NULL ;
250326
251- for (int i = 0 ; xaddr_open [i ] != NULL ; i ++ ) {
252- const char * tag = strstr (ns_pos , xaddr_open [i ]);
253- // Only accept the XAddr if it is within the same Service element (~512 bytes window)
254- if (tag && (tag - ns_pos ) < 512 ) {
255- const char * val_start = tag + strlen (xaddr_open [i ]);
256- const char * val_end = strstr (val_start , xaddr_close [i ]);
257- if (val_end ) {
258- size_t url_len = (size_t )(val_end - val_start );
259- event_url = strndup (val_start , url_len );
260- break ;
327+ if (xml ) {
328+ ezxml_t body = child_by_local_name (xml , "Body" );
329+ ezxml_t resp = body ? child_by_local_name (body , "GetServicesResponse" ) : NULL ;
330+
331+ if (resp ) {
332+ const char * events_ns = "http://www.onvif.org/ver10/events/wsdl" ;
333+ for (ezxml_t svc = resp -> child ; svc && !event_url ; svc = svc -> ordered ) {
334+ if (strcmp (local_name (svc ), "Service" ) != 0 ) continue ;
335+
336+ ezxml_t ns = child_by_local_name (svc , "Namespace" );
337+ ezxml_t xaddr = child_by_local_name (svc , "XAddr" );
338+ const char * ns_text = ns ? ezxml_txt (ns ) : "" ;
339+ const char * xaddr_text = xaddr ? ezxml_txt (xaddr ) : "" ;
340+
341+ if (ns_text && xaddr_text && xaddr_text [0 ] != '\0'
342+ && strcmp (ns_text , events_ns ) == 0 ) {
343+ event_url = strdup (xaddr_text );
344+ }
345+ }
346+ }
347+ ezxml_free (xml );
348+ }
349+
350+ /* Fallback: the old substring heuristic, for responses that ezxml
351+ * can't parse (malformed XML from buggy firmware). */
352+ if (!event_url ) {
353+ const char * events_ns = "http://www.onvif.org/ver10/events/wsdl" ;
354+ const char * ns_pos = strstr (response , events_ns );
355+ if (ns_pos ) {
356+ const char * xaddr_open [] = {"<tds:XAddr>" , "<XAddr>" , NULL };
357+ const char * xaddr_close [] = {"</tds:XAddr>" , "</XAddr>" , NULL };
358+ for (int i = 0 ; xaddr_open [i ] != NULL ; i ++ ) {
359+ const char * tag = strstr (ns_pos , xaddr_open [i ]);
360+ if (tag && (tag - ns_pos ) < 2048 ) {
361+ const char * val_start = tag + strlen (xaddr_open [i ]);
362+ const char * val_end = strstr (val_start , xaddr_close [i ]);
363+ if (val_end ) {
364+ event_url = strndup (val_start , (size_t )(val_end - val_start ));
365+ break ;
366+ }
367+ }
261368 }
262369 }
263370 }
@@ -311,16 +418,18 @@ static onvif_subscription_t *get_subscription(const char *url, const char *usern
311418
312419 if (discovered_url ) {
313420 log_info ("ONVIF: Sending CreatePullPointSubscription to discovered URL: %s" , discovered_url );
314- response = send_onvif_request_to_url (discovered_url , username , password , request_body );
421+ response = send_onvif_request_to_url (discovered_url , username , password , request_body ,
422+ ONVIF_ACTION_CREATE_PULLPOINT );
315423 free (discovered_url );
316424 }
317425
318426 if (!response ) {
319427 // Fall back through common event service path suffixes
320- const char * fallback_services [] = {"service" , "event_service" , NULL };
428+ const char * fallback_services [] = {"service" , "event_service" , "Events" , NULL };
321429 for (int i = 0 ; fallback_services [i ] && !response ; i ++ ) {
322430 log_info ("ONVIF: Trying fallback event endpoint: onvif/%s" , fallback_services [i ]);
323- response = send_onvif_request (url , username , password , request_body , fallback_services [i ]);
431+ response = send_onvif_request (url , username , password , request_body ,
432+ fallback_services [i ], ONVIF_ACTION_CREATE_PULLPOINT );
324433 }
325434 }
326435
@@ -554,7 +663,8 @@ int detect_motion_onvif(const char *onvif_url, const char *username, const char
554663 response = send_onvif_request_to_url (subscription -> subscription_address ,
555664 subscription -> username ,
556665 subscription -> password ,
557- request_body );
666+ request_body ,
667+ ONVIF_ACTION_PULL_MESSAGES );
558668 } else {
559669 // Legacy fallback: extract last path component and re-append under /onvif/
560670 log_warn ("ONVIF Detection: subscription_address is not a full URL ('%s'), using legacy path extraction" ,
@@ -565,7 +675,8 @@ int detect_motion_onvif(const char *onvif_url, const char *username, const char
565675 subscription -> username ,
566676 subscription -> password ,
567677 request_body ,
568- service );
678+ service ,
679+ ONVIF_ACTION_PULL_MESSAGES );
569680 free (service );
570681 }
571682 }
0 commit comments