#!/usr/bin/perl -w
require 5.003;
use strict;
#
# connects to specified BEEP servers,
# starts channel #1, receives BEEP messages,
# and forwards message content to an external program
#
my $TheSelect;
my $TheHandler;
package Client;
use strict;
use Fcntl;
use IO::Socket;
my @ConnectingClients = ();
sub new {
my ($proto, $addr, $sock) = @_;
die() unless @_ == 2 || @_ == 3;
my $type = ref($proto) || $proto;
my $this = {};
bless ($this, $type);
$this->{theAddr} = $addr;
$this->{theSock} = $sock;
$this->{theBufIn} = '';
$this->{doReconnect} = ! defined($sock);
if ($this->{theSock}) {
$this->noteConnected();
} else {
$this->connectLater();
}
return $this;
}
sub action {
my $this = shift;
return unless defined $this->{theAction};
my $action = $this->{theAction};
$this->{theAction} = undef();
&{$action}($this);
}
sub connect {
my $this = shift;
$this->log("connecting...", 3);
my $s = $this->{theSock} = new IO::Socket();
$this->mydie($!) unless defined $s;
my ($remote) = ($this->{theAddr} =~ /^(.*):/);
my ($port) = ($this->{theAddr} =~ /:(\d+)$/);
$this->mydie("malformed server address") unless $remote && $port;
my $iaddr = inet_aton($remote) || $this->mydie("no host: $remote");
my $paddr = sockaddr_in($port, $iaddr);
my $proto = getprotobyname('tcp');
$s->socket(PF_INET, SOCK_STREAM, $proto) || $this->mydie("socket: $!");
if (!$s->connect($paddr)) {
$this->mydie() unless $this->{doReconnect};
$this->log("connect failed, will retry");
$this->connectLater();
} else {
$this->log("connected", 3);
$this->noteConnected();
}
}
sub noteConnected {
my $this = shift;
my $s = $this->{theSock} || die();
$s->print("RPY 0 0 . 0 11\r\n");
$s->print('');
$s->print("END\r\n");
my $start =
"\n".
" \n".
"\n";
$s->printf("MSG 0 1 . 11 %d\r\n", length($start));
$s->print($start);
$s->print("END\r\n");
fcntl($s, F_SETFL, O_NONBLOCK) || $this->mydie("fcntl: $!");
$TheSelect->add([$s, $this]);
}
sub noteReadReady {
my $this = shift;
my $s = $this->{theSock};
if (my @newText = $s->getlines()) {
$this->{theBufIn} .= join('', @newText);
$this->noteContent();
} else {
$TheSelect->remove($s);
$s->close();
$this->{theSock} = undef();
if ($this->{doReconnect}) {
$this->log("disconnected, will try to reconnect");
$this->connectLater();
} else {
$this->log("disconnected");
}
}
}
sub noteContent {
my $this= shift;
while ($this->{theBufIn} =~ s|]*)/>||m) {
my $cnt = $&;
$this->{theBufIn} = $';
$this->log("received: $cnt", 2);
$this->handle($cnt);
chomp($cnt);
print(STDOUT "$cnt\n");
}
}
sub handle {
my ($this, $msg) = @_;
return unless defined $TheHandler;
my %savedENV = %ENV;
while (length($msg)) {
$msg =~ s|^[^<]+||;
while ($msg =~ s~^.*?<(\w+)\s+(\w+)=(?:\'([^\']*)\'|\"([^\"]*)\")\s*~<$1 ~m) {
$ENV{"$1__$2"} = $3;
# print("\$ENV{\"$1__$2\"} = $3\n");
}
while ($msg =~ s|^.*?<(\w+)\s*/>||m) {
$ENV{"$1__"} = $1;
# print("\$ENV{\"$1__\"} = $1;\n");
}
}
system($TheHandler) == 0 or $this->mydie("$TheHandler failed: $!");
%ENV = %savedENV;
}
sub connectLater {
my $this = shift;
$this->{theAction} = \&connect;
push @Client::ConnectingClients, $this;
}
sub log {
my ($this, $msg, $lvl) = @_;
&Log($this->{theAddr}. ": $msg", $lvl);
}
sub mydie {
my $this = shift;
$this->log('fatal error: '. join(' ', @_), -1);
die("\n");
}
1;
use IO::Select;
my $VerbLevel = 1;
my @Clients = ();
my @TheDoormanAddrs = ();
my $TheListenAddr;
&getOpts();
die(&usage()) unless @TheDoormanAddrs || $TheListenAddr;
$SIG{__WARN__} = sub { print(STDERR &usage()); die $_[0] };
exit(&main());
sub main {
$TheSelect = new IO::Select;
foreach my $addr (map { (&range2arr($_)) } @TheDoormanAddrs) {
push @Clients, new Client($addr);
}
die() unless @Clients == @Client::ConnectingClients;
&startServer($TheListenAddr) if $TheListenAddr;
while (1) {
&checkIo();
&checkClients();
select(undef, undef, undef, 1) unless $TheSelect->count();
}
}
sub startServer {
my $addr = shift;
my $s = new IO::Socket::INET(LocalAddr => $addr,
Listen => 1024, Reuse => 1, Proto => "tcp");
die("failed to listen at $addr: $!\n") unless defined $s;
fcntl($s, F_SETFL, O_NONBLOCK) || die("fcntl: $!");
$TheSelect->add([$s, undef()]);
&Log("listening at $addr", 1);
}
sub checkIo {
return unless $TheSelect->count();
&Log("waiting for activity...", 3);
my $timeout = @Client::ConnectingClients ? 1 : undef();
foreach my $h ($TheSelect->can_read($timeout)) {
my ($sock, $clt) = @{$h};
$clt ? $clt->noteReadReady($sock) : &accept($sock);
}
}
sub checkClients {
my @cltsToCheck = @Client::ConnectingClients;
@Client::ConnectingClients = ();
foreach my $clt (@cltsToCheck) {
$clt->action();
}
}
sub accept {
my $listSock = shift;
my $sock= $listSock->accept();
die("failed to accept a connection at $TheListenAddr: $!\n") unless $sock;
my $them = sprintf('%s:%d', $sock->peerhost(), $sock->peerport());
&Log("accepted a connection from $them", 2);
my $clt = new Client($them, $sock);
push @Clients, $clt;
}
sub Log {
my ($msg, $level) = @_;
$msg .= "\n" unless $msg =~ /\n$/;
$msg = "$0: $msg" unless $msg =~ /:\s/;
print(STDERR $msg) if !defined($level) || ($level <= $VerbLevel);
}
sub range2arr {
my $range = shift;
my @bins = ();
while ($range =~ s/([.:])?([^.:]+)//) {
my $sep = $1 || '';
my $spec = $2;
my ($min, $max) = $spec =~ /-/ ?
($spec =~ /^(\d+)-(\d+)$/) : ($spec =~ /^(\d+)$/);
return undef unless defined $min;
$max = $min if !defined($max);
push @bins, { min=>$min, max=>$max, pos=>$min, sep=>$sep };
}
my @res = ();
while (1) {
push @res, &curAddr(\@bins);
} continue {
last unless nextIter(\@bins);
}
return @res;
}
sub nextIter {
my ($bins, $level) = @_;
$level = $#{$bins} if !defined $level;
return undef if $level < 0;
my $b = $bins->[$level];
if ($b->{pos} >= $b->{max}) {
$b->{pos} = $b->{min};
return &nextIter($bins, $level-1);
}
$b->{pos}++;
return 1;
}
sub curAddr {
my $bins = shift;
my $addr = '';
for (my $i = 0; $i <= $#{$bins}; ++$i) {
my $b = $bins->[$i];
$addr .= $b->{sep};
$addr .= sprintf("%d", $b->{pos});
}
return $addr;
}
sub getOpts {
my @newOpts = ();
for (my $i = 0; $i <= $#ARGV; ++$i) {
my $opt = $ARGV[$i];
if ($opt !~ /^--/) {
push @newOpts, $opt;
next;
}
if ($opt =~ /^--recv_from?$/) {
my $addr = $ARGV[++$i];
die("$0: --recv_from requires an argument\n") if !defined($addr);
push (@TheDoormanAddrs, $addr);
next;
}
if ($opt eq '--listen_at') {
my $addr = $ARGV[++$i];
die("$0: --listen_at requires an argument\n") if !defined($addr);
die("$0: only one --listen option is allowed\n") if defined($TheListenAddr);
$TheListenAddr = $addr;
next;
}
if ($opt eq '--handler') {
$TheHandler = $ARGV[++$i];
die("$0: --handler requires an argument\n") if !defined($TheHandler);
next;
}
if ($opt eq '--verb_lvl') {
$VerbLevel = $ARGV[++$i];
die("$0: --verb_lvl requires an integer argument\n") if
!defined($VerbLevel) || ($VerbLevel !~ /^\-?\d+/);
next;
}
die("$0: unknown option: $opt\n");
}
push (@TheDoormanAddrs, @newOpts);
}
sub usage {
return "usage: $0 [--option] ... [recv_from_addr ...]\n".
" --recv_from where to connect to receieve beep messages\n".
" --listen_at where to listen for beep messages\n".
" --handler execute cmd for every message received\n".
" --verb_lvl verbosity level for stderr log\n".
"At least one `recv_from' or `listen_at' address must be given\n";
}