File Coverage

blib/lib/Net/Server/Proto.pm
Criterion Covered Total %
statement 82 150 54.6
branch 57 140 40.7
condition 25 72 34.7
subroutine 7 10 70.0
pod 1 5 20.0
total 172 377 45.6


line stmt bran cond sub pod time code
1             # -*- perl -*-
2             #
3             # Net::Server::Proto - Net::Server Protocol compatibility layer
4             #
5             # Copyright (C) 2001-2022
6             #
7             # Paul Seamons
8             #
9             # This package may be distributed under the terms of either the
10             # GNU General Public License
11             # or the
12             # Perl Artistic License
13             #
14             # All rights reserved.
15             #
16             ################################################################
17              
18             package Net::Server::Proto;
19              
20 43     43   21256 use strict;
  43         76  
  43         1013  
21 43     43   174 use warnings;
  43         61  
  43         797  
22 43     43   165 use Socket ();
  43         67  
  43         115136  
23              
24             my $requires_ipv6 = 0;
25             my $ipv6_package;
26              
27             sub parse_info {
28 100     100 0 312 my ($class, $port, $host, $proto, $ipv, $server) = @_;
29              
30 100         132 my $info;
31 100 100       333 if (ref($port) eq 'HASH') {
32 9 50       20 die "Missing port in hashref passed in port argument.\n" if ! $port->{'port'};
33 9         10 $info = $port;
34             } else {
35 91         135 $info = {};
36 91 100       278 $info->{'unix_type'} = $1
37             if $port =~ s{ (?<=[\w*\]]) [,|\s:/]+ (sock_stream|sock_dgram) \b }{}x; # legacy /some/path|sock_dgram
38 91 100       367 $ipv = $1 if $port =~ s{ (?<=[\w*\]]) [,|\s:/]+ IPv([*\d]+) }{}xi; # allow for 80|IPv*
39 91 50       211 $ipv .= $1 if $port =~ s{ (?<=[\w*\]]) [,|\s:/]+ IPv([*\d]+) }{}xi; # allow for 80|IPv4|IPv6 stacked
40 91 100 66     581 $proto = $1 if $port =~ s{ (?<=[\w*\]]) [,|\s:/]+ (tcp|udp|ssl|ssleay|unix|unixdgram|\w+(?: ::\w+)+) $ }{}xi # allow for 80/tcp or 200/udb or 90/Net::Server::Proto::TCP
41             || $port =~ s{ / (\w+) $ }{}x; # legacy 80/MyTcp support
42 91 100       476 $host = $1 if $port =~ s{ ^ (.*?) [,|\s:]+ (?= \w+ $) }{}x; # allow localhost:80
43 91         220 $info->{'port'} = $port;
44             }
45 100   50     195 $info->{'port'} ||= 0;
46              
47              
48 100 100 66     694 $info->{'host'} ||= (defined($host) && length($host)) ? $host : '*';
      66        
49 100 50       237 $ipv = $1 if $info->{'host'} =~ s{ (?<=[\w*\]]) [,|\s:/]+ IPv([*\d]+) }{}xi; # allow for 80|IPv*
50 100 50       229 $ipv .= $1 if $info->{'host'} =~ s{ (?<=[\w*\]]) [,|\s:/]+ IPv([*\d]+) }{}xi; # allow for 80|IPv4|IPv6 stacked
51 100 100       566 if ( $info->{'host'} =~ m{^ \[ ([\w/.\-:]+ | \*?) \] $ }x) { # allow for [::1] or [host.example.com]
    50          
52 1 50       5 $info->{'host'} = length($1) ? $1 : '*';
53             } elsif ($info->{'host'} =~ m{^ ([\w/.\-:]+ | \*?) $ }x) {
54 99         241 $info->{'host'} = $1; # untaint
55             } else {
56 0         0 $server->fatal("Could not determine host from \"$info->{'host'}\"");
57             }
58              
59              
60 100   100     618 $info->{'proto'} ||= $proto || 'tcp';
      66        
61 100 50       180 $ipv = $1 if $info->{'proto'} =~ s{ (?<=[\w*\]]) [,|\s:/]+ IPv([*\d]+) }{}xi; # allow for 80|IPv*
62 100 50       173 $ipv .= $1 if $info->{'proto'} =~ s{ (?<=[\w*\]]) [,|\s:/]+ IPv([*\d]+) }{}xi; # allow for 80|IPv4|IPv6 stacked
63 100 50       429 if ($info->{'proto'} =~ /^(\w+ (?:::\w+)*)$/x) {
64 100         218 $info->{'proto'} = $1;
65             } else {
66 0         0 $server->fatal("Could not determine proto from \"$proto\"");
67             }
68 100         208 $proto = lc $info->{'proto'};
69              
70 100 100       205 if ($info->{'proto'} =~ /^UNIX/i) {
71 8         55 return ({%$info, ipv => '*'});
72             }
73 92   100     470 $ipv = $info->{'ipv'} || $ipv || $ENV{'IPV'} || '';
74 92 50       189 $ipv = join '', @$ipv if ref($ipv) eq 'ARRAY';
75 92 50 66     314 $server->fatal("Invalid ipv parameter - must contain 4, 6, or *") if $ipv && $ipv !~ /[46*]/;
76 92         134 my @_info;
77 92 100 100     572 if (!$ipv || $ipv =~ /[*]/) {
    50 33        
78 41         49 my @rows = eval { $class->get_addr_info(@$info{qw(host port proto)}) };
  41         106  
79 41 50 0     82 $server->fatal($@ || "Could not find valid addresses for [$info->{'host'}]:$info->{'port'} with ipv set to '*'") if ! @rows;
80 41         64 foreach my $row (@rows) {
81 41         80 my ($host, $port, $ipv, $warn) = @$row;
82 41 50       176 push @_info, {host => $host, port => $port, ipv => $ipv, proto => $info->{'proto'}, $warn ? (warn => $warn) : ()};
83 41 50 33     122 $requires_ipv6++ if $ipv ne '4' && $proto ne 'ssl'; # we need to know if Proto::TCP needs to reparent as a child of an IPv6 compatible socket library
84             }
85 41 50 33     86 if (@rows > 1 && $rows[0]->[1] == 0) {
86 0         0 $server->log(2, "Determining auto-assigned port (0) for host $info->{'host'} (prebind)");
87 0         0 my $sock = $class->object($_info[-1], $server);
88 0         0 $sock->connect($server);
89 0         0 @$_{qw(port orig_port)} = ($sock->NS_port, 0) for @_info;
90             }
91 41         61 foreach my $_info (@_info) {
92             $server->log(2, "Resolved [$info->{'host'}]:$info->{'port'} to [$_info->{'host'}]:$_info->{'port'}, IPv$_info->{'ipv'}")
93 41 100 66     368 if $_info->{'host'} ne $info->{'host'} || $_info->{'port'} ne $info->{'port'};
94 41 50       205 $server->log(2, delete $_info->{'warn'}) if $_info->{'warn'};
95             }
96             } elsif ($ipv =~ /6/ || $info->{'host'} =~ /:/) {
97 0         0 push @_info, {%$info, ipv => '6'};
98 0 0       0 $requires_ipv6++ if $proto ne 'ssl'; # IO::Socket::SSL does its own determination
99 0 0 0     0 push @_info, {%$info, ipv => '4'} if $ipv =~ /4/ && $info->{'host'} !~ /:/;
100             } else {
101 51         273 push @_info, {%$info, ipv => '4'};
102             }
103              
104 92         398 return @_info;
105             }
106              
107             sub get_addr_info {
108 81     81 0 641 my ($class, $host, $port, $proto) = @_;
109 81 50       273 $host = '*' if ! defined $host;
110 81 100       208 $port = 0 if ! defined $port;
111 81 100       202 $proto = 'tcp' if ! defined $proto;
112 81 50       181 return ([$host, $port, '*']) if $proto =~ /UNIX/i;
113 81 50 0     233 $port = (getservbyname($port, $proto))[2] or die "Could not determine port number from host [$host]:$_[2]\n" if $port =~ /\D/;
114              
115 81         111 my @info;
116 81 100 33     338 if ($host =~ /^\d+(?:\.\d+){3}$/) {
    50 0        
    50 33        
117 1 50       25 my $addr = Socket::inet_aton($host) or die "Unresolveable host [$host]:$port: invalid ip\n";
118 1         16 push @info, [Socket::inet_ntoa($addr), $port, 4]
119 80         8624 } elsif (!$ENV{'NO_IPV6'} && eval { require Socket6 } && (eval { require IO::Socket::IP } || eval { require IO::Socket::INET6 })) {
120 0 0       0 my $proto_id = getprotobyname(lc($proto) eq 'udp' ? 'udp' : 'tcp');
121 0 0       0 my $socktype = lc($proto) eq 'udp' ? Socket::SOCK_DGRAM() : Socket::SOCK_STREAM();
122 0 0       0 my @res = Socket6::getaddrinfo($host eq '*' ? '' : $host, $port, Socket::AF_UNSPEC(), $socktype, $proto_id, Socket6::AI_PASSIVE());
123 0 0       0 die "Unresolveable [$host]:$port: $res[0]\n" if @res < 5;
124 0         0 while (@res >= 5) {
125 0         0 my ($afam, $socktype, $proto, $saddr, $canonname) = splice @res, 0, 5;
126 0         0 my @res2 = Socket6::getnameinfo($saddr, Socket6::NI_NUMERICHOST() | Socket6::NI_NUMERICSERV());
127 0 0       0 die "getnameinfo failed on [$host]:$port: $res2[0]\n" if @res2 < 2;
128 0         0 my ($ip, $port) = @res2;
129 0 0       0 my $ipv = ($afam == Socket6::AF_INET6()) ? 6 : ($afam == Socket::AF_INET()) ? 4 : '*';
    0          
130 0         0 push @info, [$ip, $port, $ipv];
131             }
132 0 0       0 my %ipv6mapped = map {$_->[0] eq '::' ? ('0.0.0.0' => $_) : $_->[0] =~ /^::ffff:(\d+(?:\.\d+){3})$/ ? ($1 => $_) : ()} @info;
  0 0       0  
133 0 0 0     0 if ((scalar(keys %ipv6mapped)
      0        
134 0         0 && grep {$ipv6mapped{$_->[0]}} @info)
135             && not my $only = $class->_bindv6only) {
136 0         0 for my $i4 (@info) {
137 0   0     0 my $i6 = $ipv6mapped{$i4->[0]} || next;
138 0 0 0     0 if ($host eq '*' && $i6->[0] eq '::' && !length($only)
      0        
      0        
139 0 0       0 && !eval{IO::Socket::INET6->new->configure({LocalAddr => '', LocalPort => 0, Listen => 1, ReuseAddr => 1, Domain => Socket6::AF_INET6()}) or die $!}) {
140 0         0 $i4->[3] = "Host [*] resolved to IPv6 address [::] but IO::Socket::INET6->new fails: $@";
141 0         0 $i6->[0] = '';
142             } else {
143 0 0       0 $i6->[3] = "Not including resolved host [$i4->[0]] IPv4 because it ".(length($only) ? 'will' : 'should')." be handled by [$i6->[0]] IPv6";
144 0         0 $i4->[0] = '';
145             }
146             }
147 0         0 @info = grep {length $_->[0]} @info;
  0         0  
148             }
149             } elsif ($host =~ /:/) {
150 0         0 die "Unresolveable host [$host]:$port - could not load IO::Socket::INET6: $@";
151             } else {
152 80         190 my @addr;
153 80 100       186 if ($host eq '*') {
154 40         69 push @addr, Socket::INADDR_ANY();
155             } else {
156 40         16382 (undef, undef, undef, undef, @addr) = gethostbyname($host);
157 40 50       237 die "Unresolveable host [$host]:$port via IPv4 gethostbyname\n" if !@addr;
158             }
159 80         716 push @info, [Socket::inet_ntoa($_), $port, 4] for @addr
160             }
161              
162 81         400 return @info;
163             }
164              
165             sub _bindv6only {
166 0     0   0 my $class = shift;
167 0         0 my $val = $class->_sysctl('net.ipv6.bindv6only'); # linux
168 0 0       0 $val = $class->_sysctl('net.inet6.ip6.v6only') if ! length($val); # bsd
169 0         0 return $val;
170             }
171              
172             sub _sysctl {
173 0     0   0 my ($class, $key) = @_;
174 0         0 (my $file = "/proc/sys/$key") =~ y|.|/|;
175 0 0       0 if (-e $file) {
    0          
176 0 0       0 open my $fh, "<", $file or return '';
177 0   0     0 my $val = <$fh> || return '';
178 0         0 chomp $val;
179 0         0 return $val;
180             } elsif (-x "/sbin/sysctl") {
181 0         0 my $val = (split /\s+/, `/sbin/sysctl -n $key 2>/dev/null`)[0];
182 0 0       0 return defined($val) ? $val : '';
183             }
184 0         0 return '';
185             }
186              
187             sub object {
188 100     100 1 177 my ($class, $info, $server) = @_;
189 100         164 my $proto_class = $info->{'proto'};
190 100 100       297 if ($proto_class !~ /::/) {
191 99 50       415 $server->fatal("Invalid proto class \"$proto_class\"") if $proto_class !~ /^\w+$/;
192 99         237 $proto_class = "Net::Server::Proto::" .uc($proto_class);
193             }
194 100         463 (my $file = "${proto_class}.pm") =~ s|::|/|g;
195 100 50       169 $server->fatal("Unable to load module for proto \"$proto_class\": $@") if ! eval { require $file };
  100         14746  
196 100         576 return $proto_class->object($info, $server);
197             }
198              
199 107 50   107 0 343 sub requires_ipv6 { $requires_ipv6 ? 1 : undef }
200              
201             sub ipv6_package {
202 0     0 0   my ($class, $server) = @_;
203 0 0         return $ipv6_package if $ipv6_package;
204              
205 0 0         eval { require Socket6 }
  0            
206             or $server->fatal("Port configuration using IPv6 could not be started becauses of Socket6 library issues: $@");
207              
208 0           my $pkg = $server->{'server'}->{'ipv6_package'};
209 0 0         if ($pkg) {
    0          
    0          
    0          
210 0           (my $file = "$pkg.pm") =~ s|::|/|g;
211 0 0         eval { require $file } or $server->fatal("Could not load ipv6_package $pkg: $@");
  0            
212             } elsif ($INC{'IO/Socket/IP.pm'}) { # already loaded
213 0           $pkg = 'IO::Socket::IP';
214             } elsif ($INC{'IO/Socket/INET6.pm'}) {
215 0           $pkg = 'IO::Socket::INET6';
216 0           } elsif (eval { require IO::Socket::IP }) {
217 0           $pkg = 'IO::Socket::IP';
218             } else {
219 0           my $err = $@;
220 0 0         if (eval { require IO::Socket::INET6 }) {
  0            
221 0           $pkg = 'IO::Socket::INET6';
222             } else {
223 0           $server->fatal("Port ocnfiguration using IPv6 could not be started. Could not find or load IO::Socket::IP or IO::Socket::INET6:\n $err $@")
224             }
225             }
226 0           return $ipv6_package = $pkg;
227             }
228              
229             1;
230              
231             __END__