File Coverage

blib/lib/XAS/Lib/Net/Server.pm
Criterion Covered Total %
statement 24 178 13.4
branch 0 14 0.0
condition 0 15 0.0
subroutine 8 33 24.2
pod 13 14 92.8
total 45 254 17.7


line stmt bran cond sub pod time code
1             package XAS::Lib::Net::Server;
2              
3             our $VERSION = '0.05';
4              
5 1     1   3 use POE;
  1         1  
  1         8  
6 1     1   258 use Try::Tiny;
  1         2  
  1         43  
7 1     1   4 use Socket ':all';
  1         1  
  1         769  
8 1     1   446 use POE::Filter::Line;
  1         1860  
  1         22  
9 1     1   496 use POE::Wheel::ReadWrite;
  1         4321  
  1         21  
10 1     1   522 use POE::Wheel::SocketFactory;
  1         4219  
  1         70  
11              
12             use XAS::Class
13 1         17 debug => 0,
14             version => $VERSION,
15             base => 'XAS::Lib::POE::Service',
16             mixin => 'XAS::Lib::Mixins::Keepalive XAS::Lib::Mixins::Handlers',
17             utils => ':validation weaken params',
18             accessors => 'session clients',
19             constants => 'ARRAY HASHREF',
20             vars => {
21             PARAMS => {
22             -port => 1,
23             -tcp_keepalive => { optional => 1, default => 0 },
24             -inactivity_timer => { optional => 1, default => 600 },
25             -filter => { optional => 1, default => undef },
26             -address => { optional => 1, default => 'localhost' },
27             -eol => { optional => 1, default => "\015\012" },
28             }
29             }
30 1     1   5 ;
  1         1  
31              
32             #use Data::Dumper;
33              
34             # ----------------------------------------------------------------------
35             # Public Methods
36             # ----------------------------------------------------------------------
37              
38             sub session_initialize {
39 0     0 1   my $self = shift;
40              
41 0           my $alias = $self->alias;
42              
43 0           $self->log->debug("$alias: entering session_intialize()");
44              
45             # private events
46              
47 0           $poe_kernel->state('client_error', $self, '_client_error');
48 0           $poe_kernel->state('client_input', $self, '_client_input');
49 0           $poe_kernel->state('client_reaper', $self, '_client_reaper');
50 0           $poe_kernel->state('client_output', $self, '_client_output');
51 0           $poe_kernel->state('client_flushed', $self, '_client_flushed');
52 0           $poe_kernel->state('client_connected', $self, '_client_connected');
53 0           $poe_kernel->state('client_connection', $self, '_client_connection');
54 0           $poe_kernel->state('client_connection_failed', $self, '_client_connection_failed');
55              
56 0           $poe_kernel->state('handle_connection', $self, '_handle_connection');
57              
58             # walk the chain
59              
60 0           $self->SUPER::session_initialize();
61              
62 0           $self->log->debug("$alias: leaving session_intialize()");
63              
64             }
65              
66             sub session_startup {
67 0     0 1   my $self = shift;
68              
69 0           my $alias = $self->alias;
70              
71 0           $self->log->debug("$alias: entering session_startup()");
72              
73 0           $poe_kernel->call($alias, 'client_connection');
74              
75             # walk the chain
76              
77 0           $self->SUPER::session_startup();
78              
79 0           $self->log->debug("$alias: leaving session_startup()");
80              
81             }
82              
83             sub session_shutdown {
84 0     0 1   my $self = shift;
85              
86 0           my $alias = $self->alias;
87 0           my @clients = keys %{$self->{'clients'}};
  0            
88              
89 0           $self->log->debug("$alias: entering session_shutdown()");
90              
91 0           foreach my $client (@clients) {
92              
93 0           $poe_kernel->alarm_remove($self->clients->{$client}->{'watchdog'});
94 0           $client = undef;
95              
96             }
97              
98 0           delete $self->{'listener'};
99              
100             # walk the chain
101              
102 0           $self->SUPER::session_shutdown();
103              
104 0           $self->log->debug("$alias: leaving session_shutdown()");
105              
106             }
107              
108             sub session_pause {
109 0     0 1   my $self = shift;
110              
111 0           my $alias = $self->alias;
112 0           my @clients = keys %{$self->{'clients'}};
  0            
113              
114 0           $self->log->debug("$alias: entering session_pause()");
115              
116 0           foreach my $client (@clients) {
117              
118 0           $client->pause_input();
119 0           $poe_kernel->alarm_remove($self->clients->{$client}->{'watchdog'});
120              
121             }
122              
123             # walk the chain
124              
125 0           $self->SUPER::session_pause();
126              
127 0           $self->log->debug("$alias: leaving session_pause()");
128              
129             }
130              
131             sub session_resume {
132 0     0 1   my $self = shift;
133              
134 0           my $alias = $self->alias;
135 0           my @clients = keys %{$self->{'clients'}};
  0            
136 0           my $inactivity = $self->inactivity_timer;
137              
138 0           $self->log->debug("$alias: entering session_resume()");
139              
140 0           foreach my $client (@clients) {
141              
142 0           $client->resume_input();
143 0           $self->clients->{$client}->{'watchdog'} = $poe_kernel->alarm_set('client_reaper', $inactivity, $client);
144              
145             }
146              
147             # walk the chain
148              
149 0           $self->SUPER::session_resume();
150              
151 0           $self->log->debug("$alias: leaving session_resume()");
152              
153             }
154              
155             sub reaper {
156 0     0 1   my $self = shift;
157 0           my ($wheel) = validate_params(\@_, [1]);
158              
159 0           my $alias = $self->alias;
160              
161 0           $self->log->debug_msg('net_client_reaper', $alias, $self->host($wheel), $self->peerport($wheel));
162              
163             }
164              
165             sub process_request {
166 0     0 1   my $self = shift;
167 0           my ($input, $ctx) = validate_params(\@_, [
168             1,
169             type => HASHREF,
170             ]);
171              
172 0           $self->process_response($input, $ctx);
173              
174             }
175              
176             sub process_response {
177 0     0 1   my $self = shift;
178 0           my ($output, $ctx) = validate_params(\@_, [
179             1,
180             type => HASHREF,
181             ]);
182              
183 0           my $alias = $self->alias;
184              
185 0           $poe_kernel->post($alias, 'client_output', $output, $ctx);
186              
187             }
188              
189             sub process_errors {
190 0     0 1   my $self = shift;
191 0           my ($errors, $ctx) = validate_params(\@_, [
192             1,
193             type => HASHREF,
194             ]);
195              
196 0           $self->process_response($errors, $ctx);
197              
198             }
199              
200             sub handle_connection {
201 0     0 1   my $self = shift;
202 0           my ($wheel) = validate_params(\@_, [1]);
203              
204             }
205              
206             # ----------------------------------------------------------------------
207             # Public Accessors
208             # ----------------------------------------------------------------------
209              
210             sub peerport {
211 0     0 1   my $self = shift;
212 0           my ($wheel) = validate_params(\@_, [1]);
213              
214 0           return $self->clients->{$wheel}->{'port'};
215              
216             }
217              
218             sub peerhost {
219 0     0 0   my $self = shift;
220 0           my ($wheel) = validate_params(\@_, [1]);
221              
222 0           return $self->clients->{$wheel}->{'host'};
223              
224             }
225              
226             sub client {
227 0     0 1   my $self = shift;
228 0           my ($wheel) = validate_params(\@_, [1]);
229              
230 0           return $self->clients->{$wheel}->{'client'};
231              
232             }
233              
234             # ----------------------------------------------------------------------
235             # Public Events
236             # ----------------------------------------------------------------------
237              
238             # ----------------------------------------------------------------------
239             # Private Events
240             # ----------------------------------------------------------------------
241              
242             sub _handle_connection {
243 0     0     my ($self, $wheel) = @_[OBJECT, ARG0];
244              
245 0           my $alias = $self->alias;
246              
247 0           $self->log->debug("$alias: _handle_connection()");
248 0           $self->handle_connection($wheel);
249              
250             }
251              
252             sub _client_connection {
253 0     0     my ($self) = $_[OBJECT];
254              
255 0           my $alias = $self->alias;
256              
257 0           $self->log->debug("$alias: _client_connection()");
258              
259             # start listening for connections
260              
261 0           $self->{'listener'} = POE::Wheel::SocketFactory->new(
262             BindAddress => $self->address,
263             BindPort => $self->port,
264             SocketType => SOCK_STREAM,
265             SocketDomain => AF_INET,
266             SocketProtocol => 'tcp',
267             Reuse => 1,
268             SuccessEvent => 'client_connected',
269             FailureEvent => 'client_connection_failed'
270             );
271              
272             }
273              
274             sub _client_connected {
275 0     0     my ($self, $socket, $peeraddr, $peerport, $wheel_id) = @_[OBJECT,ARG0..ARG3];
276              
277 0           my $alias = $self->alias;
278 0           my $inactivity = $self->inactivity_timer;
279              
280 0           $self->log->debug("$alias: _client_connected()");
281              
282 0 0         if ($self->tcp_keepalive) {
283              
284 0           $self->log->debug("$alias: keepalive activated");
285              
286 0           $self->enable_keepalive($socket);
287              
288             }
289              
290 0           my $client = POE::Wheel::ReadWrite->new(
291             Handle => $socket,
292             Filter => $self->filter,
293             InputEvent => 'client_input',
294             ErrorEvent => 'client_error',
295             FlushedEvent => 'client_flushed',
296             );
297              
298 0           my $wheel = $client->ID;
299 0           my $host = gethostbyaddr($peeraddr, AF_INET);
300              
301 0           $self->{'clients'}->{$wheel}->{'host'} = $host;
302 0           $self->{'clients'}->{$wheel}->{'port'} = $peerport;
303 0           $self->{'clients'}->{$wheel}->{'client'} = $client;
304 0           $self->{'clients'}->{$wheel}->{'active'} = time();
305 0           $self->{'clients'}->{$wheel}->{'socket'} = $socket;
306 0           $self->{'clients'}->{$wheel}->{'watchdog'} = $poe_kernel->alarm_set('client_reaper', $inactivity, $wheel);
307              
308 0           $self->log->info_msg('net_client_connect', $alias, $host, $peerport);
309              
310 0           $poe_kernel->post($alias, 'handle_connection', $wheel);
311            
312             }
313              
314             sub _client_connection_failed {
315 0     0     my ($self, $syscall, $errnum, $errstr, $wheel) = @_[OBJECT,ARG0..ARG3];
316              
317 0           my $alias = $self->alias;
318              
319 0           $self->log->error_msg('net_client_connection_failed', $alias, $errnum, $errstr);
320              
321 0           delete $self->{'listener'};
322              
323             }
324              
325             sub _client_input {
326 0     0     my ($self, $input, $wheel) = @_[OBJECT,ARG0,ARG1];
327              
328 0           my $alias = $self->alias;
329 0           my $ctx = {
330             wheel => $wheel
331             };
332              
333 0           $self->log->debug("$alias: _client_input()");
334              
335 0           $self->{'clients'}->{$wheel}->{'active'} = time();
336              
337 0           $self->process_request($input, $ctx);
338              
339             }
340              
341             sub _client_output {
342 0     0     my ($self, $data, $ctx) = @_[OBJECT,ARG0,ARG1];
343              
344 0           my $alias = $self->alias;
345 0           my $wheel = $ctx->{'wheel'};
346 0           my @buffer;
347              
348 0           $self->log->debug("$alias: _client_output()");
349              
350             try {
351              
352 0 0 0 0     if (defined($wheel) and defined($self->clients->{$wheel})) {
353              
354             # emulate IO::Socket connected() method. this method
355             # calls getpeername(). getpeername() returns undef when
356             # the network stack can't validate the socket.
357              
358 1     1   2420 no warnings;
  1         2  
  1         420  
359              
360 0 0         if (getpeername($self->clients->{$wheel}->{'socket'})) {
361              
362 0           push(@buffer, $data);
363 0           $self->clients->{$wheel}->{'client'}->put(@buffer);
364              
365             } else {
366              
367 0   0       $self->log->error_msg(
      0        
368             'net_client_nosocket',
369             $alias,
370             $self->peerhist($wheel) || 'unknown',
371             $self->peerport($wheel) || 'unknown'
372             );
373 0           delete $self->clients->{$wheel};
374              
375             }
376              
377             } else {
378              
379 0           $self->log->error_msg('net_client_nowheel', $alias);
380              
381             }
382              
383             } catch {
384              
385 0     0     my $ex = $_;
386              
387 0           $self->exception_handler($ex, $alias);
388              
389 0           delete $self->clients->{$wheel};
390              
391 0           };
392              
393             }
394              
395             sub _client_error {
396 0     0     my ($self, $syscall, $errnum, $errstr, $wheel) = @_[OBJECT,ARG0..ARG3];
397              
398 0           my $alias = $self->alias;
399 0   0       my $port = $self->peerport($wheel) || 'unknown';
400 0   0       my $host = $self->peerhost($wheel) || 'unknown';
401              
402 0           $self->log->debug("$alias: _client_error()");
403              
404 0 0         if ($errnum == 0) {
405              
406 0           $self->log->info_msg('net_client_disconnect', $alias, $host, $port);
407              
408             } else {
409              
410 0           $self->log->error_msg('net_client_error', $alias, $errnum, $errstr);
411              
412             }
413              
414 0           delete $self->clients->{$wheel};
415              
416             }
417              
418             sub _client_reaper {
419 0     0     my ($self, $wheel) = @_[OBJECT,ARG0];
420              
421 0           my $timeout = time() - $self->inactivity_timer;
422              
423 0 0         if (defined($self->clients->{$wheel})) {
424              
425 0 0         if ($self->clients->{$wheel}->{'active'} < $timeout) {
426              
427 0           $self->reaper($wheel);
428              
429             }
430              
431             }
432              
433             }
434              
435             sub _client_flushed {
436 0     0     my ($self, $wheel) = @_[OBJECT,ARG0];
437              
438 0           my $alias = $self->alias;
439 0   0       my $host = $self->peerhost($wheel) || 'unknown';
440 0   0       my $port = $self->peerport($wheel) || 'unknown';
441              
442 0           $self->log->debug(sprintf('%s: _client_flushed(), wheel: %s, host: %s, port: %s', $alias, $wheel, $host, $port));
443            
444             }
445              
446             # ----------------------------------------------------------------------
447             # Private Methods
448             # ----------------------------------------------------------------------
449              
450             sub init {
451 0     0 1   my $class = shift;
452              
453 0           my $self = $class->SUPER::init(@_);
454              
455 0           $self->init_keepalive(); # init tcp keepalive definations
456              
457 0 0         unless (defined($self->filter)) {
458              
459 0           $self->{'filter'} = POE::Filter::Line->new(
460             InputLiteral => $self->eol,
461             OutputLiteral => $self->eol,
462             );
463              
464             }
465              
466 0           return $self;
467              
468             }
469              
470             1;
471              
472             __END__
473              
474             =head1 NAME
475              
476             XAS::Lib::Net::Server - A basic network server for the XAS Environment
477              
478             =head1 SYNOPSIS
479              
480             my $server = XAS::Lib::Net::Server->new(
481             -port => 9505,
482             -host => 'localhost',
483             -filter => POE::Filter::Line->new(),
484             -alias => 'server',
485             -inactivity_timer => 600,
486             -eol => "\015\012"
487             }
488              
489             =head1 DESCRIPTION
490              
491             This module implements a simple text orientated network protocol. Data is
492             sent out as delimited strings. Which means every string has a consistent EOL.
493             These strings may be formatted, such as JSON. This module inherits from
494             L<XAS::Lib::POE::Session|XAS::Lib::POE::Session>.
495              
496             =head1 METHODS
497              
498             =head2 new
499              
500             This initializes the module and starts listening for requests. There are
501             five parameters that can be passed. They are the following:
502              
503             =over 4
504              
505             =item B<-alias>
506              
507             The name of the POE session.
508              
509             =item B<-port>
510              
511             The IP port to listen on.
512              
513             =item B<-address>
514              
515             The address to bind too.
516              
517             =item B<-inactivity_timer>
518              
519             Sets an inactivity timer on clients. When it is surpassed, the method reaper()
520             is called with the POE wheel id. What reaper() does is application specific.
521             The default is 600 seconds.
522              
523             =item B<-filter>
524              
525             An optional filter to use, defaults to POE::Filter::Line
526              
527             =item B<-eol>
528              
529             An optional EOL, defaults to "\015\012";
530              
531             =item B<-tcp_keeplive>
532              
533             Turns on TCP keepalive for each connection.
534              
535             =back
536              
537             =head2 reaper($wheel)
538              
539             Called when the inactivity timer is triggered.
540              
541             =over 4
542              
543             =item B<$wheel>
544              
545             The POE wheel that triggered the timer.
546              
547             =back
548              
549             =head2 process_request($input, $ctx)
550              
551             This method will process the input from the client. It takes the
552             following parameters:
553              
554             =over 4
555              
556             =item B<$input>
557              
558             The input received from the socket.
559              
560             =item B<$ctx>
561              
562             A hash variable to maintain context. This will be initialized with a "wheel"
563             field. Others fields may be added as needed.
564              
565             =back
566              
567             =head2 process_response($output, $ctx)
568              
569             This method will process the output for the client. It takes the
570             following parameters:
571              
572             =over 4
573              
574             =item B<$output>
575              
576             The output to be sent to the socket.
577              
578             =item B<$ctx>
579              
580             A hash variable to maintain context. This uses the "wheel" field to direct
581             output to the correct socket. Others fields may have been added as needed.
582              
583             =back
584              
585             =head2 process_errors($errors, $ctx)
586              
587             This method will process the error output from the client. It takes the
588             following parameters:
589              
590             =over 4
591              
592             =item B<$errors>
593              
594             The output to be sent to the socket.
595              
596             =item B<$ctx>
597              
598             A hash variable to maintain context. This uses the "wheel" field to direct
599             output to the correct socket. Others fields may have been added as needed.
600              
601             =back
602              
603             =head2 handle_connection($wheel)
604              
605             This method is called after the client connects. This is for additional
606             post connection processing as needed. It takes the following parameters:
607              
608             =over 4
609              
610             =item B<$wheel>
611              
612             The id of the clients wheel.
613              
614             =back
615              
616             =head1 ACCESSORS
617              
618             =head2 peerport($wheel)
619              
620             This returns the current port for that wheel.
621              
622             =over 4
623              
624             =item B<$wheel>
625              
626             The POE wheel to use.
627              
628             =back
629              
630             =head2 host($wheel)
631              
632             This returns the current host name for that wheel.
633              
634             =over 4
635              
636             =item B<$wheel>
637              
638             The POE wheel to use.
639              
640             =back
641              
642             =head2 client($wheel)
643              
644             This returns the current client for that wheel.
645              
646             =over 4
647              
648             =item B<$wheel>
649              
650             The POE wheel to use.
651              
652             =back
653              
654             =head1 SEE ALSO
655              
656             =over 4
657              
658             =item L<XAS::Lib::Net::Client|XAS::Lib::Net::Client>
659              
660             =item L<XAS::Lib::Net::POE::Client|XAS::Lib::Net::POE::Client>
661              
662             =item L<POE::Filter::Line|https://metacpan.org/pod/POE::Filter::Line>
663              
664             =item L<XAS|XAS>
665              
666             =back
667              
668             =head1 AUTHOR
669              
670             Kevin L. Esteb, E<lt>kevin@kesteb.usE<gt>
671              
672             =head1 COPYRIGHT AND LICENSE
673              
674             Copyright (c) 2012-2015 Kevin L. Esteb
675              
676             This is free software; you can redistribute it and/or modify it under
677             the terms of the Artistic License 2.0. For details, see the full text
678             of the license at http://www.perlfoundation.org/artistic_license_2_0.
679              
680             =cut