Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
zmeventnotification.pl
#!/usr/bin/perl -T
#
# ==========================================================================
#
# HEAVILY HACKED-UP VERSION OF zmeventnotification.pl from
# https://github.com/pliablepixels/zmeventserver/blob/master/zmeventnotification.pl
# as of b31b08f
#
# Modified to just execute a system() shell command and pass it the EventId, MonitorId and Cause.
# The command is run with "&" appended to background it and return control to Perl so we don't
# miss events while the command runs.
#
# Edit line 405 to set the command to run.
#
# DISCLAIMER: I haven't written a line of Perl in a decade. I really don't know what
# I'm doing. About 90% of this script is COMPLETELY unused and leftover from the upstream
# code. If anyone knows enough Perl to clean this up and keep it working, I'd certainly
# appreciate the assistance.
#
# ==========================================================================
#
# ZoneMinder Realtime Notification System
#
# A light weight event notification daemon
# Uses shared memory to detect new events (polls SHM)
# Also opens a websocket connection at a configurable port
# so events can be reported
# Any client can connect to this web socket and handle it further
# for example, send it out via APNS/GCM or any other mechanism
#
# This is a much faster and low overhead method compared to zmfilter
# as there is no DB overhead nor SQL searches for event matches
# ~ PP
#
# 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# ==========================================================================
use strict;
use bytes;
# ==========================================================================
#
# Starting v1.0, configuration has moved to a separate file, please make sure
# you see README
#
# Starting v0.95, I've moved to FCM which means I no longer need to maintain
# my own push server. Plus this uses HTTP which is the new recommended
# way. Note that 0.95 will only work with zmNinja 1.2.510 and beyond
# Conversely, old versions of the event server will NOT work with zmNinja
# 1.2.510 and beyond, so make sure you upgrade both
#
# ==========================================================================
my $app_version="1.0";
# ==========================================================================
#
# These are app defaults
# Note that you really should not have to to change these values.
# It is better you change them inside the ini file.
# These values are used ONLY if the server cannot find its ini file
# The only one you may want to change is DEFAULT_CONFIG_FILE to point
# to your custom ini file if you don't use --config. The rest should
# go into that config file.
# ==========================================================================
use constant DEFAULT_CONFIG_FILE => "/etc/zmeventnotification.ini";
use constant DEFAULT_PORT => 9000;
use constant DEFAULT_ADDRESS => '[::]';
use constant DEFAULT_AUTH_ENABLE => 1;
use constant DEFAULT_AUTH_TIMEOUT => 20;
use constant DEFAULT_FCM_ENABLE => 1;
use constant DEFAULT_FCM_TOKEN_FILE => '/etc/private/tokens.txt';
use constant DEFAULT_SSL_ENABLE => 1;
use constant DEFAULT_CUSTOMIZE_VERBOSE => 0;
use constant DEFAULT_CUSTOMIZE_EVENT_CHECK_INTERVAL => 3;
use constant DEFAULT_CUSTOMIZE_MONITOR_RELOAD_INTERVAL => 300;
use constant DEFAULT_CUSTOMIZE_READ_ALARM_CAUSE => 0;
use constant DEFAULT_CUSTOMIZE_TAG_ALARM_EVENT_ID => 0;
use constant DEFAULT_CUSTOMIZE_USE_CUSTOM_NOTIFICATION_SOUND => 0;
# Declare options.
my $help;
my $config_file;
my $config_file_present;
my $check_config;
my $port;
my $address;
my $auth_enabled;
my $auth_timeout;
my $use_fcm;
my $fcm_api_key;
my $token_file;
my $ssl_enabled;
my $ssl_cert_file;
my $ssl_key_file;
my $verbose;
my $event_check_interval;
my $monitor_reload_interval;
my $read_alarm_cause;
my $tag_alarm_event_id;
my $use_custom_notification_sound;
#default key. Please don't change this
use constant NINJA_API_KEY => "AAAApYcZ0mA:APA91bG71SfBuYIaWHJorjmBQB3cAN7OMT7bAxKuV3ByJ4JiIGumG6cQw0Bo6_fHGaWoo4Bl-SlCdxbivTv5Z-2XPf0m86wsebNIG15pyUHojzmRvJKySNwfAHs7sprTGsA_SIR_H43h";
my $dummyEventTest = 0; # if on, will generate dummy events. Not in config for a reason. Only dev testing
my $dummyEventInterval = 30; # timespan to generate events in seconds
my $dummyEventTimeLastSent = time();
# This part makes sure we have the right deps
if (!try_use ("Net::WebSocket::Server")) {Fatal ("Net::WebSocket::Server missing");}
if (!try_use ("IO::Socket::SSL")) {Fatal ("IO::Socket::SSL missing");}
if (!try_use ("Config::IniFiles")) {Fatal ("Config::Inifiles missing");}
if (!try_use ("Getopt::Long")) {Fatal ("Getopt::Long missing");}
if (!try_use ("File::Basename")) {Fatal ("File::Basename missing");}
if (!try_use ("File::Spec")) {Fatal ("File::Spec missing");}
if (!try_use ("Crypt::MySQL qw(password password41)")) {Fatal ("Crypt::MySQL missing");}
if (!try_use ("JSON"))
{
if (!try_use ("JSON::XS"))
{ Fatal ("JSON or JSON::XS missing");exit (-1);}
}
# Fetch whatever options are available from CLI arguments.
use constant USAGE => <<'USAGE';
Usage: zmeventnotification.pl [OPTION]...
--help Print this page.
--config=FILE Read options from configuration file (default: /etc/zmeventnotification.ini).
Any CLI options used below will override config settings.
--check-config Print configuration and exit.
--port=PORT Port for Websockets connection (default: 9000).
--address=ADDRESS Address for Websockets server (default: [::]).
--enable-auth Check username/password against ZoneMinder database (default: true).
--no-enable-auth Don't check username/password against ZoneMinder database (default: false).
--enable-fcm Use FCM for messaging (default: true).
--no-enable-fcm Don't use FCM for messaging (default: false).
--fcm-api-key=KEY API key for FCM (default: zmNinja FCM key).
--token-file=FILE Auth token store location (default: /etc/private/tokens.txt).
--enable-ssl Enable SSL (default: true).
--no-enable-ssl Disable SSL (default: false).
--ssl-cert-file=FILE Location to SSL cert file.
--ssl-key-file=FILE Location to SSL key file.
--verbose Display messages to console (default: false).
--no-verbose Don't display messages to console (default: true).
--event-check-interval=SECONDS Interval, in seconds, after which we will check for new events (default: 5).
--monitor-reload-interval=SECONDS Interval, in seconds, to reload known monitors (default: 300).
--read-alarm-cause Read monitor alarm cause (Requires ZoneMinder >= 1.31.2, default: false).
--no-read-alarm-cause Don't read monitor alarm cause (default: true).
--tag-alarm-event-id Tag event IDs with the alarm (default: false).
--no-tag-alarm-event-id Don't tag event IDs with the alarm (default: true).
--use-custom-notification-sound Use custom notification sound (default: true).
--no-use-custom-notification-sound Don't use custom notification sound (default: false).
USAGE
GetOptions(
"help" => \$help,
"config=s" => \$config_file,
"check-config" => \$check_config,
"port=i" => \$port,
"address=s" => \$address,
"enable-auth!" => \$auth_enabled,
"enable-fcm!" => \$use_fcm,
"fcm-api-key=s" => \$fcm_api_key,
"token-file=s" => \$token_file,
"enable-ssl!" => \$ssl_enabled,
"ssl-cert-file=s" => \$ssl_cert_file,
"ssl-key-file=s" => \$ssl_key_file,
"verbose!" => \$verbose,
"event-check-interval=i" => \$event_check_interval,
"monitor-reload-interval=i" => \$monitor_reload_interval,
"read-alarm-cause!" => \$read_alarm_cause,
"tag-alarm-event-id!" => \$tag_alarm_event_id,
"use-custom-notification-sound!" => \$use_custom_notification_sound
);
exit(print(USAGE)) if $help;
# Read options from a configuration file. If --config is specified, try to
# read it and fail if it can't be read. Otherwise, try the default
# configuration path, and if it doesn't exist, take all the default values by
# loading a blank Config::IniFiles object.
if (! $config_file) {
$config_file = DEFAULT_CONFIG_FILE;
$config_file_present = -e $config_file;
} else {
if ( ! -e $config_file) {
Fatal ("$config_file does not exist!");
}
$config_file_present = 1;
}
my $config;
if ($config_file_present) {
Info ("using config file: $config_file");
$config = Config::IniFiles->new(-file => $config_file);
unless ($config) {
Fatal(
"Encountered errors while reading $config_file:\n" .
join("\n", @Config::IniFiles::errors)
);
}
} else {
$config = Config::IniFiles->new;
Info ("No config file found, using inbuilt defaults");
}
# If an option set a value, leave it. If there's a value in the config, use
# it. Otherwise, use a default value if it's available.
$port //= config_get_val($config, "network", "port", DEFAULT_PORT);
$address //= config_get_val($config, "network", "address", DEFAULT_ADDRESS);
$auth_enabled //= config_get_val($config, "auth", "enable", DEFAULT_AUTH_ENABLE);
$auth_timeout //= config_get_val($config, "auth", "timeout", DEFAULT_AUTH_TIMEOUT);
$use_fcm //= config_get_val($config, "fcm", "enable", DEFAULT_FCM_ENABLE);
$fcm_api_key //= config_get_val($config, "fcm", "api_key", NINJA_API_KEY);
$token_file //= config_get_val($config, "fcm", "token_file", DEFAULT_FCM_TOKEN_FILE);
$ssl_enabled //= config_get_val($config, "ssl", "enable", DEFAULT_SSL_ENABLE);
$ssl_cert_file //= config_get_val($config, "ssl", "cert");
$ssl_key_file //= config_get_val($config, "ssl", "key");
$verbose //= config_get_val($config, "customize", "verbose", DEFAULT_CUSTOMIZE_VERBOSE);
$event_check_interval //= config_get_val($config, "customize", "event_check_interval", DEFAULT_CUSTOMIZE_EVENT_CHECK_INTERVAL);
$monitor_reload_interval //= config_get_val($config, "customize", "monitor_reload_interval", DEFAULT_CUSTOMIZE_MONITOR_RELOAD_INTERVAL);
$read_alarm_cause //= config_get_val($config, "customize", "read_alarm_cause", DEFAULT_CUSTOMIZE_READ_ALARM_CAUSE);
$tag_alarm_event_id //= config_get_val($config, "customize", "tag_alarm_event_id", DEFAULT_CUSTOMIZE_TAG_ALARM_EVENT_ID);
$use_custom_notification_sound //= config_get_val($config, "customize", "use_custom_notification_sound", DEFAULT_CUSTOMIZE_USE_CUSTOM_NOTIFICATION_SOUND);
my %ssl_push_opts = ();
if ($ssl_enabled && (!$ssl_cert_file || !$ssl_key_file)) {
Fatal ("SSL is enabled, but key or certificate file is missing");
}
my $notId = 1;
use constant PENDING_WEBSOCKET => '1';
use constant INVALID_WEBSOCKET => '-1';
use constant INVALID_APNS => '-2';
use constant INVALID_AUTH => '-3';
use constant VALID_WEBSOCKET => '0';
# this is just a wrapper around Config::IniFiles val
# older versions don't support a default parameter
sub config_get_val {
my ( $config, $sect, $parm, $def ) = @_;
my $val = $config->val($sect, $parm);
return defined($val)? $val:$def;
}
sub true_or_false {
return $_[0] ? "true" : "false";
}
sub value_or_undefined {
return $_[0] || "(undefined)";
}
sub present_or_not {
return $_[0] ? "(defined)" : "(undefined)";
}
sub print_config {
my $abs_config_file = File::Spec->rel2abs($config_file);
print(<<"EOF"
${\(
$config_file_present ?
"Configuration (read $abs_config_file)" :
"Default configuration ($abs_config_file doesn't exist)"
)}:
Port .......................... ${\(value_or_undefined($port))}
Address ....................... ${\(value_or_undefined($address))}
Event check interval .......... ${\(value_or_undefined($event_check_interval))}
Monitor reload interval ....... ${\(value_or_undefined($monitor_reload_interval))}
Auth enabled .................. ${\(true_or_false($auth_enabled))}
Auth timeout .................. ${\(value_or_undefined($auth_timeout))}
Use FCM ....................... ${\(true_or_false($use_fcm))}
FCM API key ................... ${\(present_or_not($fcm_api_key))}
Token file .................... ${\(value_or_undefined($token_file))}
SSL enabled ................... ${\(true_or_false($ssl_enabled))}
SSL cert file ................. ${\(value_or_undefined($ssl_cert_file))}
SSL key file .................. ${\(value_or_undefined($ssl_key_file))}
Verbose ....................... ${\(true_or_false($verbose))}
Read alarm cause .............. ${\(true_or_false($read_alarm_cause))}
Tag alarm event id ............ ${\(true_or_false($tag_alarm_event_id))}
Use custom notification sound . ${\(true_or_false($use_custom_notification_sound))}
EOF
)
}
exit(print_config()) if $check_config;
print_config() if $verbose;
# ==========================================================================
#
# Don't change anything below here
#
# ==========================================================================
use lib '/usr/local/lib/x86_64-linux-gnu/perl5';
use ZoneMinder;
use POSIX;
use DBI;
use Data::Dumper;
$| = 1;
$ENV{PATH} = '/bin:/usr/bin';
$ENV{SHELL} = '/bin/sh' if exists $ENV{SHELL};
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
sub Usage
{
print( "This daemon is not meant to be invoked from command line\n");
exit( -1 );
}
logInit();
logSetSignal();
my $dbh = zmDbConnect();
my %monitors;
my $monitor_reload_time = 0;
my $apns_feedback_time = 0;
my $proxy_reach_time=0;
my $wss;
my @events=();
my @active_connections=();
my $alarm_header="";
my $alarm_mid="";
my $alarm_eid="";
# MAIN
printdbg ("******You are running version: $app_version");
Info( "Event Notification daemon v $app_version starting\n" );
my $res;
while (1) {
$res = checkEvents();
printdbg("Result: $res");
foreach my $evt (@events) {
Info( "Event: " . Dumper($evt) );
printdbg("call");
system("/usr/local/bin/zmevent_handler.py -E $evt->{EventId} -M $evt->{MonitorId} -C '$evt->{Cause}' &");
printdbg("after call");
}
sleep $event_check_interval;
}
Info( "Event Notification daemon exiting\n" );
exit();
# Try to load a perl module
# and if it is not available
# generate a log
sub try_use
{
my $module = shift;
eval("use $module");
return($@ ? 0:1);
}
# console print
sub printdbg
{
my $a = shift;
my $now = strftime('%Y-%m-%d,%H:%M:%S',localtime);
print($now," ",$a, "\n") if $verbose;
}
# This function uses shared memory polling to check if
# ZM reported any new events. If it does find events
# then the details are packaged into the events array
# so they can be JSONified and sent out
sub checkEvents()
{
my $eventFound = 0;
if ( (time() - $monitor_reload_time) > $monitor_reload_interval )
{
Debug ("Reloading Monitors...");
foreach my $monitor (values(%monitors))
{
zmMemInvalidate( $monitor );
}
loadMonitors();
}
@events = ();
$alarm_header = "";
$alarm_mid="";
$alarm_eid = ""; # only take 1 if several occur
foreach my $monitor ( values(%monitors) )
{
my $alarm_cause="";
my ( $state, $last_event, $trigger_cause, $trigger_text)
= zmMemRead( $monitor,
[ "shared_data:state",
"shared_data:last_event",
"trigger_data:trigger_cause",
"trigger_data:trigger_text",
]
);
if ($state == STATE_ALARM || $state == STATE_ALERT)
{
Debug ("state is STATE_ALARM or ALERT for ".$monitor->{Name});
if ( !defined($monitor->{LastEvent})
|| ($last_event != $monitor->{LastEvent}))
{
$alarm_cause=zmMemRead($monitor,"shared_data:alarm_cause") if ($read_alarm_cause);
$alarm_cause = $trigger_cause if (defined($trigger_cause) && $alarm_cause eq "" && $trigger_cause ne "");
printdbg ("Unified Alarm details: $alarm_cause");
Info( "New event $last_event reported for ".$monitor->{Name}." ".$alarm_cause."\n");
$monitor->{LastState} = $state;
$monitor->{LastEvent} = $last_event;
my $name = $monitor->{Name};
my $mid = $monitor->{Id};
my $eid = $last_event;
Debug ("Creating event object for ".$monitor->{Name}." with $last_event");
push @events, {Name => $name, MonitorId => $mid, EventId => $last_event, Cause=> $alarm_cause};
$alarm_eid = $last_event;
$alarm_header = "Alarms: " if (!$alarm_header);
$alarm_header = $alarm_header . $name ;
$alarm_header = $alarm_header." ".$alarm_cause if (defined $alarm_cause);
$alarm_header = $alarm_header." ".$trigger_cause if (defined $trigger_cause);
$alarm_mid = $alarm_mid.$mid.",";
$alarm_header = $alarm_header . " (".$last_event.") " if ($tag_alarm_event_id);
$alarm_header = $alarm_header . "," ;
$eventFound = 1;
}
}
}
chop($alarm_header) if ($alarm_header);
chop ($alarm_mid) if ($alarm_mid);
# Send out dummy events for testing
if (!$eventFound && $dummyEventTest && (time() - $dummyEventTimeLastSent) >= $dummyEventInterval ) {
$dummyEventTimeLastSent = time();
my $random_mon = $monitors{(keys %monitors)[rand keys %monitors]};
Info ("Sending dummy event to: ".$random_mon->{Name});
push @events, {Name => $random_mon->{Name}, MonitorId => $random_mon->{Id}, EventId => $random_mon->{LastEvent}, Cause=> "Dummy"};
$alarm_header = "Alarms: Dummy alarm at ".$random_mon->{Name};
$alarm_mid = $random_mon->{Id};
$eventFound = 1;
}
return ($eventFound);
}
# Refreshes list of monitors from DB
#
sub loadMonitors
{
Debug ( "Loading monitors\n" );
$monitor_reload_time = time();
my %new_monitors = ();
my $sql = "SELECT * FROM Monitors
WHERE find_in_set( Function, 'Modect,Mocord,Nodect' )".
( $Config{ZM_SERVER_ID} ? 'AND ServerId=?' : '' );
Debug ("SQL to be executed is :$sql");
my $sth = $dbh->prepare_cached( $sql )
or Fatal( "Can't prepare '$sql': ".$dbh->errstr() );
my $res = $sth->execute( $Config{ZM_SERVER_ID} ? $Config{ZM_SERVER_ID} : () )
or Fatal( "Can't execute: ".$sth->errstr() );
while( my $monitor = $sth->fetchrow_hashref() )
{
if ( !zmMemVerify( $monitor ) ) {
zmMemInvalidate( $monitor );
next;
}
# next if ( !zmMemVerify( $monitor ) ); # Check shared memory ok
if ( defined($monitors{$monitor->{Id}}->{LastState}) )
{
$monitor->{LastState} = $monitors{$monitor->{Id}}->{LastState};
}
else
{
$monitor->{LastState} = zmGetMonitorState( $monitor );
}
if ( defined($monitors{$monitor->{Id}}->{LastEvent}) )
{
$monitor->{LastEvent} = $monitors{$monitor->{Id}}->{LastEvent};
}
else
{
$monitor->{LastEvent} = zmGetLastEvent( $monitor );
}
$new_monitors{$monitor->{Id}} = $monitor;
}
%monitors = %new_monitors;
}
# Checks if the monitor for which
# an alarm occurred is part of the monitor list
# for that connection
sub getInterval
{
my $intlist = shift;
my $monlist = shift;
my $mid = shift;
#print ("getInterval:MID:$mid INT:$intlist AND MON:$monlist\n");
my @ints = split (',',$intlist);
my @mids = split (',',$monlist);
my $idx = -1;
foreach (@mids)
{
$idx++;
#print ("Comparing $mid with $_\n");
if ($mid eq $_)
{
last;
}
}
#print ("RETURNING index:$idx with Value:".$ints[$idx]."\n");
return $ints[$idx];
}
# Checks if the monitor for which
# an alarm occurred is part of the monitor list
# for that connection
sub isInList
{
my $monlist = shift;
my $mid = shift;
my @mids = split (',',$monlist);
my $found = 0;
foreach (@mids)
{
if ($mid eq $_)
{
$found = 1;
last;
}
}
return $found;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment