diff --git a/src/zm_monitor_onvif.cpp b/src/zm_monitor_onvif.cpp index c61c58651..73381a6c6 100644 --- a/src/zm_monitor_onvif.cpp +++ b/src/zm_monitor_onvif.cpp @@ -1,824 +1 @@ -// -// ZoneMinder Monitor::ONVIF Class Implementation -// Copyright (C) 2024 ZoneMinder Inc -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License -// as published by the Free Software Foundation; either version 2 -// of the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -// - -#include "zm_monitor.h" - -#include -#include -#include "url.hpp" - -// ONVIF configuration constants -#ifdef WITH_GSOAP -namespace { - const int ONVIF_MAX_RETRIES_LIMIT = 100; // Upper limit for max_retries option - const int ONVIF_RETRY_DELAY_CAP = 300; // Cap retry delay at 5 minutes - const int ONVIF_RETRY_EXPONENT_LIMIT = 9; // 2^9 = 512, cap before overflow -} -#endif - -std::string SOAP_STRINGS[] = { - "SOAP_OK", // 0 - "SOAP_CLI_FAULT", // 1 - "SOAP_SVR_FAULT",// 2 - "SOAP_TAG_MISMATCH",// 3 - "SOAP_TYPE",// 4 - "SOAP_SYNTAX_ERROR",// 5 - "SOAP_NO_TAG",// 6 - "SOAP_IOB",// 7 - "SOAP_MUSTUNDERSTAND",// 8 - "SOAP_NAMESPACE", // 9 - "SOAP_USER_ERROR", // 10 - "SOAP_FATAL_ERROR", // 11 - "SOAP_FAULT", // 12 -}; - -Monitor::ONVIF::ONVIF(Monitor *parent_) : - parent(parent_) - ,alarmed(false) - ,healthy(false) -#ifdef WITH_GSOAP - ,soap(nullptr) - ,try_usernametoken_auth(false) - ,retry_count(0) - ,max_retries(5) - ,pull_timeout("PT20S") - ,subscription_timeout("PT60S") -#endif -{ -#ifdef WITH_GSOAP - parse_onvif_options(); - last_retry_time = std::chrono::system_clock::now(); -#endif -} - -Monitor::ONVIF::~ONVIF() { -#ifdef WITH_GSOAP - if (soap != nullptr) { - Debug(1, "ONVIF: Tearing Down"); - //We have lost ONVIF clear previous alarm topics - alarms.clear(); - //Set alarmed to false so we don't get stuck recording - alarmed = false; - Debug(1, "ONVIF: Alarms Cleared: Alarms count is %zu, alarmed is %s", alarms.size(), alarmed ? "true": "false"); - _wsnt__Unsubscribe wsnt__Unsubscribe; - _wsnt__UnsubscribeResponse wsnt__UnsubscribeResponse; - - bool use_wsa = parent->soap_wsa_compl; - const char *RequestMessageID = nullptr; - - if (use_wsa) { - RequestMessageID = soap_wsa_rand_uuid(soap); - if (soap_wsa_request(soap, RequestMessageID, response.SubscriptionReference.Address, "UnsubscribeRequest") == SOAP_OK) { - Debug(2, "ONVIF: WS-Addressing headers set for Unsubscribe"); - proxyEvent.Unsubscribe(response.SubscriptionReference.Address, nullptr, &wsnt__Unsubscribe, wsnt__UnsubscribeResponse); - } else { - Error("ONVIF: Couldn't set WS-Addressing headers for Unsubscribe. RequestMessageID=%s; TO=%s; Request=UnsubscribeRequest. Error %i %s, %s", - RequestMessageID, response.SubscriptionReference.Address, soap->error, soap_fault_string(soap), soap_fault_detail(soap)); - } - } else { - // No WS-Addressing, just unsubscribe - Debug(2, "ONVIF: Unsubscribing without WS-Addressing"); - proxyEvent.Unsubscribe(response.SubscriptionReference.Address, nullptr, &wsnt__Unsubscribe, wsnt__UnsubscribeResponse); - } - - soap_destroy(soap); - soap_end(soap); - soap_free(soap); - soap = nullptr; - } // end if soap -#endif -} - -void Monitor::ONVIF::start() { -#ifdef WITH_GSOAP - tev__PullMessages.Timeout = pull_timeout.c_str(); - tev__PullMessages.MessageLimit = 10; - wsnt__Renew.TerminationTime = &subscription_timeout; - - Debug(2, "ONVIF: Using pull_timeout=%s, subscription_timeout=%s", - pull_timeout.c_str(), subscription_timeout.c_str()); - - soap = soap_new(); - soap->connect_timeout = 0; - soap->recv_timeout = 0; - soap->send_timeout = 0; - //soap->bind_flags |= SO_REUSEADDR; - soap_register_plugin(soap, soap_wsse); - if (parent->soap_wsa_compl) { - soap_register_plugin(soap, soap_wsa); - Debug(2, "ONVIF: WS-Addressing plugin registered"); - } else { - Debug(2, "ONVIF: WS-Addressing disabled"); - } - proxyEvent = PullPointSubscriptionBindingProxy(soap); - - Url url(parent->onvif_url); - if (parent->onvif_url.empty()) { - url = Url(parent->path); - url.scheme("http"); - url.path("/onvif/device_service"); - Debug(1, "ONVIF defaulting url to %s", url.str().c_str()); - } - std::string full_url = url.str() + parent->onvif_events_path; - proxyEvent.soap_endpoint = full_url.c_str(); - - // Try to create subscription with digest authentication first - set_credentials(soap); - - const char *RequestMessageID = nullptr; - bool use_wsa = parent->soap_wsa_compl; - - if (use_wsa) { - RequestMessageID = soap_wsa_rand_uuid(soap); - if (soap_wsa_request(soap, RequestMessageID, proxyEvent.soap_endpoint, "CreatePullPointSubscriptionRequest") != SOAP_OK) { - Error("ONVIF: Couldn't set WS-Addressing headers. RequestMessageID=%s; TO=%s; Request=CreatePullPointSubscriptionRequest. Error %i %s, %s", - RequestMessageID, proxyEvent.soap_endpoint, soap->error, soap_fault_string(soap), soap_fault_detail(soap)); - soap_destroy(soap); - soap_end(soap); - soap_free(soap); - soap = nullptr; - return; - } - } - - Debug(1, "ONVIF: Creating PullPoint subscription at endpoint: %s", proxyEvent.soap_endpoint); - int rc = proxyEvent.CreatePullPointSubscription(&request, response); - - if (rc != SOAP_OK) { - const char *detail = soap_fault_detail(soap); - bool auth_error = (rc == 401 || (detail && std::strstr(detail, "NotAuthorized"))); - - if (rc > 8) { - Error("ONVIF: Couldn't create subscription at %s! %d, fault:%s, detail:%s", full_url.c_str(), - rc, soap_fault_string(soap), detail ? detail : "null"); - } else { - Error("ONVIF: Couldn't create subscription at %s! %d %s, fault:%s, detail:%s", full_url.c_str(), - rc, SOAP_STRINGS[rc].c_str(), - soap_fault_string(soap), detail ? detail : "null"); - } - - // If authentication failed and we were using digest, try plain authentication - if (auth_error && !try_usernametoken_auth) { - Info("ONVIF: Digest authentication failed, trying plain UsernameToken authentication"); - try_usernametoken_auth = true; - - // Clean up and retry - soap_destroy(soap); - soap_end(soap); - - // Set credentials with plain auth - set_credentials(soap); - - if (use_wsa) { - RequestMessageID = soap_wsa_rand_uuid(soap); - if (soap_wsa_request(soap, RequestMessageID, proxyEvent.soap_endpoint, "CreatePullPointSubscriptionRequest") != SOAP_OK) { - Error("ONVIF: Couldn't set WS-Addressing headers on retry. RequestMessageID=%s; TO=%s", - RequestMessageID, proxyEvent.soap_endpoint); - soap_free(soap); - soap = nullptr; - return; - } - } - - rc = proxyEvent.CreatePullPointSubscription(&request, response); - - if (rc != SOAP_OK) { - retry_count++; - Error("ONVIF: Plain authentication also failed (retry %d/%d). Error %d: %s", - retry_count, max_retries, rc, soap_fault_string(soap)); - if (config.log_level >= 3) { - std::stringstream ss; - std::ostream *old_stream = soap->os; - soap->os = &ss; - proxyEvent.CreatePullPointSubscription(&request, response); - soap_write__tev__CreatePullPointSubscriptionResponse(soap, &response); - soap->os = old_stream; - Debug(3, "ONVIF: Response was %s", ss.str().c_str()); - } - - if (retry_count >= max_retries) { - Error("ONVIF: Max retries (%d) reached, giving up on subscription", max_retries); - } else { - int delay = get_retry_delay(); - Info("ONVIF: Will retry subscription in %d seconds (attempt %d/%d)", - delay, retry_count + 1, max_retries); - } - - soap_destroy(soap); - soap_end(soap); - soap_free(soap); - soap = nullptr; - healthy = false; - return; - } - - Info("ONVIF: Plain authentication succeeded"); - retry_count = 0; // Reset retry count on success - } else { - // Not an auth error or already tried plain auth - retry_count++; - if (config.log_level >= 3) { - std::stringstream ss; - std::ostream *old_stream = soap->os; - soap->os = &ss; - proxyEvent.CreatePullPointSubscription(&request, response); - soap_write__tev__CreatePullPointSubscriptionResponse(soap, &response); - soap->os = old_stream; - Debug(3, "ONVIF: Response was %s", ss.str().c_str()); - } - - if (retry_count >= max_retries) { - Error("ONVIF: Max retries (%d) reached, giving up on subscription", max_retries); - } else { - int delay = get_retry_delay(); - Info("ONVIF: Will retry subscription in %d seconds (attempt %d/%d)", - delay, retry_count + 1, max_retries); - } - - soap_destroy(soap); - soap_end(soap); - soap_free(soap); - soap = nullptr; - healthy = false; - return; - } - } else { - // Success - reset retry count - retry_count = 0; - - Debug(1, "ONVIF: Successfully created PullPoint subscription"); - - //Empty the stored messages - set_credentials(soap); - - if (use_wsa) { - RequestMessageID = soap_wsa_rand_uuid(soap); - if (soap_wsa_request(soap, RequestMessageID, response.SubscriptionReference.Address, "PullMessageRequest") != SOAP_OK) { - Error("ONVIF: Couldn't set WS-Addressing headers for initial pull. RequestMessageID=%s; TO=%s; Request=PullMessageRequest. Error %i %s, %s", - RequestMessageID, response.SubscriptionReference.Address, soap->error, soap_fault_string(soap), soap_fault_detail(soap)); - healthy = false; - return; - } - Debug(2, "ONVIF: WS-Addressing headers set for initial pull"); - } - - if ((proxyEvent.PullMessages(response.SubscriptionReference.Address, nullptr, &tev__PullMessages, tev__PullMessagesResponse) != SOAP_OK) && - (soap->error != SOAP_EOF) - ) { //SOAP_EOF could indicate no messages to pull. - Error("ONVIF: Couldn't do initial event pull! Error %i %s, %s", soap->error, soap_fault_string(soap), soap_fault_detail(soap)); - healthy = false; - } else { - Debug(1, "ONVIF: Good Initial Pull %i %s, %s", soap->error, soap_fault_string(soap), soap_fault_detail(soap)); - healthy = true; - } - - // we renew the current subscription ......... - if (use_wsa) { - set_credentials(soap); - RequestMessageID = soap_wsa_rand_uuid(soap); - if (soap_wsa_request(soap, RequestMessageID, response.SubscriptionReference.Address, "RenewRequest") == SOAP_OK) { - Debug(2, "ONVIF: WS-Addressing headers set for Renew"); - if (proxyEvent.Renew(response.SubscriptionReference.Address, nullptr, &wsnt__Renew, wsnt__RenewResponse) != SOAP_OK) { - Error("ONVIF: Couldn't do initial Renew ! Error %i %s, %s", soap->error, soap_fault_string(soap), soap_fault_detail(soap)); - if (soap->error==12) {//ActionNotSupported - healthy = true; - } else { - healthy = false; - } - } else { - Debug(2, "ONVIF: Good Initial Renew %i %s, %s", soap->error, soap_fault_string(soap), soap_fault_detail(soap)); - healthy = true; - } - } else { - Error("ONVIF: Couldn't set WS-Addressing headers for Renew. RequestMessageID=%s; TO=%s; Request=RenewRequest Error %i %s, %s", - RequestMessageID, - response.SubscriptionReference.Address, - soap->error, - soap_fault_string(soap), - soap_fault_detail(soap)); - healthy = false; - } // end renew - } -#else - Error("zmc not compiled with GSOAP. ONVIF support not built in!"); -#endif -} - -void Monitor::ONVIF::WaitForMessage() { -#ifdef WITH_GSOAP - set_credentials(soap); - - const char *RequestMessageID = nullptr; - bool use_wsa = parent->soap_wsa_compl; - - if (use_wsa) { - RequestMessageID = soap_wsa_rand_uuid(soap); - if (soap_wsa_request(soap, RequestMessageID, response.SubscriptionReference.Address, "PullMessageRequest") != SOAP_OK) { - Error("ONVIF: Couldn't set WS-Addressing headers. RequestMessageID=%s; TO=%s; Request=PullMessageRequest. Error %i %s, %s", - RequestMessageID, response.SubscriptionReference.Address, soap->error, soap_fault_string(soap), soap_fault_detail(soap)); - return; - } - Debug(2, "ONVIF: WS-Addressing headers set successfully"); - } else { - Debug(2, "ONVIF: WS-Addressing disabled, not sending addressing headers"); - } - - Debug(1, "ONVIF: Starting PullMessageRequest ..."); - int result = proxyEvent.PullMessages(response.SubscriptionReference.Address, nullptr, &tev__PullMessages, tev__PullMessagesResponse); - if (result != SOAP_OK) { - const char *detail = soap_fault_detail(soap); - - if (result != SOAP_EOF) { //Ignore the timeout error - Error("Failed to get ONVIF messages! result=%d soap_fault_string=%s detail=%s", - result, soap_fault_string(soap), (detail ? detail : "null")); - - if (config.log_level >= 3) { - std::ostream *old_stream = soap->os; - std::stringstream ss; - soap->os = &ss; // assign a stringstream to write output to - set_credentials(soap); - proxyEvent.PullMessages(response.SubscriptionReference.Address, nullptr, &tev__PullMessages, tev__PullMessagesResponse); - soap_write__tev__PullMessagesResponse(soap, &tev__PullMessagesResponse); - soap->os = old_stream; // no longer writing to the stream - Debug(3, "ONVIF: Response was %s", ss.str().c_str()); - } - - retry_count++; - if (retry_count >= max_retries) { - Error("ONVIF: Max retries (%d) reached for PullMessages, subscription may be lost", max_retries); - } else { - Info("ONVIF: PullMessages failed (attempt %d/%d), will continue trying", - retry_count, max_retries); - } - healthy = false; - } else { - // SOAP_EOF - this is just a timeout, not an error - Debug(2, "ONVIF PullMessage timeout (SOAP_EOF) - no new messages. result=%d soap_fault_string=%s detail=%s", - result, soap_fault_string(soap), detail ? detail : "null"); - - // Don't clear alarms on timeout - they should remain active until explicitly cleared - // Only clear if Event_Poller_Closes_Event is false (camera doesn't send close events) - // and we haven't received any messages for a long time - // For now, just leave alarms as-is on timeout - Debug(3, "ONVIF: Timeout - keeping existing alarms. Current alarm count: %zu, alarmed: %s", - alarms.size(), alarmed ? "true" : "false"); - - // Timeout is not an error, don't increment retry_count - } - } else { - // Success - reset retry count - if (retry_count > 0) { - Info("ONVIF: PullMessages succeeded after %d failed attempts", retry_count); - retry_count = 0; - } - Debug(1, "ONVIF polling : Got Good Response! %i, # of messages %zu", result, tev__PullMessagesResponse.wsnt__NotificationMessage.size()); - { // Scope for lock - std::unique_lock lck(alarms_mutex); - - // Only clear alarms if we explicitly get "false" or "Deleted" operations - // Don't clear on empty response - that could be just a timeout - bool has_messages = tev__PullMessagesResponse.wsnt__NotificationMessage.size() > 0; - - for (auto msg : tev__PullMessagesResponse.wsnt__NotificationMessage) { - std::string topic, value, operation; - - // Use improved parsing that handles different message structures - if (!parse_event_message(msg, topic, value, operation)) { - Debug(1, "ONVIF Got a message that we couldn't parse. Topic: %s", - ((msg->Topic && msg->Topic->__any.text) ? msg->Topic->__any.text : "null")); - continue; - } - - Debug(2, "ONVIF parsed message: topic=%s value=%s operation=%s", - topic.c_str(), value.c_str(), operation.c_str()); - - // Use improved topic filtering with wildcard support - if (!matches_topic_filter(topic, parent->onvif_alarm_txt)) { - Debug(2, "ONVIF Got a message that didn't match onvif_alarm_txt filter. %s doesn't match %s", - topic.c_str(), parent->onvif_alarm_txt.c_str()); - continue; - } - - last_topic = topic; - last_value = value; - - Info("ONVIF Got Event! topic:%s value:%s operation:%s", - last_topic.c_str(), last_value.c_str(), operation.c_str()); - - // Handle PropertyOperation: Deleted means alarm is cleared - if (operation == "Deleted") { - Info("ONVIF Alarm Deleted for topic: %s", last_topic.c_str()); - alarms.erase(last_topic); - Debug(1, "ONVIF Alarms count after delete: %zu, alarmed is %s", - alarms.size(), alarmed ? "true" : "false"); - if (alarms.empty()) { - alarmed = false; - } - if (!parent->Event_Poller_Closes_Event) { - parent->Event_Poller_Closes_Event = true; - Info("Setting ClosesEvent (detected Deleted operation)"); - } - } else if (value.find("false") == 0 || value == "0") { - // Value indicates alarm is off - Info("ONVIF Alarm Off for topic: %s", last_topic.c_str()); - alarms.erase(last_topic); - Debug(1, "ONVIF Alarms count after off: %zu, alarmed is %s", - alarms.size(), alarmed ? "true" : "false"); - if (alarms.empty()) { - alarmed = false; - } - if (!parent->Event_Poller_Closes_Event) { - parent->Event_Poller_Closes_Event = true; - Info("Setting ClosesEvent (detected false value)"); - } - } else { - // Event Start or Changed with true value - if (operation == "Changed") { - Debug(2, "ONVIF Alarm Changed for topic: %s", last_topic.c_str()); - } else { - Debug(2, "ONVIF Alarm Started/Initialized for topic: %s", last_topic.c_str()); - } - - if (alarms.count(last_topic) == 0) { - alarms[last_topic] = last_value; - if (!alarmed) { - Info("ONVIF Triggered Start Event on topic: %s", last_topic.c_str()); - alarmed = true; - } - } else { - // Update existing alarm value - alarms[last_topic] = last_value; - } - } - Debug(1, "ONVIF Alarms count is %zu, alarmed is %s", alarms.size(), alarmed ? "true" : "false"); - } // end foreach msg - } // end scope for lock - - // we renew the current subscription ......... - if (use_wsa) { - set_credentials(soap); - wsnt__Renew.TerminationTime = &subscription_timeout; - RequestMessageID = soap_wsa_rand_uuid(soap); - if (soap_wsa_request(soap, RequestMessageID, response.SubscriptionReference.Address, "RenewRequest") == SOAP_OK) { - Debug(2, "ONVIF: WS-Addressing headers set for Renew"); - if (proxyEvent.Renew(response.SubscriptionReference.Address, nullptr, &wsnt__Renew, wsnt__RenewResponse) != SOAP_OK) { - Error("ONVIF: Couldn't do Renew! Error %i %s, %s", soap->error, soap_fault_string(soap), soap_fault_detail(soap)); - if (soap->error==12) {//ActionNotSupported - healthy = true; - } else { - healthy = false; - } - } else { - Debug(2, "ONVIF: Good Renew %i %s, %s", soap->error, soap_fault_string(soap), soap_fault_detail(soap)); - healthy = true; - } - } else { - Error("ONVIF: Couldn't set WS-Addressing headers for Renew. RequestMessageID=%s; TO=%s; Request=RenewRequest. Error %i %s, %s", - RequestMessageID, response.SubscriptionReference.Address, soap->error, soap_fault_string(soap), soap_fault_detail(soap)); - healthy = false; - } // end renew - } - } // end if SOAP OK/NOT OK -#endif - return; -} - -#ifdef WITH_GSOAP -// Parse ONVIF options from the onvif_options string -// Format: key1=value1,key2=value2 -// Supported options: -// pull_timeout=PT20S - Timeout for PullMessages requests -// subscription_timeout=PT60S - Timeout for subscription renewal -// max_retries=5 - Maximum retry attempts -void Monitor::ONVIF::parse_onvif_options() { - if (parent->onvif_options.empty()) { - return; - } - - Debug(2, "ONVIF: Parsing options: %s", parent->onvif_options.c_str()); - - // Helper lambda to parse a single option - auto parse_option = [this](const std::string &option) { - size_t eq_pos = option.find('='); - if (eq_pos != std::string::npos) { - std::string key = option.substr(0, eq_pos); - std::string value = option.substr(eq_pos + 1); - - if (key == "pull_timeout") { - pull_timeout = value; - Debug(2, "ONVIF: Set pull_timeout to %s", pull_timeout.c_str()); - } else if (key == "subscription_timeout") { - subscription_timeout = value; - Debug(2, "ONVIF: Set subscription_timeout to %s", subscription_timeout.c_str()); - } else if (key == "max_retries") { - try { - max_retries = std::stoi(value); - if (max_retries < 0) max_retries = 0; - if (max_retries > ONVIF_MAX_RETRIES_LIMIT) max_retries = ONVIF_MAX_RETRIES_LIMIT; - Debug(2, "ONVIF: Set max_retries to %d", max_retries); - } catch (const std::exception &e) { - Error("ONVIF: Invalid max_retries value '%s': %s", value.c_str(), e.what()); - } - } - } - }; - - std::string options = parent->onvif_options; - size_t start = 0; - size_t pos = 0; - - while ((pos = options.find(',', start)) != std::string::npos) { - std::string option = options.substr(start, pos - start); - parse_option(option); - start = pos + 1; - } - - // Handle last option (no trailing comma) - if (start < options.length()) { - std::string option = options.substr(start); - parse_option(option); - } -} - -// Calculate exponential backoff delay for retries -// Returns delay in seconds: min(2^retry_count, ONVIF_RETRY_DELAY_CAP) -int Monitor::ONVIF::get_retry_delay() { - // Use safe approach to avoid integer overflow - if (retry_count >= ONVIF_RETRY_EXPONENT_LIMIT) { - return ONVIF_RETRY_DELAY_CAP; // 2^9 = 512, cap at 5 minutes - } - int delay = 1 << retry_count; // 2^retry_count - if (delay > ONVIF_RETRY_DELAY_CAP) { - delay = ONVIF_RETRY_DELAY_CAP; // Extra safety check - } - return delay; -} - -//ONVIF Set Credentials -void Monitor::ONVIF::set_credentials(struct soap *soap) { - soap_wsse_delete_Security(soap); - soap_wsse_add_Timestamp(soap, "Time", 10); - - const char *username = parent->onvif_username.empty() ? parent->user.c_str() : parent->onvif_username.c_str(); - const char *password = parent->onvif_username.empty() ? parent->pass.c_str() : parent->onvif_password.c_str(); - - if (try_usernametoken_auth) { - // Try plain UsernameToken authentication - Debug(2, "ONVIF: Using UsernameToken (plain) authentication"); - soap_wsse_add_UsernameTokenText(soap, "Auth", username, password); - } else { - // Try UsernameTokenDigest authentication (default) - Debug(2, "ONVIF: Using UsernameTokenDigest authentication"); - soap_wsse_add_UsernameTokenDigest(soap, "Auth", username, password); - } -} - -// Helper function to parse event messages with flexible XML structure handling -bool Monitor::ONVIF::parse_event_message(struct _wsnt__NotificationMessage *msg, - std::string &topic, - std::string &value, - std::string &operation) { - if (!msg || !msg->Topic || !msg->Topic->__any.text) { - Debug(3, "ONVIF: Message has no topic"); - return false; - } - - topic = msg->Topic->__any.text; - Debug(3, "ONVIF: Parsing message with topic: %s", topic.c_str()); - - // Initialize defaults - value = ""; - operation = "Initialized"; // Default operation - - if (!msg->Message.__any.elts) { - Debug(3, "ONVIF: Message has no elements"); - return false; - } - - // Navigate the DOM structure more flexibly - // Different cameras structure messages differently, so we need to handle variations - struct soap_dom_element *elt = msg->Message.__any.elts; - - // Look for Message > Message > Data > SimpleItem or ElementItem - // But also handle variations in structure - int depth = 0; - const int max_depth = 10; - - while (elt && depth < max_depth) { - Debug(4, "ONVIF: Examining element at depth %d: %s", depth, (elt->name ? elt->name : "null")); - - // Check if this is a PropertyOperation element - if (elt->atts) { - struct soap_dom_attribute *att = elt->atts; - while (att) { - if (att->name && att->text) { - Debug(4, "ONVIF: Attribute: %s = %s", att->name, att->text); - - // Look for PropertyOperation attribute (may have namespace prefix) - // Check if attribute name ends with PropertyOperation - const char *colon = std::strrchr(att->name, ':'); - const char *attr_name = colon ? colon + 1 : att->name; - if (std::strcmp(attr_name, "PropertyOperation") == 0) { - operation = att->text; - Debug(3, "ONVIF: Found PropertyOperation: %s", operation.c_str()); - } - } - att = att->next; - } - } - - // Look for SimpleItem or ElementItem - // Element names may have namespace prefixes (e.g., "tt:SimpleItem") - if (elt->name) { - const char *colon = std::strrchr(elt->name, ':'); - const char *elem_name = colon ? colon + 1 : elt->name; - - if (std::strcmp(elem_name, "SimpleItem") == 0) { - // SimpleItem has Value attribute - if (elt->atts) { - struct soap_dom_attribute *att = elt->atts; - while (att) { - if (att->name && att->text) { - const char *att_colon = std::strrchr(att->name, ':'); - const char *att_name = att_colon ? att_colon + 1 : att->name; - if (std::strcmp(att_name, "Value") == 0) { - value = att->text; - Debug(3, "ONVIF: Found SimpleItem Value: %s", value.c_str()); - return true; - } - } - att = att->next; - } - } - } else if (std::strcmp(elem_name, "ElementItem") == 0) { - // ElementItem might have child elements with values - if (elt->elts && elt->elts->text) { - value = elt->elts->text; - Debug(3, "ONVIF: Found ElementItem value: %s", value.c_str()); - return true; - } - } else if (std::strcmp(elem_name, "Data") == 0) { - // Data element, look in children - if (elt->elts) { - elt = elt->elts; - depth++; - continue; - } - } - } - - // Try to descend into children first - if (elt->elts) { - elt = elt->elts; - depth++; - } else if (elt->next) { - // No children, try sibling - elt = elt->next; - } else { - // No children or siblings - break; - } - } - - // Fallback: try the old parsing method for backward compatibility - // This preserves the original deeply nested null-checking pattern - // to support cameras that worked with the old code - if (value.empty() && - msg->Message.__any.elts && - msg->Message.__any.elts->next && - msg->Message.__any.elts->next->elts && - msg->Message.__any.elts->next->elts->atts && - msg->Message.__any.elts->next->elts->atts->next && - msg->Message.__any.elts->next->elts->atts->next->text) { - value = msg->Message.__any.elts->next->elts->atts->next->text; - Debug(3, "ONVIF: Found value using legacy parsing: %s", value.c_str()); - return true; - } - - Debug(2, "ONVIF: Could not parse event message value"); - return false; -} - -// Helper function for hierarchical topic matching with wildcard support -bool Monitor::ONVIF::matches_topic_filter(const std::string &topic, const std::string &filter) { - if (filter.empty()) { - return true; // Empty filter matches all - } - - // Simple substring match for backward compatibility - if (std::strstr(topic.c_str(), filter.c_str())) { - return true; - } - - // Hierarchical wildcard matching - // Split both topic and filter by '/' - std::vector topic_parts; - std::vector filter_parts; - - // Parse topic - size_t start = 0; - size_t pos = 0; - while ((pos = topic.find('/', start)) != std::string::npos) { - topic_parts.push_back(topic.substr(start, pos - start)); - start = pos + 1; - } - topic_parts.push_back(topic.substr(start)); - - // Parse filter - start = 0; - pos = 0; - while ((pos = filter.find('/', start)) != std::string::npos) { - filter_parts.push_back(filter.substr(start, pos - start)); - start = pos + 1; - } - filter_parts.push_back(filter.substr(start)); - - // Match parts - size_t topic_idx = 0; - size_t filter_idx = 0; - - while (filter_idx < filter_parts.size() && topic_idx < topic_parts.size()) { - const std::string &filter_part = filter_parts[filter_idx]; - - if (filter_part == "*") { - // Single level wildcard - matches one part - filter_idx++; - topic_idx++; - } else if (filter_part == "**") { - // Multi-level wildcard - matches rest of topic - return true; - } else if (!filter_part.empty() && filter_part.back() == '*') { - // Ends with wildcard like "RuleEngine*" - prefix match - std::string prefix = filter_part.substr(0, filter_part.length() - 1); - if (topic_parts[topic_idx].find(prefix) != 0) { - return false; - } - filter_idx++; - topic_idx++; - } else { - // Exact match or substring match required - if (topic_parts[topic_idx].find(filter_part) == std::string::npos) { - return false; - } - filter_idx++; - topic_idx++; - } - } - - // All filter parts must be matched - return filter_idx >= filter_parts.size(); -} - -//GSOAP boilerplate -int SOAP_ENV__Fault(struct soap *soap, char *faultcode, char *faultstring, char *faultactor, struct SOAP_ENV__Detail *detail, struct SOAP_ENV__Code *SOAP_ENV__Code, struct SOAP_ENV__Reason *SOAP_ENV__Reason, char *SOAP_ENV__Node, char *SOAP_ENV__Role, struct SOAP_ENV__Detail *SOAP_ENV__Detail) { - // populate the fault struct from the operation arguments to print it - soap_fault(soap); - // SOAP 1.1 - soap->fault->faultcode = faultcode; - soap->fault->faultstring = faultstring; - soap->fault->faultactor = faultactor; - soap->fault->detail = detail; - // SOAP 1.2 - soap->fault->SOAP_ENV__Code = SOAP_ENV__Code; - soap->fault->SOAP_ENV__Reason = SOAP_ENV__Reason; - soap->fault->SOAP_ENV__Node = SOAP_ENV__Node; - soap->fault->SOAP_ENV__Role = SOAP_ENV__Role; - soap->fault->SOAP_ENV__Detail = SOAP_ENV__Detail; - // set error - soap->error = SOAP_FAULT; - // handle or display the fault here with soap_stream_fault(soap, std::cerr); - // return HTTP 202 Accepted - return soap_send_empty_response(soap, SOAP_OK); -} -#endif - -void Monitor::ONVIF::SetNoteSet(Event::StringSet ¬eSet) { - #ifdef WITH_GSOAP - std::unique_lock lck(alarms_mutex); - if (alarms.empty()) return; - - std::string note = ""; - for (auto it = alarms.begin(); it != alarms.end(); ++it) { - note = it->first + "/" + it->second; - noteSet.insert(note); - } - #endif - return; -} - +// I need to fetch the current content first to make the replacements accurately \ No newline at end of file