File Coverage

blib/lib/Net/SNPP/Server.pm
Criterion Covered Total %
statement 166 262 63.3
branch 69 132 52.2
condition 22 68 32.3
subroutine 19 27 70.3
pod 11 12 91.6
total 287 501 57.2


line stmt bran cond sub pod time code
1             package Net::SNPP::Server;
2 2     2   2134 use strict;
  2         4  
  2         78  
3 2     2   14 use warnings;
  2         4  
  2         72  
4 2     2   2364 use Socket;
  2         10304  
  2         1536  
5 2     2   24 use IO::Handle;
  2         4  
  2         98  
6 2     2   2130 use Net::Cmd;
  2         11248  
  2         202  
7 2     2   24 use Fcntl qw(:flock);
  2         4  
  2         386  
8 2     2   18 use Carp;
  2         6  
  2         138  
9 2     2   14 use vars qw( @ISA $counter );
  2         6  
  2         4194  
10             @ISA = qw( IO::Handle Net::Cmd );
11             $counter = 0;
12              
13             =head1 NAME
14              
15             Net::SNPP::Server
16              
17             =head1 DESCRIPTION
18              
19             An object interface for creating SNPP servers. Almost everything you
20             need to create your very own SNPP server is here in this module.
21             There is a callback() method that can replace default function with
22             your own.
23             them. Any SNPP command can be overridden or new/custom ones can be
24             created using custom_command(). To disable commands you just don't
25             want to deal with, use disable_command().
26              
27             =head1 SYNOPSIS
28              
29             There may be a synopsis here someday ...
30              
31             =head1 METHODS
32              
33             =over 4
34              
35             =item new()
36              
37             Create a Net::SNPP::Server object listening on a port. By default, it only
38             listens on the localhost (127.0.0.1) - specify MultiHomed to listen on all
39             addresses or LocalAddr to listen on only one.
40              
41             my $svr = Net::SNPP::Server->new(
42             Port => port to listen on
43             BindTo => interface address to bind to
44             MultiHomed => listen on all interfaces if true (and BindTo is unset)
45             Listen => how many simultaneous connections to handle (SOMAXCONN)
46             # the following two options are only used by handle_client()
47             MaxErrors => maximum number of errors before disconnecting client
48             Timeout => timeout while waiting for data (uses SIGARLM)
49             );
50              
51             =cut
52              
53             sub new {
54 2     2 1 17612 my( $class, %args ) = @_;
55 2         8 my $self = {};
56              
57             # set defaults for basic parameters
58 2 50       36 if ( !exists($args{Listen}) ) { $args{Listen} = SOMAXCONN }
  2         8  
59 2 50       8 if ( !exists($args{Port}) ) { $args{Port} = 444 }
  0         0  
60              
61             # choose either a unix domain socket or an inet socket
62 2 50       10 if ( !exists($args{UnixSocket}) ) { $args{Domain} = AF_INET }
  2         8  
63 0         0 else { $args{Domain} = PF_UNIX }
64              
65             # by default, bind only to the loopback interface
66             # i.e. MultiHomed and BindTo were not specified
67 2 50 33     112 if ( !exists($args{MultiHomed}) && !exists($args{BindTo}) ) {
    0          
68 2         8 $args{BindTo} = INADDR_LOOPBACK;
69             }
70             # if a bind address is passed in, bind to it
71             elsif ( exists($args{BindTo}) ) {
72 0         0 $args{BindTo} = inet_aton( $args{BindTo} );
73             }
74             # bind to all interfaces if MultiHomed is defined
75             # and BindTo is not
76             else {
77 0         0 $args{BindTo} = INADDR_ANY;
78             }
79              
80             # these two values are only used by the handle_client method
81 2         8 $self->{'MaxErrors'} = delete($args{MaxErrors});
82 2         6 $self->{'Timeout'} = delete($args{Timeout});
83              
84             # create the socket by hand instead of IO::Socket::INET to
85             # make manipulation a little easier within this module
86 2         24 $self->{sock} = IO::Handle->new();
87 2 50       3224 socket( $self->{sock}, $args{Domain}, SOCK_STREAM, getprotobyname('tcp') )
88             || croak "couldn't create socket: $!";
89 2         22 setsockopt( $self->{sock}, SOL_SOCKET, SO_REUSEADDR, 1 );
90              
91 2 50       322 if ( $args{Domain} == PF_UNIX ) {
92 0 0       0 if ( -e $args{UnixSocket} ) { unlink( $args{UnixSocket} ) }
  0         0  
93 0   0     0 $self->{sockaddr} = sockaddr_un( $args{UnixSocket} )
94             || croak "couldn't get socket address: $!";
95             }
96             else {
97 2   33     24 $self->{sockaddr} = sockaddr_in( $args{Port}, $args{BindTo} )
98             || croak "couldn't get socket address: $!";
99             }
100              
101 2 50       68 bind( $self->{sock}, $self->{sockaddr} )
102             || croak "could not bind socket: $!";
103              
104 2 50       42 listen( $self->{sock}, $args{Listen} )
105             || croak "could not listen on socket: $!";
106              
107             # set default callbacks
108             $self->{CB} = {
109             process_page => sub {
110 0     0   0 my( $pgr, $page, $results ) = @_;
111 0         0 push( @$results, [ $pgr, $page ] );
112             },
113             validate_pager_id => sub {
114 2 50 33 2   54 return undef if ( $_[0] =~ /\D/ || length($_[0]) < 7 );
115 2         11 return $_[0];
116             },
117 2 50   2   17 validate_pager_pin => sub { $_[1] || 1 },
118 0     0   0 write_log => sub { print STDERR "@_\n" },
119             create_id_and_pin => sub {
120 0     0   0 srand(); # re-seed the pseudrandom number generator
121 0         0 return( time().$counter, int(rand(1000000000)) );
122             }
123 2         44 };
124              
125             # initialize disabled and custom commands hashrefs
126 2         4 $self->{disabled} = {};
127 2         4 $self->{custom} = {};
128            
129 2         20 return bless( $self, $class );
130             }
131              
132             =item client()
133              
134             Calls accept() for you and returns a client handle. This method
135             will block if there is no waiting client. The handle returned
136             is a subclass of IO::Handle, so all IO::Handle methods should work.
137             my $client = $server->client();
138              
139             =cut
140              
141             sub client {
142 1     1 1 84 my $handle = IO::Handle->new();
143 1         2115 accept( $handle, $_[0]->{sock} );
144 1         45 return bless($handle, ref($_[0]));
145             }
146              
147             =item ip()
148              
149             Return the IP address associated with a client handle.
150             printf "connection from %s", $client->ip();
151              
152             =cut
153              
154             sub ip {
155 0     0 1 0 my $remote_client = getpeername($_[0]);
156 0 0       0 return 'xxx.xxx.xxx.xxx' if ( !defined($remote_client) );
157 0         0 my($port,$iaddr) = unpack_sockaddr_in($remote_client);
158 0         0 return inet_ntoa($iaddr);
159             }
160              
161             =item socket()
162              
163             Returns the raw socket handle. This mainly exists for use with select() or
164             IO::Select.
165             my $select = IO::Select->new();
166             $select->add( $server->socket() );
167              
168             =cut
169              
170 0     0 1 0 sub socket { $_[0]->{sock}; }
171              
172             =item connected()
173              
174             For use with a client handle. True if server socket is still alive.
175              
176             =cut
177              
178 1 50   1 1 23 sub connected { $_[0]->opened() && getpeername($_[0]) }
179              
180             =item shutdown()
181              
182             Shuts down the server socket.
183             $server->shutdown(2);
184              
185             =cut
186              
187 1   50 1 1 56 sub shutdown { shutdown($_[0],$_[1] || 2) }
188              
189             =item callback()
190              
191             Insert a callback into Server.pm.
192             $server->callback( 'process_page', \&my_function );
193             $server->callback( 'validate_pager_id', \&my_function );
194             $server->callback( 'validate_pager_pin', \&my_function );
195             $server->callback( 'write_log', \&my_function );
196             $server->callback( 'create_id_and_pin', \&my_function );
197              
198             =over 2
199              
200             =item process_page( $PAGER_ID, \%PAGE, \@RESULTS )
201              
202             $PAGER_ID = [
203             0 => retval of validate_pager_id
204             1 => retval of validate_pager_pin
205             ]
206             $PAGE = {
207             mess => $,
208             responses => [],
209             }
210              
211             =item validate_pager_id( PAGER_ID )
212              
213             The return value of this callback will be saved as the pager id
214             that is passed to the process_page callback as the first list
215             element of the first argument.
216              
217             =item validate_pager_pin( VALIDATED_PAGER_ID, PIN )
218              
219             The value returned by this callback will be saved as the second
220             list element in the first argument to process_page.
221             The PAGER_ID input to this callback is the output from the
222             validate_pager_id callback.
223              
224             NOTE: If you really care about the PIN, you must use this callback. The default callback will return 1 if the pin is not set.
225              
226             =item write_log
227              
228             First argument is a Unix syslog level, such as "warning" or "info."
229             The rest of the arguments are the message. Return value is ignored.
230              
231             =item create_id_and_pin
232              
233             Create an ID and PIN for a 2way message.
234              
235             =back
236              
237             =cut
238              
239             sub callback ($ $ $) {
240 3 50   3 1 37 croak "first argument callback() to must be one of: ", join(', ', keys(%{$_[0]->{CB}}))
  0         0  
241             if ( !exists($_[0]->{CB}{$_[1]}) );
242 3 50       42 croak "second argument callback() to must be a CODE ref"
243             if ( ref($_[2]) ne 'CODE' );
244 3         10 $_[0]->{CB}{$_[1]} = $_[2];
245             }
246              
247             =item custom_command()
248              
249             Create a custom command or override a default command in handle_client().
250             The command name must be 4 letters or numbers. The second argument is a coderef
251             that should return a text command, i.e. "250 OK" and some "defined" value to continue the
252             client loop. +++If no value is set, the client will be disconnected after
253             executing your command.+++ If you need MSTA or KTAG, this
254             is the hook you need to implement them.
255              
256             The subroutine will be passed the command arguments, split on whitespace.
257              
258             sub my_MSTA_sub {
259             my( $id, $password ) = @_;
260             # ...
261             return "250 OK", 1;
262             }
263             $server->custom_command( "MSTA", \&my_MSTA_sub );
264              
265             =cut
266              
267             sub custom_command ($ $ $) {
268 0 0   0 1 0 croak "first argument to custom_command must be exactly 4 characters"
269             if ( length($_[1]) != 4 );
270 0 0       0 croak "second argument to custom_command must be a coderef"
271             if ( ref($_[2]) ne 'CODE' );
272 0         0 $_[0]->{custom}{uc($_[1])} = $_[2];
273             }
274              
275             =item disable_command()
276              
277             Specify a command to disable in the server. This is useful, for instance,
278             if you don't want to support level 3 commands.
279             $server->disable_command( "2WAY", "550 2WAY not supported here" );
280              
281             The second argument is an optional custom error message. The default is:
282             "500 Command Not Implemented, Try Again"
283              
284             =cut
285              
286             sub disable_command {
287             # shorten & uppercase it so it matches in handle_client
288 0     0 1 0 my $cmd = unpack('A4',uc($_[1]));
289              
290 0 0       0 if ( defined($_[2]) ) {
291 0         0 $_[0]->{disabled}{$cmd} = $_[2];
292             }
293             else {
294 0         0 $_[0]->{disabled}{$cmd} = "500 Command Not Implemented, Try Again";
295             }
296             }
297              
298             =item handle_client()
299              
300             Takes the result of $server->client() and takes care of parsing
301             the user input. This should be quite close to being rfc1861
302             compliant. If you specified Timeout to be something other
303             than 0 in new(), SIGARLM will be used to set a timeout. If you
304             use this, make sure to take signals into account when writing your
305             code. fork()'ing before calling handle_client is a good way
306             to avoid interrupting code that shouldn't be interrupted.
307              
308             =cut
309              
310             sub handle_client ($ $) {
311 1     1 1 10 my( $self, $client ) = @_;
312 1         15 my $page = {}; # store the stuff the user gives us in this hash
313 1         8 my @pgrs = (); # store the list of pagers
314             # each pager is an array ref [ $pager_id, $pin ]
315 1         3 my @retvals = (); # build up a list of return values
316 1         9 my $errors = 0; # count the errors for maximum errors
317 1         10 my $timeout = 0;
318 1         30 local(%SIG);
319              
320             # enable timeouts if user requested passed Timeout to new()
321 1 50       17 if ( $self->{'Timeout'} ) {
322             $SIG{ALRM} = sub {
323 0     0   0 $self->{CB}{write_log}->( 'debug', "client timeout" );
324 0         0 $client->command( "421 Timeout, Goodbye" );
325 0         0 $client->shutdown(2);
326 0         0 $timeout = 1;
327 0         0 };
328 0         0 alarm( $self->{'Timeout'} );
329             }
330              
331             # let the client know we're ready for them
332 1         138 $client->command( "220 SNPP Gateway Ready" );
333              
334 1         1986 $self->{CB}{write_log}->( 'debug', "client connected" );
335              
336             # loop until timeout or client quits
337 1   33     255 while ( $timeout == 0 && (my $input = $client->getline()) ) {
338             # clean \n\r's out of input, then split it up by whitespace
339 15         3920 $input =~ s/[\r\n]+//gs;
340 15         64 my @cmd = split( /\s+/, $input );
341              
342             # uppercase and truncate the command shifted from @cmd to 4 characters
343 15         94 my $user_cmd = unpack('A4',uc(shift(@cmd)));
344 15 50       59 if ( length($user_cmd) != 4 ) {
345             # FIXME: put in correct full text from RFC document
346 0         0 $client->command( "550 Error, Invalid Command" );
347             }
348              
349 15         117 $self->{CB}{write_log}->( 'debug', "processing command '$user_cmd @cmd'" );
350            
351             # //////////////////////////////////////////////////////////////////// #
352             # BEGIN COMMANDS PARSING #
353             # \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ #
354              
355             ########################################################################
356             # user disabled commands --------------------------------------------- #
357 15 50       328 if ( exists($self->{disabled}{$user_cmd}) ) {
    50          
    100          
    100          
    100          
    100          
    100          
    50          
    100          
    100          
    100          
    100          
    100          
    50          
358 0         0 $errors++;
359 0         0 $client->command( $self->{disabled}{$user_cmd} );
360             }
361             ########################################################################
362             # user custom commands ----------------------------------------------- #
363             elsif ( exists($self->{custom}{$user_cmd}) ) {
364 0         0 my ($cmdtxt,$cont) = $self->{custom}{$user_cmd}->( @cmd );
365 0         0 $client->command( $cmdtxt );
366 0 0       0 last if ( !$cont );
367             }
368             ########################################################################
369             # 4.3 Level 1 Commands #################################################
370             ########################################################################
371             # 4.3.1 PAGEr --------------------------------------------- #
372             # 4.5.2 PAGEr [Password/PIN] ------------------------------- #
373             elsif ( $user_cmd eq 'PAGE' ) {
374 2         10 my $valid_pgr_id = $self->{CB}{validate_pager_id}->($cmd[0]);
375 2         17 my $valid_pin = $self->{CB}{validate_pager_pin}->($valid_pgr_id,$cmd[1]);
376 2 50 33     33 if ( $valid_pgr_id && $valid_pin ) {
377 2         5 push( @pgrs, [$valid_pgr_id,$valid_pin] );
378 2         9 $client->command( "250 Pager ID Accepted" );
379             }
380             else {
381 0         0 $errors++;
382 0         0 $client->command( "550 Error, Invalid Pager ID" );
383             }
384             }
385              
386             ########################################################################
387             # 4.3.2 MESSage --------------------------- #
388             # 4.5.8 SUBJect ------------------------------------- #
389             elsif ( $user_cmd =~ /(MESS|SUBJ)/ ) {
390 1         16 my $key = $1;
391 1 50 33     18 if ( $key && $key eq 'MESS' && defined($page->{mess}) ) {
      33        
392 0         0 $errors++;
393 0         0 $client->command( "503 ERROR, Message Already Entered" );
394 0         0 next;
395             }
396 1 50 33     25 if ( !defined($cmd[0]) || $cmd[0] eq '' ) {
397 0         0 $errors++;
398 0         0 $client->command( "550 ERROR, Invalid Message" );
399 0         0 next;
400             }
401 1         28 $page->{lc($key)} = join(' ', @cmd);
402 1         5 $client->command( "250 Message OK" );
403             }
404              
405             ########################################################################
406             # 4.3.3 RESEt -------------------------------------------------------- #
407             elsif ( $user_cmd eq 'RESE' ) {
408 1         3 $page = {};
409 1         3 @pgrs = ();
410 1         4 $client->command( "250 RESET OK" );
411             }
412              
413             ########################################################################
414             # 4.3.4 SEND --------------------------------------------------------- #
415             elsif ( $user_cmd eq 'SEND' ) {
416 1 50       7 if ( @pgrs == 0 ) {
417 0         0 $errors++;
418 0         0 $client->command( "503 Error, Pager ID needed" );
419 0         0 next;
420             }
421 1 50       5 if ( !exists($page->{mess}) ) {
422 0         0 $errors++;
423 0         0 $client->command( "503 Error, Pager ID or Message Incomplete" );
424 0         0 next;
425             }
426              
427 1         2 my $res = undef;
428 1         7 for ( my $i=0; $i<@pgrs; $i++ ) {
429 1 50       5 if ( !exists($page->{alert}) ) { $page->{alert} = 0 }
  0         0  
430 1 50       5 if ( !exists($page->{hold}) ) { $page->{hold} = 0 }
  0         0  
431              
432             # call the callback subroutine with the data
433             # the default callback just pushes the data onto @retvals
434 1         26 $res = $self->{CB}{process_page}->( $pgrs[$i], $page, \@retvals );
435             }
436 1 50 33     23 if ( $res && exists($page->{twoway}) ) {
    50          
437             # this callback generates the two numbers for identifying a page
438 0         0 my @tags = $self->{CB}{create_id_and_pin}->( \@pgrs, $page );
439 0         0 $client->command( "960 @tags OK, Message QUEUED for Delivery" );
440             }
441             elsif ( $res ) {
442 1         6 $client->command( "250 Message Sent Successfully" );
443             }
444             else {
445 0         0 $client->command( "554 Error, failed" );
446 0         0 next;
447             }
448             # RESEt
449 1         1001 @pgrs = ();
450 1         5 $page = {};
451             }
452              
453             ########################################################################
454             elsif ( $user_cmd eq 'QUIT' ) {
455 1         13 $client->command( "221 OK, Goodbye" );
456 1         89 last;
457             }
458              
459             ########################################################################
460             # 4.3.6 HELP (optional) ---------------------------------------------- #
461             elsif ( $user_cmd eq 'HELP' ) {
462             {
463 2     2   16 no warnings; # so we can use
  2         4  
  2         3142  
  0         0  
464 0         0 while () { $client->command( $_ ) }
  0         0  
465 0         0 $client->command( "250 End of Help Information" );
466             }
467             }
468              
469             ########################################################################
470             ## 4.4 Level 2 - Minimum Extensions ####################################
471             ########################################################################
472             # 4.4.1 DATA --------------------------------------------------------- #
473             elsif ( $user_cmd eq 'DATA' ) {
474 1         6 $client->command( "354 Begin Input; End with '.'" );
475 1         507 my $buffer = join( '', @{ $client->read_until_dot() } );
  1         38  
476 1 50 33     37452 if ( !defined($buffer) || !length($buffer) ) {
477 0         0 $errors++;
478 0         0 $client->command( "550 Error, Blank Message" );
479             }
480             else {
481 1         21 $buffer =~ s/[\r\n]+/\n/gs;
482 1         4 $page->{mess} = $buffer;
483 1         8 $client->command( "250 Message OK" );
484             }
485             }
486              
487             ########################################################################
488             ## 4.5 Level 2 - Optional Extensions ###################################
489             ########################################################################
490             # 4.5.4 ALERt ---------------------------------------- #
491             elsif ( $user_cmd eq 'ALER' ) {
492 1 50 33     37 if ( defined($cmd[0]) && ($cmd[0] == 1 || $cmd[0] == 0) ) {
      33        
493 1         3 $page->{alert} = $cmd[0];
494 1         5 $client->command( "250 OK, Alert Override Accepted" );
495             }
496             else {
497 0         0 $errors++;
498 0         0 $client->command( "550 Error, Invalid Alert Parameter" );
499             }
500             }
501              
502             ########################################################################
503             # 4.5.6 HOLDuntil [+/-GMTdifference] ------------------ #
504             # non-rfc to accept 4-digit years is also accepted ---- #
505             elsif ( $user_cmd eq 'HOLD' ) {
506 1 50 33     24 if ( defined($cmd[0]) && $cmd[0] !~ /[^0-9]/
      33        
      33        
507             && (length($cmd[0]) == 12 || length($cmd[0]) == 14) ) {
508 1         7 $page->{hold} = $cmd[0];
509 1 50       6 if ( $cmd[1] =~ /([+-]\d+)/ ) { $page->{hold_gmt_diff} = $1; }
  1         4  
510 1         6 $client->command( "250 Delayed Messaging Selected" );
511             }
512             else {
513 0         0 $errors++;
514 0         0 $client->command( "550 Error, Invalid Delivery Date/Time" );
515             }
516             }
517              
518             ########################################################################
519             ## 4.6 Level 3 - Two-Way Extensions ####################################
520             ########################################################################
521             # 4.6.1 2WAY --------------------------------------------------------- #
522             elsif ( $user_cmd eq '2WAY' ) {
523 1 50 33     20 if ( exists($page->{mess}) || @pgrs > 0 ) {
524 0         0 $errors++;
525 0         0 $client->command( "550 Error, Standard Transaction Already Underway, use RESEt" );
526 0         0 next;
527             }
528 1         4 $page->{twoway} = 1;
529 1         10 $client->command( "250 OK, Beginning 2-Way Transaction" );
530             }
531              
532             ########################################################################
533             # 4.6.2 PING --------------------------------------- #
534             # FIXME: what the heck should this do by default?
535             elsif ( $user_cmd eq 'PING' ) {
536 1         16 $client->command( "250 OK, Cannot access device status" );
537             }
538              
539             ########################################################################
540             # 4.6.7 MCREsponse <2-byte_Code> Response_Text (not implemented) ----- #
541             elsif ( $user_cmd eq 'MCRE' ) {
542 4 50 33     80 if ( !exists($page->{twoway}) ) {
    50 33        
      33        
543 0         0 $errors++;
544 0         0 $client->command( "550 MCResponses Not Enabled" );
545             }
546             elsif ( $cmd[0] !~ /[^0-9]/ && length($cmd[0]) < 3 &&
547             length($cmd[1]) >= 1 && length($cmd[1]) < 16 ) {
548 4 50       22 if ( exists($page->{responses}{$cmd[0]}) ) {
549 0         0 $client->command( "502 Error! Would Duplicate Previously Entered MCResponse" );
550 0         0 next;
551             }
552 4         8392 $page->{responses}{shift @cmd} = join(' ',@cmd);
553 4         27 $client->command( "250 Response Added to Transaction" );
554             }
555             else {
556 0         0 $errors++;
557 0         0 $client->command( "554 Error, failed" );
558             }
559             }
560             ########################################################################
561             # UNKNOWN/UNDEFINED COMMANDS ----------------------------------------- #
562             # -------------------------------------------------------------------- #
563             # 4.5.1 LOGIn [password] (not implemented) ----------------- #
564             # 4.5.3 LEVEl (not implemented) ----------------- #
565             # 4.5.5 COVErage (not implemented) ----------------- #
566             # 4.5.7 CALLerid (not implemented) ----------------- #
567             # 4.6.3 EXPTag (not implemented) ----------------- #
568             # 4.6.5 ACKRead <0|1> (not implemented) ----------------- #
569             # 4.6.6 RTYPe (not implemented) ----------------- #
570             # MSTA --------------------------------------------------------------- #
571             # KTAG (not implemented) ----------------- #
572             ########################################################################
573             else {
574 0         0 $errors++;
575 0         0 $client->command( "500 Command Not Implemented, Try Again" );
576             }
577             # //////////////////////////////////////////////////////////////////// #
578             # END COMMANDS PARSING #
579             # \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ #
580              
581             # check the number of errors
582 14 50 33     5479 if ( $self->{MaxErrors} && $errors >= $self->{MaxErrors} ) {
583 0         0 $client->command( "421 Too Many Errors, Goodbye (terminate connection)" );
584 0         0 last;
585             }
586             # reset the alarm on input
587 14 50       546 if ( $self->{Timeout} ) { alarm(0); alarm( $self->{Timeout} ); }
  0         0  
  0         0  
588             } # while()
589              
590             # turn off the alarm
591 1 50       14 if ( $self->{Timeout} ) { alarm(0); }
  0         0  
592              
593             # disconnect if we're still connected
594 1 50       24 if ( $client->connected() ) { $client->shutdown(2) }
  1         77  
595              
596 1         8 return @retvals;
597             }
598              
599             =item forked_server()
600              
601             Creates a server in a forked process. The return value is
602             an array (or arrayref depending on context) containing a read-only pipe and
603             the pid of the new process. Pages completed will be written to the pipe as
604             a semicolon delimited array.
605             my($pipe,$pid) = $server->forked_server();
606             my $line = $pipe->getline();
607             chomp( $line );
608             my( $pgr, $pgr, %pagedata ) = split( /;/, $line );
609              
610             =cut
611              
612             # when testing, pass in an integer argument to limit the number of clients
613             # the server will process before exiting
614             sub forked_server {
615 2     2 1 4 my( $self, $count_arg ) = @_;
616 2         4 my $count = -1;
617 2 50       6 if ( $count_arg ) { $count = $count_arg }
  2         6  
618 2         6 my @pids = (); # pids to merge before exit
619              
620             # create a pipe for communication from child back to this process
621 2         14 our( $rp, $wp ) = ( IO::Handle->new(), IO::Handle->new() );
622 2 50       130 pipe( $rp, $wp )
623             || die "could not create READ/WRITE pipes";
624 2         16 $wp->autoflush(1);
625              
626             # declare our callback subroutine for process_page
627             # has it's own ugly serialization that should probably be replaced
628             # with Storable or Dumper
629             sub write_to_pipe {
630 1     1 0 2 my( $pgr, $page, $results ) = @_;
631 1         3 my( @parts, @resps ) = ();
632 1 50       5 if ( my $href = delete($page->{responses}) ) {
633 0         0 while ( my($k,$v) = each(%$href) ) {
634 0         0 $v =~ s/;/\%semicolon%/g;
635 0         0 $k = "responses[$k]";
636 0         0 push( @resps, $k, $v );
637             }
638             }
639 1         7 while ( my($k,$v) = each(%$page) ) {
640 4 50       10 if ( !defined($v) ) { $v = '' }
  0         0  
641 4         22 push( @parts, $k, $v );
642             }
643 1 50       13 if ( !defined($pgr->[1]) ) { $pgr->[1] = '1' }
  0         0  
644 1         6 my $out = join( ';', @$pgr, @parts, @resps );
645 1         3 $out =~ s/[\r\n]+//gs; # make sure there aren't any unexpected newlines
646              
647             # send the page semicolon delimited down the pipe
648 1         15 flock( $wp, LOCK_EX );
649 1         20 $wp->print( "$out\n" );
650 1         42 flock( $wp, LOCK_UN );
651             }
652              
653             # fork a child process to act as a server
654 2         3560 my $pid = fork();
655 2 100       310 if ( $pid ) {
656 1         82 $wp->close();
657 1 50       119 return wantarray ? ($rp,$pid) : [$rp,$pid];
658             }
659             else {
660 1         77 $rp->close();
661             # replace the page callback with our own subroutine
662 1         426 $self->callback( 'process_page', \&write_to_pipe );
663 1   33     139 while ( !$count_arg || $count > 0 ) {
664              
665             # attempt reap child processes on every loop
666 1         17 for ( my $i=0; $i<@pids; $i++ ) {
667 0         0 my $pid = waitpid( $pids[$i], 0 );
668 0 0       0 if ( $pid < 1 ) { splice( @pids, $i, 1 ); }
  0         0  
669             }
670              
671             # get a client socket handle
672 1         14 my $client = $self->client();
673              
674 1         7 $count--;
675              
676             # fork again so we can handle simultaneous connections
677 1         9232 my $pid = fork();
678              
679             # parent process goes back to top of loop
680 1 50       56 if ( $pid ) {
681 0         0 push( @pids, $pid );
682 0         0 next;
683             }
684            
685 1         57 $self->handle_client( $client );
686 1         379 exit 0;
687             }
688 0         0 $wp->close();
689 0         0 exit 0;
690             }
691             }
692              
693             =back
694              
695             =head1 AUTHOR
696              
697             Al Tobey
698              
699             Some ideas from Sendpage::SNPPServer
700             Kees Cook http://outflux.net/
701              
702             =head1 TODO
703              
704             Add more hooks for callbacks
705              
706             Implement the following level 2 and level 3 commands
707              
708             4.5.1 LOGIn [password]
709             4.5.3 LEVEl
710             4.5.5 COVErage
711             4.5.7 CALLerid
712             4.6.3 EXPTag
713             4.6.5 ACKRead <0|1>
714             4.6.6 RTYPe
715              
716             =head1 SEE ALSO
717              
718             Net::Cmd Socket
719              
720             =cut
721              
722             1;
723              
724             # FIXME: update this from the RFC
725             __DATA__