File Coverage

blib/lib/IO/Async/Internals/Connector.pm
Criterion Covered Total %
statement 111 140 79.2
branch 33 72 45.8
condition 35 58 60.3
subroutine 17 17 100.0
pod 0 2 0.0
total 196 289 67.8


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2008-2024 -- leonerd@leonerd.org.uk
5              
6             package # hide from CPAN
7             IO::Async::Internals::Connector 0.802;
8              
9 3     3   38 use v5.14;
  3         10  
10 3     3   13 use warnings;
  3         4  
  3         201  
11              
12 3     3   14 use Scalar::Util qw( weaken blessed );
  3         12  
  3         200  
13              
14 3     3   12 use POSIX qw( EINPROGRESS );
  3         5  
  3         28  
15 3     3   196 use Socket qw( SOL_SOCKET SO_ERROR );
  3         4  
  3         180  
16              
17 3     3   13 use Future 0.44; # ->result
  3         59  
  3         67  
18 3     3   12 use Future::Utils 0.18 qw( try_repeat_until_success );
  3         49  
  3         175  
19              
20 3     3   16 use IO::Async::OS;
  3         6  
  3         83  
21              
22 3     3   30 use Carp;
  3         5  
  3         220  
23              
24             use constant {
25 3         4247 CONNECT_EWOULDLBOCK => IO::Async::OS->HAVE_CONNECT_EWOULDBLOCK,
26             HAVE_SOCKADDR_IN6 => IO::Async::OS->HAVE_SOCKADDR_IN6,
27 3     3   28 };
  3         4  
28              
29             # Internal constructor
30             sub new
31             {
32 3     3 0 7 my $class = shift;
33 3         10 my ( %params ) = @_;
34              
35 3 50       16 my $loop = delete $params{loop} or croak "Expected a 'loop'";
36              
37 3         8 my $self = bless {}, $class;
38 3         19 weaken( $self->{loop} = $loop );
39              
40 3         17 return $self;
41             }
42              
43             ## Utility function
44             sub _get_sock_err
45             {
46 11     11   23 my ( $sock ) = @_;
47              
48 11         67 my $err = $sock->getsockopt( SOL_SOCKET, SO_ERROR );
49              
50 11 50       222 if( defined $err ) {
51             # 0 means no error, but is still defined
52 11 100       48 return undef if !$err;
53              
54 1         4 $! = $err;
55 1         7 return $!;
56             }
57              
58             # It seems we can't call getsockopt to query SO_ERROR. We'll try getpeername
59 0 0       0 if( defined getpeername( $sock ) ) {
60 0         0 return undef;
61             }
62              
63 0         0 my $peername_errno = $!+0;
64 0         0 my $peername_errstr = "$!";
65              
66             # Not connected so we know this ought to fail
67 0 0       0 if( read( $sock, my $buff, 1 ) ) {
68             # That was most unexpected. getpeername fails because we're not
69             # connected, yet read succeeds.
70 0         0 warn "getpeername fails with $peername_errno ($peername_errstr) but read is successful\n";
71 0         0 warn "Please see http://rt.cpan.org/Ticket/Display.html?id=38382\n";
72              
73 0         0 $! = $peername_errno;
74 0         0 return $!;
75             }
76              
77 0         0 return $!;
78             }
79              
80             sub _connect_addresses
81             {
82 15     15   22 my $self = shift;
83 15         28 my ( $addrlist, $on_fail ) = @_;
84              
85 15         27 my $loop = $self->{loop};
86              
87 15         23 my ( $connecterr, $binderr, $socketerr );
88              
89             my $future = try_repeat_until_success {
90 15     15   1205 my $addr = shift;
91             my ( $family, $socktype, $protocol, $localaddr, $peeraddr ) =
92 15         30 @{$addr}{qw( family socktype protocol localaddr peeraddr )};
  15         49  
93              
94 15         61 my $sock = IO::Async::OS->socket( $family, $socktype, $protocol );
95              
96 15 50       2159 if( !$sock ) {
97 0         0 $socketerr = $!;
98 0 0       0 $on_fail->( "socket", $family, $socktype, $protocol, $! ) if $on_fail;
99 0         0 return Future->fail( 1 );
100             }
101              
102 15 50 66     69 if( $localaddr and not $sock->bind( $localaddr ) ) {
103 0         0 $binderr = $!;
104 0 0       0 $on_fail->( "bind", $sock, $localaddr, $! ) if $on_fail;
105 0         0 return Future->fail( 1 );
106             }
107              
108 15         160 $sock->blocking( 0 );
109              
110             # TODO: $sock->connect returns success masking EINPROGRESS
111 15         1673 my $ret = connect( $sock, $peeraddr );
112 15 100 100     191 if( $ret ) {
    100          
113             # Succeeded already? Dubious, but OK. Can happen e.g. with connections to
114             # localhost, or UNIX sockets, or something like that.
115 2         11 return Future->done( $sock );
116             }
117             elsif( $! != EINPROGRESS and !CONNECT_EWOULDLBOCK || $! != POSIX::EWOULDBLOCK ) {
118 2         7 $connecterr = $!;
119 2 50       10 $on_fail->( "connect", $sock, $peeraddr, $! ) if $on_fail;
120 2         29 return Future->fail( 1 );
121             }
122              
123             # Else
124 11         59 my $f = $loop->new_future;
125             $loop->watch_io(
126             handle => $sock,
127             on_write_ready => sub {
128 11         49 $loop->unwatch_io( handle => $sock, on_write_ready => 1 );
129              
130 11         32 my $err = _get_sock_err( $sock );
131              
132 11 100       54 return $f->done( $sock ) if !$err;
133              
134 1         3 $connecterr = $!;
135 1 50       7 $on_fail->( "connect", $sock, $peeraddr, $err ) if $on_fail;
136 1         15 return $f->fail( 1 );
137             },
138 11         132 );
139             $f->on_cancel(
140 0         0 sub { $loop->unwatch_io( handle => $sock, on_write_ready => 1 ); }
141 11         70 );
142 11         270 return $f;
143 15         119 } foreach => $addrlist;
144              
145             return $future->else_with_f( sub {
146 3     3   375 my $f = shift;
147              
148 3 50       11 return $future->new->fail( "connect: $connecterr", connect => connect => $connecterr )
149             if $connecterr;
150 0 0       0 return $future->new->fail( "bind: $binderr", connect => bind => $binderr )
151             if $binderr;
152 0 0       0 return $future->new->fail( "socket: $socketerr", connect => socket => $socketerr )
153             if $socketerr;
154              
155             # If it gets this far then something went wrong
156 0         0 return $f;
157 15         1653 } );
158             }
159              
160             sub connect
161             {
162 15     15 0 27 my $self = shift;
163 15         42 my ( %params ) = @_;
164              
165 15         27 my $loop = $self->{loop};
166              
167 15         24 my $on_fail = $params{on_fail};
168              
169 15         24 my %gai_hints;
170 15   100     104 exists $params{$_} and $gai_hints{$_} = $params{$_} for qw( family socktype protocol flags );
171              
172 15 50 66     88 if( exists $params{host} or exists $params{local_host} or exists $params{local_port} ) {
      33        
173             # We'll be making a ->getaddrinfo call
174             defined $gai_hints{socktype} or defined $gai_hints{protocol} or
175 6 50 33     24 carp "Attempting to ->connect without either 'socktype' or 'protocol' hint is not portable";
176             }
177              
178 15         20 my $peeraddrfuture;
179 15 100 66     85 if( exists $params{host} and exists $params{service} ) {
    50 33        
    0          
180 6 50       16 my $host = $params{host} or croak "Expected 'host'";
181 6 50       28 my $service = $params{service} or croak "Expected 'service'";
182              
183 6         69 $peeraddrfuture = $loop->resolver->getaddrinfo(
184             host => $host,
185             service => $service,
186             %gai_hints,
187             );
188             }
189             elsif( exists $params{addrs} or exists $params{addr} ) {
190 9 50       23 my @addrs = exists $params{addrs} ? @{ $params{addrs} } : ( $params{addr} );
  0         0  
191              
192             # Warn about some common mistakes
193 9         16 foreach my $peer ( @addrs ) {
194 9         76 my ( $p_family, undef, undef, $p_addr ) = IO::Async::OS->extract_addrinfo( $peer );
195              
196 9         30 local our @CARP_NOT = qw( IO::Async::Loop IO::Async::Handle );
197              
198 9 100       27 if( $p_family == Socket::AF_INET ) {
    50          
199 7 50       50 carp "Connecting to 0.0.0.0 is non-portable and ill-advised"
200             if ( Socket::unpack_sockaddr_in $p_addr )[1] eq Socket::INADDR_ANY;
201             }
202             elsif( HAVE_SOCKADDR_IN6 and $p_family == Socket::AF_INET6 ) {
203 0 0       0 carp "Connecting to :: is non-portable and ill-advised"
204             if ( Socket::unpack_sockaddr_in6 $p_addr )[1] eq Socket::IN6ADDR_ANY;
205             }
206             }
207              
208 9         42 $peeraddrfuture = $loop->new_future->done( @addrs );
209             }
210             elsif( exists $params{peer} ) {
211 0         0 my $peer = delete $params{peer};
212 0 0 0     0 croak "Expected 'peer' to be an IO::Socket or subclass"
213             unless blessed $peer and $peer->isa( "IO::Socket" );
214              
215 0         0 my $p_family = $peer->sockdomain;
216              
217 0         0 $peeraddrfuture = $loop->new_future->done(
218             [ $p_family, $peer->socktype, $peer->protocol, IO::Async::OS->make_addr_for_peer( $p_family, $peer->sockname ) ]
219             );
220             }
221             else {
222 0         0 croak "Expected 'host' and 'service' or 'addrs' or 'addr' arguments";
223             }
224              
225 15         342 my $localaddrfuture;
226 15 100 66     102 if( defined $params{local_host} or defined $params{local_service} ) {
    50 33        
227             # Empty is fine on either of these
228 1         3 my $host = $params{local_host};
229 1         3 my $service = $params{local_service};
230              
231 1         3 $localaddrfuture = $loop->resolver->getaddrinfo(
232             host => $host,
233             service => $service,
234             %gai_hints,
235             );
236             }
237             elsif( exists $params{local_addrs} or exists $params{local_addr} ) {
238 0 0       0 $localaddrfuture = $loop->new_future->done( exists $params{local_addrs} ? @{ $params{local_addrs} } : ( $params{local_addr} ) );
  0         0  
239             }
240             else {
241 14         36 $localaddrfuture = $loop->new_future->done( {} );
242             }
243              
244             return Future->needs_all( $peeraddrfuture, $localaddrfuture )
245             ->then( sub {
246 15     15   2291 my @peeraddrs = $peeraddrfuture->result;
247 15         224 my @localaddrs = $localaddrfuture->result;
248              
249 15         133 my @addrs;
250              
251 15         41 foreach my $local ( @localaddrs ) {
252 15         71 my ( $l_family, $l_socktype, $l_protocol, $l_addr ) =
253             IO::Async::OS->extract_addrinfo( $local, 'local_addr' );
254 15         30 foreach my $peer ( @peeraddrs ) {
255 15         39 my ( $p_family, $p_socktype, $p_protocol, $p_addr ) =
256             IO::Async::OS->extract_addrinfo( $peer );
257              
258 15 50 66     48 next if $l_family and $p_family and $l_family != $p_family;
      66        
259 15 50 66     43 next if $l_socktype and $p_socktype and $l_socktype != $p_socktype;
      66        
260 15 50 66     45 next if $l_protocol and $p_protocol and $l_protocol != $p_protocol;
      66        
261              
262 15   66     154 push @addrs, {
      66        
      100        
263             family => $l_family || $p_family,
264             socktype => $l_socktype || $p_socktype,
265             protocol => $l_protocol || $p_protocol,
266             localaddr => $l_addr,
267             peeraddr => $p_addr,
268             };
269             }
270             }
271              
272 15         46 return $self->_connect_addresses( \@addrs, $on_fail );
273 15         421 } );
274             }
275              
276             0x55AA;