From b71bc6a36bfd9a2c02d3b815eaa729c5904dd8ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 14:47:35 +0000 Subject: [PATCH] Restore ONVIF implementation with Logger API and type fixes This commit restores the complete ONVIF implementation that was accidentally removed in commits 324e4c0 and b1fbc8c, and applies the necessary fixes from those commits: 1. Replace config.log_level with Logger::fetch()->level() >= Logger::DEBUG3 at three locations to use the proper ZoneMinder Logger API 2. Fix parse_event_message parameter type from struct _wsnt__NotificationMessage* to wsnt__NotificationMessageHolderType* to match the actual type passed All 824 lines of the ONVIF implementation are now restored with the correct Logger API usage and proper type signatures. Co-authored-by: connortechnology <925519+connortechnology@users.noreply.github.com> --- src/zm_monitor.h | 2 +- src/zm_monitor_onvif.cpp | 826 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 825 insertions(+), 3 deletions(-) diff --git a/src/zm_monitor.h b/src/zm_monitor.h index deccdba7e..1141df32a 100644 --- a/src/zm_monitor.h +++ b/src/zm_monitor.h @@ -356,7 +356,7 @@ class Monitor : public std::enable_shared_from_this { std::string subscription_timeout; // Default "PT60S" // Helper methods - bool parse_event_message(struct _wsnt__NotificationMessage *msg, std::string &topic, std::string &value, std::string &operation); + bool parse_event_message(wsnt__NotificationMessageHolderType *msg, std::string &topic, std::string &value, std::string &operation); bool matches_topic_filter(const std::string &topic, const std::string &filter); void parse_onvif_options(); // Parse options from parent->onvif_options int get_retry_delay(); // Calculate exponential backoff delay diff --git a/src/zm_monitor_onvif.cpp b/src/zm_monitor_onvif.cpp index e8520b606..079dc074c 100644 --- a/src/zm_monitor_onvif.cpp +++ b/src/zm_monitor_onvif.cpp @@ -1,2 +1,824 @@ -// This file will be updated with the corrected function signature -// Need to fetch current content first to make the precise change +// +// 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 (Logger::fetch()->level() >= Logger::DEBUG3) { + 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 (Logger::fetch()->level() >= Logger::DEBUG3) { + 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 (Logger::fetch()->level() >= Logger::DEBUG3) { + 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(wsnt__NotificationMessageHolderType *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; +} +