File Coverage

blib/lib/Mojo/IOLoop/Client/Paranoid.pm
Criterion Covered Total %
statement 67 96 69.7
branch 14 40 35.0
condition 6 17 35.2
subroutine 13 17 76.4
pod 1 1 100.0
total 101 171 59.0


line stmt bran cond sub pod time code
1             package Mojo::IOLoop::Client::Paranoid 0.01;
2 2     2   43 use 5.020;
  2         8  
3 2     2   1177 use experimental 'signatures';
  2         6502  
  2         13  
4              
5 2     2   335 use IO::Socket::IP;
  2         5  
  2         21  
6 2     2   1411 use IO::Socket::UNIX;
  2         4  
  2         18  
7 2     2   5296 use Scalar::Util qw(weaken);
  2         4  
  2         122  
8 2     2   14 use Socket qw(IPPROTO_TCP SOCK_STREAM TCP_NODELAY);
  2         5  
  2         177  
9 2     2   14 use Mojo::Base 'Mojo::IOLoop::Client';
  2         3  
  2         42  
10              
11 2     2   1630 use Net::DNS::Paranoid; # does this handle IPv6 ??
  2         131807  
  2         380  
12              
13             =head1 NAME
14              
15             Mojo::IOLoop::Client::Paranoid - paranoid IOLoop proxy
16              
17             =head1 SYNOPSIS
18              
19             my $user_client = Mojo::IOLoop::Client::Paranoid->new(
20             paranoid_dns => Net::DNS::Paranoid->new(
21             blocked_hosts => [qr{\.dev.example\.com$}]
22             ),
23             );
24             $user_client->connect($host); # will not connect to internal hosts
25              
26             =head1 METHODS
27              
28             =head2 C<< ->new >>
29              
30             my $client = Mojo::IOLoop::Client::Paranoid->new(
31             paranoid_dns => Net::DNS::Paranoid->new(...),
32             )
33              
34             Creates a new paranoid client. The C<< ->connect >> method will check that the
35             given hostname does not resolve to any internal or blacklisted IP address.
36             See L for how to configure the white- and blacklists.
37              
38             =cut
39              
40 2 50   2   29 use constant NNR => $ENV{MOJO_NO_NNR} ? 0 : !!eval { require Net::DNS::Native; Net::DNS::Native->VERSION('0.15'); 1 };
  2         5  
  2         14  
  2         3030  
  0         0  
  0         0  
41             our $NDN;
42              
43             has 'paranoid_dns' => sub { Net::DNS::Paranoid->new() };
44              
45             # We can't easily override the DNS resolver that Mojolicious uses as it is
46             # a lexical variable, and everything else would involve monkeypatching.
47             # So we copy the relevant code here.
48              
49 1 0 33 1   10 sub _port { $_[0]{socks_port} || $_[0]{port} || ($_[0]{tls} ? 443 : 80) }
    50          
50              
51 4     4 1 9 sub connect( $self, @args) {
  4         6  
  4         16  
  4         5  
52 4 50       23 my ($args) = (ref $args[0] ? $args[0] : {@args});
53             # Timeout
54 4         8 weaken $self;
55 4         16 my $reactor = $self->reactor;
56 4         72 my $r = $reactor;
57 4   50 0   30 $self->{timer} = $reactor->timer($args->{timeout} || 10, sub { $self->emit(error => 'Connect timeout') });
  0         0  
58             # Blocking name resolution
59 4   66     189 $_ && s/[[\]]//g for @$args{qw(address socks_address)};
60 4   33     25 my $address = $args->{socks_address} || ($args->{address} ||= '127.0.0.1');
61 4 50   4   18 return $reactor->next_tick(sub { $self && $self->_connect($args) }) if !NNR || $args->{handle} || $args->{path};
  4         526  
62             # Non-blocking name resolution
63 0         0 my $paranoid_dns = $self->paranoid_dns;
64              
65 0 0       0 if( $paranoid_dns->_bad_host($args->{address})) {
66             # We need to delay the error by one tick so the callback variables get
67             # initialized
68             # warn "Immediate bad host ($self)";
69 0 0   0   0 return $reactor->next_tick( sub { warn "Lost self" unless $self; $self && $self->emit(error => "Can't connect: Bad host '$args->{address}'"); undef $self });
  0 0       0  
  0         0  
  0         0  
70             }
71              
72 0   0     0 $NDN //= Net::DNS::Native->new(pool => 5, extra_thread => 1);
73             #warn "# Using Net::DNS::Native to resolve '$address'";
74             my $handle = $self->{dns}
75 0         0 = $NDN->getaddrinfo($address, _port($args), {protocol => IPPROTO_TCP, socktype => SOCK_STREAM});
76             $reactor->io(
77             $handle => sub {
78 0     0   0 my $reactor = shift;
79 0         0 $reactor->remove($self->{dns});
80 0         0 my ($err, @res) = $NDN->get_result(delete $self->{dns});
81 0 0       0 return $self->emit(error => "Can't resolve: $err") if $err;
82              
83             #use Data::Dumper;
84             #warn "# $address resolved via Net::DNS::Native: " . Dumper \@res;
85              
86 0         0 $args->{addr_info} = \@res;
87 0         0 $self->_connect($args);
88             }
89 0         0 )->watch($handle, 1, 0);
90             }
91              
92 0     0   0 sub _to_address( $self, $addrinfo) {
  0         0  
  0         0  
  0         0  
93             return $addrinfo->{family} == AF_INET ?
94             inet_ntoa((unpack_sockaddr_in($addrinfo->{addr}))[1]) : # IPv4
95 0 0       0 Socket::inet_ntop(AF_INET6, (unpack_sockaddr_in6($addrinfo->{addr}))[1]); # IPv6
96             }
97              
98 4     4   11 sub _connect($self, $args) {
  4         5  
  4         7  
  4         7  
99 4         8 my $path = $args->{path};
100 4         13 my $handle = $self->{handle} = $args->{handle};
101 4 50       39 unless ($handle) {
102 4         12 my $paranoid_dns = $self->paranoid_dns;
103 4 50       23 my $class = $path ? 'IO::Socket::UNIX' : 'IO::Socket::IP';
104 4         11 my %options = (Blocking => 0);
105              
106             # UNIX domain socket
107 4 50       9 if ($path) { $options{Peer} = $path }
  0         0  
108             # IP socket
109             else {
110 4 50       15 if(!$args->{addr_info}) {
111 4         20 my ($resolved, $errmsg) = $paranoid_dns->resolve($args->{address});
112 4 100       267318 if( $resolved ) {
113 1         4 my $addr = $resolved->[0];
114 1         5 $options{PeerAddr} = $resolved;
115             } else {
116 3         18 return $self->emit(error => "Bad host: $errmsg");
117             }
118             }
119              
120 1 50       10 if (my $info = $args->{addr_info}) {
121             #use Data::Dumper; warn "Using pre-received addr_info " . Dumper $info;
122              
123 0         0 my $addr = $self->_to_address( $info->[0] );
124              
125 0         0 my ($resolved, $errmsg) = $paranoid_dns->_bad_host( $addr );
126 0 0       0 if( $errmsg ) {
127 0         0 return $self->emit(error => "Bad host: $errmsg");
128             };
129 0         0 $options{PeerAddrInfo} = $info;
130              
131             } else {
132 1   33     12 $options{PeerAddr} = $args->{socks_address} || $args->{address};
133 1         6 $options{PeerPort} = _port($args);
134             }
135 1 50       5 @options{keys %{$args->{socket_options}}} = values %{$args->{socket_options}} if $args->{socket_options};
  1         3  
  1         29  
136             }
137 1 50       12 return $self->emit(error => "Can't connect: $@") unless $self->{handle} = $handle = $class->new(%options);
138             }
139 1         54211 $handle->blocking(0);
140 1 50       40 $path ? $self->_try_socks($args) : $self->_wait('_ready', $handle, $args);
141             }
142              
143             1;
144              
145             =head1 SEE ALSO
146              
147             L
148              
149             L
150              
151             =cut
152