mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2025-12-23 22:37:53 -05:00
Implement flexible message parsing, auth fallback, and improved WS-Addressing
Co-authored-by: connortechnology <925519+connortechnology@users.noreply.github.com>
This commit is contained in:
@@ -345,6 +345,14 @@ class Monitor : public std::enable_shared_from_this<Monitor> {
|
||||
_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
|
||||
std::string discovered_event_endpoint; // Store discovered endpoint
|
||||
|
||||
// Helper methods
|
||||
bool parse_event_message(struct _wsnt__NotificationMessage *msg, std::string &topic, std::string &value, std::string &operation);
|
||||
bool matches_topic_filter(const std::string &topic, const std::string &filter);
|
||||
void log_soap_request_response(const char *operation);
|
||||
#endif
|
||||
std::unordered_map<std::string, std::string> alarms;
|
||||
std::mutex alarms_mutex;
|
||||
|
||||
@@ -45,6 +45,8 @@ Monitor::ONVIF::ONVIF(Monitor *parent_) :
|
||||
,healthy(false)
|
||||
#ifdef WITH_GSOAP
|
||||
,soap(nullptr)
|
||||
,try_usernametoken_auth(false)
|
||||
,retry_count(0)
|
||||
#endif
|
||||
{
|
||||
}
|
||||
@@ -52,20 +54,31 @@ Monitor::ONVIF::ONVIF(Monitor *parent_) :
|
||||
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);
|
||||
@@ -88,7 +101,12 @@ void Monitor::ONVIF::start() {
|
||||
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,106 +118,159 @@ 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;
|
||||
} 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
|
||||
set_credentials(soap);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Debug(1, "ONVIF: Creating PullPoint subscription at endpoint: %s", proxyEvent.soap_endpoint);
|
||||
int rc = proxyEvent.CreatePullPointSubscription(&request, response);
|
||||
|
||||
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));
|
||||
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) {
|
||||
Error("ONVIF: Plain authentication also failed. Error %d: %s", 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());
|
||||
}
|
||||
|
||||
soap_destroy(soap);
|
||||
soap_end(soap);
|
||||
soap_free(soap);
|
||||
soap = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
Info("ONVIF: Plain authentication succeeded");
|
||||
} else {
|
||||
// Not an auth error or already tried plain auth
|
||||
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());
|
||||
}
|
||||
|
||||
soap_destroy(soap);
|
||||
soap_end(soap);
|
||||
soap_free(soap);
|
||||
soap = nullptr;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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;
|
||||
Debug(2, "ONVIF: Good Initial Renew %i %s, %s", soap->error, soap_fault_string(soap), soap_fault_detail(soap));
|
||||
healthy = true;
|
||||
}
|
||||
|
||||
// we renew the current subscription .........
|
||||
if (parent->soap_wsa_compl) {
|
||||
set_credentials(soap);
|
||||
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
|
||||
}
|
||||
}
|
||||
} 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));
|
||||
} 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!");
|
||||
@@ -209,10 +280,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,124 +305,140 @@ 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 (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());
|
||||
}
|
||||
|
||||
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<std::mutex> 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");
|
||||
}
|
||||
} else {
|
||||
Debug(1, "ONVIF polling : Got Good Response! %i, # of messages %zu", result, tev__PullMessagesResponse.wsnt__NotificationMessage.size());
|
||||
{ // Scope for lock
|
||||
std::unique_lock<std::mutex> 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");
|
||||
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;
|
||||
}
|
||||
@@ -347,10 +448,209 @@ void Monitor::ONVIF::WaitForMessage() {
|
||||
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(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
|
||||
if (std::strstr(att->name, "PropertyOperation")) {
|
||||
operation = att->text;
|
||||
Debug(3, "ONVIF: Found PropertyOperation: %s", operation.c_str());
|
||||
}
|
||||
}
|
||||
att = att->next;
|
||||
}
|
||||
}
|
||||
|
||||
// Look for SimpleItem or ElementItem
|
||||
if (elt->name) {
|
||||
if (std::strstr(elt->name, "SimpleItem")) {
|
||||
// SimpleItem has Value attribute
|
||||
if (elt->atts) {
|
||||
struct soap_dom_attribute *att = elt->atts;
|
||||
while (att) {
|
||||
if (att->name && att->text && std::strstr(att->name, "Value")) {
|
||||
value = att->text;
|
||||
Debug(3, "ONVIF: Found SimpleItem Value: %s", value.c_str());
|
||||
return true;
|
||||
}
|
||||
att = att->next;
|
||||
}
|
||||
}
|
||||
} else if (std::strstr(elt->name, "ElementItem")) {
|
||||
// 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::strstr(elt->name, "Data")) {
|
||||
// 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
|
||||
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<std::string> topic_parts;
|
||||
std::vector<std::string> 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 == "**" || filter_part.find("/*") != std::string::npos) {
|
||||
// Multi-level wildcard - matches rest of topic
|
||||
return true;
|
||||
} 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();
|
||||
}
|
||||
|
||||
// Helper function to log SOAP requests/responses for debugging
|
||||
void Monitor::ONVIF::log_soap_request_response(const char *operation) {
|
||||
if (config.log_level >= 3) {
|
||||
std::stringstream ss;
|
||||
std::ostream *old_stream = soap->os;
|
||||
soap->os = &ss;
|
||||
|
||||
Debug(3, "ONVIF: SOAP request/response for %s:", operation);
|
||||
// Note: Actual request/response logging would require intercepting at a lower level
|
||||
// This is a placeholder for the logging structure
|
||||
|
||||
soap->os = old_stream;
|
||||
}
|
||||
}
|
||||
|
||||
//GSOAP boilerplate
|
||||
|
||||
Reference in New Issue
Block a user