File Coverage

blib/lib/SRS/EPP/Proxy/Listener.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1              
2             package SRS::EPP::Proxy::Listener;
3             {
4             $SRS::EPP::Proxy::Listener::VERSION = '0.22';
5             }
6              
7 2     2   2380098 use 5.010; # for (?| alternation feature
  2         19  
  2         105  
8              
9 2     2   1478 use Moose;
  2         1103961  
  2         30  
10              
11             with 'MooseX::Log::Log4perl::Easy';
12              
13 2     2   18788 use IO::Select;
  2         3862  
  2         207  
14 2     2   2931 use Net::SSLeay::OO;
  0            
  0            
15             use Socket;
16             use IO::Socket::INET;
17             use MooseX::Params::Validate;
18              
19             our ($HAVE_V6, @SOCKET_TYPES);
20              
21             BEGIN {
22             my $sock = eval {
23             use if $] < 5.014, "Socket6";
24             require IO::Socket::INET6;
25             IO::Socket::INET6->new(
26             Listen => 1,
27             LocalAddr => '::1',
28             LocalPort => int(rand(60000)+1024),
29             Proto => 'tcp',
30             );
31             };
32             if ( $sock or $!{EADDRINUSE} ) {
33             $HAVE_V6 = 1;
34             @SOCKET_TYPES = ("IO::Socket::INET6");
35             }
36             push @SOCKET_TYPES, "IO::Socket::INET";
37             }
38              
39             sub resolve {
40             my $hostname = shift;
41             my @addr;
42             $DB::single = 1;
43             if ($HAVE_V6) {
44             my @res = getaddrinfo($hostname, "", AF_UNSPEC);
45             while (
46             my (
47             $family, $socktype, $proto, $address,
48             $canonical
49             )
50             = splice @res, 0, 5
51             )
52             {
53             my ($addr) = getnameinfo($address, &NI_NUMERICHOST);
54             push @addr, $addr unless grep { $_ eq $addr }
55             @addr;
56             }
57             }
58             else {
59             my $packed_ip = gethostbyname($hostname)
60             or die "fail to resolve host '$hostname'; $!";
61             my $ip_address = inet_ntoa($packed_ip);
62             push @addr, $ip_address;
63             }
64             @addr;
65             }
66              
67             has 'listen' =>
68             is => "ro",
69             isa => "ArrayRef[Str]",
70             required => 1,
71             default => sub { [ ($HAVE_V6 ? "[::]" : "0.0.0.0") ] },
72             ;
73              
74             has 'sockets' =>
75             is => "ro",
76             isa => "ArrayRef[IO::Socket]",
77             default => sub { [] },
78             ;
79              
80             use constant EPP_DEFAULT_TCP_PORT => 700;
81             use constant EPP_DEFAULT_LOCAL_PORT => "epp(".EPP_DEFAULT_TCP_PORT.")";
82              
83             sub fmt_addr_port {
84             my $addr = shift;
85             my $port = shift;
86             if ( $addr =~ m{:} ) {
87             "[$addr]:$port";
88             }
89             else {
90             "$addr:$port";
91             }
92             }
93              
94             sub init {
95             my $self = shift;
96              
97             my @sockets;
98             for my $addr ( @{ $self->listen } ) {
99              
100             # parse out the hostname and port; I can't see another
101             # way to supply a default port number.
102             my ($hostname, $port) = $addr =~
103             m{^(?|\[([^]]+)\]|([^:]+))(?::(\d+))?$}
104             or die "bad listen address: $addr";
105             $port ||= EPP_DEFAULT_LOCAL_PORT;
106              
107             my @addr = resolve($hostname);
108             $self->log_debug("$hostname resolved to @addr");
109              
110             for my $addr (@addr) {
111             my $SOCKET_TYPE = "IO::Socket::INET";
112             if ( $addr =~ /:/ ) {
113             $SOCKET_TYPE .= "6";
114             }
115             my $socket = $SOCKET_TYPE->new(
116             Listen => 5,
117             LocalAddr => $addr,
118             LocalPort => $port,
119             Proto => "tcp",
120             ReuseAddr => 1,
121             );
122              
123             my $addr_port = fmt_addr_port($addr,$port);
124              
125             if ( !$socket ) {
126             $self->log_error(
127             "Failed to listen on $addr_port; $!",
128             );
129             }
130             else {
131             $self->log_info(
132             "Listening on $addr_port",
133             );
134             push @sockets, $socket;
135             }
136             }
137             }
138              
139             if ( !@sockets ) {
140             die "No listening sockets; aborting";
141             }
142              
143             @{ $self->sockets } = @sockets;
144             }
145              
146             sub accept {
147             my $self = shift;
148            
149             my ( $timeout ) = pos_validated_list(
150             \@_,
151             { isa => 'Int', optional => 1 },
152             );
153            
154             my $select = IO::Select->new();
155             $select->add($_) for @{$self->sockets};
156             my @ready = $select->can_read($timeout)
157             or return;
158             while ( @ready > 1 ) {
159             if ( rand(1) > 0.5 ) {
160             shift @ready;
161             }
162             else {
163             pop @ready;
164             }
165             }
166             my $socket = $ready[0]->accept;
167             if ( !$socket ) {
168             die "accept lost a socket; exiting";
169             }
170             $socket;
171             }
172              
173             sub close {
174             my $self = shift;
175            
176             for my $socket ( @{ $self->sockets } ) {
177             $socket->close if $socket;
178             }
179             @{ $self->sockets } = ();
180             }
181              
182             1;
183              
184             __END__
185              
186             =head1 NAME
187              
188             SRS::EPP::Proxy::Listener - socket factory class
189              
190             =head1 SYNOPSIS
191              
192             my $listener = SRS::EPP::Proxy::Listener->new(
193             listen => [ "hostname:port", "address:port" ],
194             );
195              
196             # this does the listen part
197             $listener->init;
198              
199             # this normally blocks, and returns a socket.
200             # it might return undef, if you pass it a timeout.
201             my $socket = $listener->accept;
202              
203             =head1 DESCRIPTION
204              
205             This class is a TCP/IP listener. It listens on the configured ports
206             for TCP connections and returns sockets when there are incoming
207             connections waiting.
208              
209             You don't actually need to supply the port or listen addresses; the
210             defaults are to listen on INADDR_ANY (0.0.0.0) or IN6ADDR_ANY (::) on
211             port C<epp(700)>.
212              
213             If the L<IO::Socket::INET6> module is installed, then at load time the
214             module tries to listen on a random port on the IPv6 loopback address.
215             If that works (or fails with a particular plausible error, if
216             something else happened to be using that port), then IPv6 is
217             considered to be available. This means that the RFC3493-style
218             I<getaddrinfo> and such are used instead of C<gethostbyname>. You
219             will end up with a socket for every distinct address returned by
220             C<getaddrinfo> on the passed-in list.
221              
222             IPv6 addresses (not names) must be passed in square brackets, such as
223             C<[2404:130:0::42]>.
224              
225             In general these rules should make this listener behave like any
226             normal IPv6-aware daemon.
227              
228             =head1 SEE ALSO
229              
230             L<IO::Socket::INET>, L<Socket6>, L<IO::Socket::INET6>
231              
232             =head1 AUTHOR AND LICENCE
233              
234             Development commissioned by NZ Registry Services, and carried out by
235             Catalyst IT - L<http://www.catalyst.net.nz/>
236              
237             Copyright 2009, 2010, NZ Registry Services. This module is licensed
238             under the Artistic License v2.0, which permits relicensing under other
239             Free Software licenses.
240              
241             =cut
242