diff --git a/_codeql_detected_source_root b/_codeql_detected_source_root new file mode 120000 index 000000000..945c9b46d --- /dev/null +++ b/_codeql_detected_source_root @@ -0,0 +1 @@ +. \ No newline at end of file diff --git a/src/zm_monitor.h b/src/zm_monitor.h index 944f8118a..1141df32a 100644 --- a/src/zm_monitor.h +++ b/src/zm_monitor.h @@ -345,6 +345,21 @@ class Monitor : public std::enable_shared_from_this { _wsnt__RenewResponse wsnt__RenewResponse; PullPointSubscriptionBindingProxy proxyEvent; void set_credentials(struct soap *soap); + bool try_usernametoken_auth; // Track if we should try plain auth + int retry_count; // Track retry attempts + int max_retries; // Maximum retry attempts before giving up + std::string discovered_event_endpoint; // Store discovered endpoint + SystemTimePoint last_retry_time; // Time of last retry attempt + + // Configurable timeout values (can be set via onvif_options) + std::string pull_timeout; // Default "PT20S" + std::string subscription_timeout; // Default "PT60S" + + // Helper methods + 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 #endif std::unordered_map alarms; std::mutex alarms_mutex; diff --git a/src/zm_monitor_onvif.cpp b/src/zm_monitor_onvif.cpp index 7a2fa382b..a39229142 100644 --- a/src/zm_monitor_onvif.cpp +++ b/src/zm_monitor_onvif.cpp @@ -23,6 +23,15 @@ #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 @@ -45,27 +54,47 @@ Monitor::ONVIF::ONVIF(Monitor *parent_) : ,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, "Tearing Down Onvif"); + 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"); + 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; - const char *RequestMessageID = parent->soap_wsa_compl ? soap_wsa_rand_uuid(soap) : "RequestMessageID"; - if ((!parent->soap_wsa_compl) || (soap_wsa_request(soap, RequestMessageID, response.SubscriptionReference.Address, "UnsubscribeRequest") == SOAP_OK)) { - proxyEvent.Unsubscribe(response.SubscriptionReference.Address, nullptr, &wsnt__Unsubscribe, 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 { - Error("Couldn't set wsa headers RequestMessageID=%s; TO= %s; Request=UnsubscribeRequest .... ! Error %i %s, %s", - RequestMessageID, response.SubscriptionReference.Address, soap->error, soap_fault_string(soap), soap_fault_detail(soap)); + // 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); @@ -78,17 +107,25 @@ Monitor::ONVIF::~ONVIF() { void Monitor::ONVIF::start() { #ifdef WITH_GSOAP - tev__PullMessages.Timeout = "PT20S"; + tev__PullMessages.Timeout = pull_timeout.c_str(); tev__PullMessages.MessageLimit = 10; - std::string Termination_time = "PT60S"; - wsnt__Renew.TerminationTime = &Termination_time; + 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);}; + 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); @@ -100,107 +137,185 @@ void Monitor::ONVIF::start() { } 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 = parent->soap_wsa_compl ? soap_wsa_rand_uuid(soap) : "RequestMessageID"; - - if ((!parent->soap_wsa_compl) || (soap_wsa_request(soap, RequestMessageID, proxyEvent.soap_endpoint, "CreatePullPointSubscriptionRequest") == SOAP_OK)) { - Debug(1, "ONVIF Endpoint: %s", proxyEvent.soap_endpoint); - int rc = proxyEvent.CreatePullPointSubscription(&request, response); -#if 0 - std::stringstream ss; - soap->os = &ss; // assign a stringstream to write output to - soap_write__tev__CreatePullPointSubscriptionResponse(soap, &response); - soap->os = NULL; // no longer writing to the stream - Debug(1, "Response was %s", ss.str().c_str()); -#endif - - if (rc != SOAP_OK) { - const char *detail = soap_fault_detail(soap); - 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"); - } - - std::stringstream ss; - std::ostream *old_stream = soap->os; - soap->os = &ss; // assign a stringstream to write output to - proxyEvent.CreatePullPointSubscription(&request, response); - soap_write__tev__CreatePullPointSubscriptionResponse(soap, &response); - soap->os = old_stream; // no longer writing to the stream - Debug(1, "Response was %s", ss.str().c_str()); - - _wsnt__Unsubscribe wsnt__Unsubscribe; - _wsnt__UnsubscribeResponse wsnt__UnsubscribeResponse; - proxyEvent.Unsubscribe(response.SubscriptionReference.Address, nullptr, &wsnt__Unsubscribe, wsnt__UnsubscribeResponse); + + 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 { -#if 0 - std::stringstream ss; - soap->os = &ss; // assign a stringstream to write output to - int rc = proxyEvent.CreatePullPointSubscription(&request, response); - soap_write__tev__CreatePullPointSubscriptionResponse(soap, &response); - soap->os = NULL; // no longer writing to the stream - Debug(1, "Response was %s", ss.str().c_str()); -#endif - //Empty the stored messages + 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); - - RequestMessageID = parent->soap_wsa_compl ? soap_wsa_rand_uuid(soap) : nullptr; - if ((!parent->soap_wsa_compl) || (soap_wsa_request(soap, RequestMessageID, response.SubscriptionReference.Address, "PullMessageRequest") == SOAP_OK)) { - Debug(1, "ONVIF :soap_wsa_request OK "); - 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("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, "Good Initial ONVIF Pull%i %s, %s", soap->error, soap_fault_string(soap), soap_fault_detail(soap)); - healthy = true; - } - } else { - Error("ONVIF Couldn't set wsa 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)); - healthy = false; - } - - // we renew the current subscription ......... - if (parent->soap_wsa_compl) { - set_credentials(soap); + + if (use_wsa) { RequestMessageID = soap_wsa_rand_uuid(soap); - if (soap_wsa_request(soap, RequestMessageID, response.SubscriptionReference.Address, "RenewRequest") == SOAP_OK) { - Debug(1, "ONVIF :soap_wsa_request OK"); - 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(1, "Good Initial ONVIF Renew %i %s, %s", soap->error, soap_fault_string(soap), soap_fault_detail(soap)); - healthy = true; - } - } else { - Error("ONVIF Couldn't set wsa headers 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 + 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 { - Error("ONVIF Couldn't set wsa 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)); + // 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 + } + } // end else (success block) #else Error("zmc not compiled with GSOAP. ONVIF support not built in!"); #endif @@ -209,10 +324,24 @@ void Monitor::ONVIF::start() { void Monitor::ONVIF::WaitForMessage() { #ifdef WITH_GSOAP set_credentials(soap); - const char *RequestMessageID = parent->soap_wsa_compl ? soap_wsa_rand_uuid(soap) : "RequestMessageID"; - if ((!parent->soap_wsa_compl) || (soap_wsa_request(soap, RequestMessageID, response.SubscriptionReference.Address, "PullMessageRequest") == SOAP_OK)) { - Debug(1, ":soap_wsa_request OK; starting ONVIF PullMessageRequest ..."); - int result = proxyEvent.PullMessages(response.SubscriptionReference.Address, nullptr, &tev__PullMessages, tev__PullMessagesResponse); + + 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); @@ -220,137 +349,441 @@ void Monitor::ONVIF::WaitForMessage() { Error("Failed to get ONVIF messages! result=%d soap_fault_string=%s detail=%s", result, soap_fault_string(soap), (detail ? detail : "null")); - 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(1, "Response was %s", ss.str().c_str()); + 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 { - Debug(1, "Result of getting ONVIF PullMessageRequest result=%d soap_fault_string=%s detail=%s", + // 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"); - // EOF - std::unique_lock lck(alarms_mutex); - - if (!tev__PullMessagesResponse.wsnt__NotificationMessage.size()) { - if (!parent->Event_Poller_Closes_Event and alarmed) { - alarmed = false; - alarms.clear(); - } - } + + // 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); - if (!tev__PullMessagesResponse.wsnt__NotificationMessage.size()) { - if (!parent->Event_Poller_Closes_Event and alarmed) { - alarmed = false; - alarms.clear(); - } - } - + // 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) { - if ((msg->Topic != nullptr) && (msg->Topic->__any.text != nullptr) && - (msg->Message.__any.elts != nullptr) && - (msg->Message.__any.elts->next != nullptr) && - (msg->Message.__any.elts->next->elts != nullptr) && - (msg->Message.__any.elts->next->elts->atts != nullptr) && - (msg->Message.__any.elts->next->elts->atts->next != nullptr) && - (msg->Message.__any.elts->next->elts->atts->next->text != nullptr) - ) { - std::string topic = msg->Topic->__any.text; - std::string value = msg->Message.__any.elts->next->elts->atts->next->text; - - Debug(1, "ONVIF Got Motion Alarm! %s %s", last_topic.c_str(), last_value.c_str()); - if (parent->onvif_alarm_txt.empty() || std::strstr(topic.c_str(), parent->onvif_alarm_txt.c_str())) { - last_topic = topic; - last_value = value; - - Info("ONVIF Got Motion Alarm! topic:%s value:%s", last_topic.c_str(), last_value.c_str()); - // Apparently simple motion events, the value is boolean, but for people detection can be things like isMotion, isPeople - if (last_value.find("false") == 0 || last_value == "0") { - Info("Triggered off ONVIF"); - alarms.erase(last_topic); - Debug(1, "ONVIF Alarms Empty: Alarms count is %zu, alarmed is %s, empty is %d ", alarms.size(), alarmed ? "true": "false", alarms.empty()); - if (alarms.empty()) { - alarmed = false; - } - if (!parent->Event_Poller_Closes_Event) { //If we get a close event, then we know to expect them. - parent->Event_Poller_Closes_Event = true; - Info("Setting ClosesEvent"); - } - } else { - // Event Start - Info("Triggered Start on ONVIF"); - if (alarms.count(last_topic) == 0) { - alarms[last_topic] = last_value; - if (!alarmed) { - Info("Triggered Start Event on ONVIF"); - alarmed = true; - } - } else { - - } - } - Debug(1, "ONVIF Alarms count is %zu, alarmed is %s", alarms.size(), alarmed ? "true": "false"); - } else { - Debug(1, "ONVIF Got a message that didn't match onvif_alarm_txt. %s != %s", topic.c_str(), parent->onvif_alarm_txt.c_str()); + 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 { - Debug(1, "ONVIF Got a message that we couldn't parse. %s", ((msg->Topic && msg->Topic->__any.text) ? msg->Topic->__any.text : "null")); + // 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 (parent->soap_wsa_compl) { + if (use_wsa) { set_credentials(soap); - std::string Termination_time = "PT60S"; - wsnt__Renew.TerminationTime = &Termination_time; - RequestMessageID = parent->soap_wsa_compl ? soap_wsa_rand_uuid(soap) : "RequestMessageID"; - if ((!parent->soap_wsa_compl) || (soap_wsa_request(soap, RequestMessageID, response.SubscriptionReference.Address, "RenewRequest") == SOAP_OK)) { - Debug(1, ":soap_wsa_request OK"); + 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("Couldn't do Renew! Error %i %s, %s", soap->error, soap_fault_string(soap), soap_fault_detail(soap)); + 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(1, "Good Renew ONVIF Renew %i %s, %s", soap->error, soap_fault_string(soap), soap_fault_detail(soap)); + Debug(2, "ONVIF: Good Renew %i %s, %s", soap->error, soap_fault_string(soap), soap_fault_detail(soap)); healthy = true; } } else { - Error("Couldn't set wsa headers RequestMessageID=%s; TO=%s; Request= RenewRequest .... ! Error %i %s, %s", + 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 - } else { - Error("Couldn't set wsa 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)); - } // end if soap == 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); - soap_wsse_add_UsernameTokenDigest(soap, "Auth", - (parent->onvif_username.empty() ? parent->user.c_str() : parent->onvif_username.c_str()), - (parent->onvif_username.empty() ? parent->pass.c_str() : parent->onvif_password.c_str()) - ); + + 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