Created
July 5, 2018 17:14
-
-
Save jantman/b10fd9808065d36001a0d31f95830976 to your computer and use it in GitHub Desktop.
zmeventnotification.pl
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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