#!/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"; }