#!/usr/bin/perl -w
################################################
#
# SMS Dispatcher Server v0.3
#
# Written by: Michael Fung http://www.3open.org/
# Last Update: 2011-01-30
#
################################################
# load required modules
use strict;
use POSIX;
use DBI;
use Getopt::Long;
require Crypt::SSLeay;
use LWP::UserAgent;
# declare config file customizable variables
our ( $dbhost, $dbname, $dbuser, $dbpw,
$gw_userid, $gw_passwd, $gw_sendername,
$max_jobs
);
my $lockfile = "/var/run/smsd.pid";
my $logfile = "/var/log/smsd.log";
my $batch_interval = 30; # wait between each batch, in seconds
my $dbconn_retry_interval = 60; # seconds
my $min_process_interval = 10; # minimum time between each tries in seconds
my $GNOKII = '/usr/bin/gnokii';
# read in config file
do "/etc/smsd.conf" || die "Error reading configuration file!\n";
my $dsn = "DBI:mysql:database=$dbname;host=$dbhost;mysql_socket=/var/lib/mysql/mysql.sock";
# declare variables
my ( $s, $daemon, $verbose, $help, $usage,
$dbh, $now, $sql, $p, $q, $pnumrows, $qnumrows, $prow, $qrow,
$id, $hostgroup, $hostname, $sms_text, $phone_no, $sent, $error, $quota_id,
$last_process_time, $temp, $last_24hr
);
my $start_time = time();
my $gw_errors = 0;
my $local_errors = 0;
my $version = "0.3";
# define subs:
sub printlog {
my $msg = shift;
my $date_text = strftime "%Y-%m-%d %H:%M:%S", localtime;
my $logtext = "[" . $date_text . "] $msg\n";
if ($daemon) {
open(LOGFP, ">> $logfile");
print LOGFP "$logtext";
close(LOGFP);
} else {
print STDERR $logtext;
}
}
# send via Meteors SMS Gateway
sub send_gw {
my ($phone_no, $sms_text) = @_;
# url encode the sms text
$sms_text =~ s/([^A-Za-z0-9])/sprintf("%%%02X", ord($1))/seg;
my $url = "https://www.meteorsis.com/f_sendsms.aspx?username=$gw_userid&password=$gw_passwd&content=$sms_text&langeng=1&recipient=$phone_no&dos=now&senderid=$gw_sendername";
printlog("Gateway request: $url") if $verbose;
my $ua = LWP::UserAgent->new;
$ua->agent("SMS-Dispatcher/$version");
$ua->max_size(1024);
$ua->max_redirect(0);
my $response = $ua->get($url);
if ($response->is_success) {
if ($response->content =~ /SMSDID:/) {
$gw_errors = 0; # reset
return 1;
} else {
$gw_errors++;
printlog("Gateway server returned error: ". $response->content );
printlog("Consecutive errors count: $gw_errors");
return 0;
}
} else {
$gw_errors++;
printlog("Gateway server request failed: " . $response->status_line );
printlog("Consecutive errors count: $gw_errors");
return 0;
}
}
# send via local cell phone
sub send_local {
my ($phone_no, $sms_text) = @_;
if (system("/bin/echo '$sms_text' | $GNOKII --sendsms $phone_no") == 0) {
$local_errors = 0;
return 1;
} else {
$local_errors++;
return 0;
}
}
# program exit
sub prg_exit {
printlog("*** SMS Dispatcher Shutdown ***");
unlink "$lockfile";
exit;
}
$SIG{HUP} = \&prg_exit;
$SIG{TERM} = \&prg_exit;
$SIG{INT} = \&prg_exit;
sub syntax {
$s = shift or $s = 'Unknown';
$usage = <<EOT;
SMS Dispatcher Server v$version
(C) 2010 Michael Fung http://www.3open.org/ All rights reserved.
Licensed under the GPL.
Syntax: smsd.pl [-h] [-d] [-v]
-h Show help
-d Run in daemon mode
-v Verbose output
EOT
printf STDERR ("Error: $s\n\n") unless ($help);
print STDERR $usage;
exit(0) if $help;
exit(1);
}
# database connection
sub dbconn {
printlog("Connecting to database...") if $verbose;
# check if already connected
return 1 if (defined($dbh) && $dbh->{Active});
if ($dbh = DBI->connect($dsn, $dbuser, $dbpw, { RaiseError => 0, AutoCommit => 1 })) {
return 1;
} else {
printlog("$DBI::errstr");
return 0;
}
}
sub set_done {
my ($id, $error) = @_;
$dbh->do("update sms_queue set done='1', error='$error', last_update='$now' where id='$id'");
}
# daemonize
sub daemonize {
if (my $pid = fork) { exit 0; } # exit the parent process
POSIX::setsid() or die "FATAL ERROR: Can't daemonize!";
chdir "/";
umask 0;
open(STDIN, "+>/dev/null");
open(STDOUT, "+>&STDIN");
open(STDERR, "+>&STDIN");
}
###################
###################
# Main Entry
###################
###################
# avoid duplicate process
if (-e $lockfile) {
print STDERR "ERROR: Another copy of smsd is running\n";
exit;
}
Getopt::Long::Configure('bundling');
GetOptions(
"v" => \$verbose,
"d" => \$daemon,
"h" => \$help, "help" => \$help
) || syntax("Invalid option(s)");
# syntax checking
syntax if ($help);
printlog("*** SMS Dispatcher Server v$version started ***");
# daemonize
if ($daemon) {
daemonize(); # switch to daemon mode
printlog("Switched to the background, pid:$$") if $verbose;
}
# setup lock file
open(LF, "> $lockfile");
print LF "$$\n";
close(LF);
# begin endless loop
while (1) {
# connect to database
if (!dbconn()) {
printlog("Database connection failed!");
#prg_exit;
sleep($dbconn_retry_interval);
next;
};
# clear expired jobs
$now = time();
$dbh->do("update sms_queue set done='1', error='Job expired.', last_update='$now' where ('$now' > not_after) and (done !='1')");
# check jobs sent in last 24hrs
$last_24hr = time() - 86400;
$sql = <<SQL;
select count(id) as jobs from sms_done_queue
where ('sent_date' > '$last_24hr')
and (sent = '1')
SQL
printlog("Check last 24hrs jobs SQL: $sql") if $verbose;
$q = $dbh->prepare($sql);
$q->execute;
$qrow = $q->fetchrow_hashref;
$jobs = $qrow->{jobs};
if ($jobs > $max_jobs) {
printlog("WARNING: Max jobs per day reached. Jobs done: $jobs.");
# Something is wrong, adjust batch interval to 60 minutes
# optionally alert administrator
# and don't process the queue
$batch_interval = 3600;
# disconnect db and take a nap
$q->finish;
$dbh->disconnect;
sleep($batch_interval);
next;
}
printlog("Jobs done in last 24hrs: $jobs.") if $verbose;
# get jobs to be processed
$now = time();
$last_process_time = $now - $min_process_interval; # prevent fast looping
$sql = <<SQL;
select * from sms_queue
where ('$now' < not_after or not_after is null)
and ('$now' > not_before or not_before is null)
and (last_update < '$last_process_time' or last_update is null)
and (done != '1')
SQL
printlog("Get jobs SQL: $sql") if $verbose;
$q = $dbh->prepare($sql);
$q->execute;
$qnumrows = $q->rows;
printlog("Messages to process: $qnumrows") if $verbose;
# do some housekeeping and sleep some time if no jobs
if ($qnumrows == 0) {
# housekeeping: relocate done jobs
$dbh->do("lock tables sms_queue write, sms_done_queue write");
if ($dbh->do("insert into sms_done_queue select * from sms_queue where sms_queue.done='1'") > 0) {
$dbh->do("delete from sms_queue where done='1'");
}
$dbh->do("unlock tables");
# disconnect db and take a nap
$q->finish;
$dbh->disconnect;
sleep($batch_interval);
next;
}
# loop through each message
while ($qrow = $q->fetchrow_hashref) {
$id = $qrow->{id};
$phone_no = $qrow->{phone_no};
$sms_text = $qrow->{sms_text};
$sent = $qrow->{sent};
$now = time();
$error = '';
printlog("Processing ID:$id") if $verbose;
# catch inconsistency
if ($sent) {
printlog("ID:$id OK. Already sent, just mark it as done.");
set_done($id, $error);
next;
}
# try to send
if (send_gw($phone_no, $sms_text)) {
printlog("ID:$id OK. Sent via external gateway.");
$now = time();
$dbh->do("update sms_queue set done='1', sent='1', sent_date='$now', send_via='G', tries=tries+1, last_update='$now' where id='$id'");
}
elsif (send_local($phone_no, $sms_text)) {
printlog("ID:$id OK. Sent via local cell phone.");
$now = time();
$dbh->do("update sms_queue set done='1', sent='1', sent_date='$now', send_via='L', tries=tries+1, last_update='$now' where id='$id'");
}
else {
printlog("ID:$id ERROR: Send failed with all methods.");
$now = time();
$dbh->do("update sms_queue set tries=tries+1, last_update='$now' where id='$id'");
}
} # end db records loop
# clean up
$q->finish;
} # end endless loop
Back to top