File Coverage

blib/lib/MojoX/Ping.pm
Criterion Covered Total %
statement 29 116 25.0
branch 1 28 3.5
condition 0 6 0.0
subroutine 9 21 42.8
pod 1 3 33.3
total 40 174 22.9


line stmt bran cond sub pod time code
1             package MojoX::Ping;
2              
3 1     1   1271 use strict;
  1         2  
  1         30  
4 1     1   5 use warnings;
  1         3  
  1         47  
5              
6             our $VERSION = 0.512;
7 1     1   13 use base 'Mojo::Base';
  1         2  
  1         97  
8              
9 1     1   5 use Mojo::IOLoop;
  1         1  
  1         14  
10 1     1   26 use Socket qw/SOCK_RAW/;
  1         2  
  1         59  
11 1     1   5 use Time::HiRes 'time';
  1         1  
  1         9  
12 1     1   127 use IO::Socket::INET qw/sockaddr_in inet_aton/;
  1         1  
  1         11  
13 1     1   73 use List::Util ();
  1         2  
  1         1359  
14             require Carp;
15              
16             __PACKAGE__->attr(ioloop => sub { Mojo::IOLoop->singleton });
17             __PACKAGE__->attr(interval => 0.2);
18             __PACKAGE__->attr(timeout => 5);
19             __PACKAGE__->attr('error');
20              
21             my $ICMP_PING = 'ccnnnA*';
22              
23             my $ICMP_ECHOREPLY = 0; # Echo Reply
24             my $ICMP_DEST_UNREACH = 3; # Destination Unreachable
25             my $ICMP_SOURCE_QUENCH = 4; # Source Quench
26             my $ICMP_REDIRECT = 5; # Redirect (change route)
27             my $ICMP_ECHO = 8; # Echo Request
28             my $ICMP_TIME_EXCEEDED = 11; # Time Exceeded
29              
30             sub new {
31 1     1 1 449 my $class = shift;
32              
33 1         8 my $self = $class->SUPER::new(@_);
34              
35             # Create RAW socket
36 1 50       13 my $socket = IO::Socket::INET->new(
37             Proto => 'icmp',
38             Type => SOCK_RAW,
39             Blocking => 0
40             ) or Carp::croak "Unable to create icmp socket : $!";
41              
42 1         205 $self->{_socket} = $socket;
43              
44             # Create Poll object
45             $self->ioloop->iowatcher->watch(
46             $socket,
47 0     0     sub { $self->_on_read },
48 0     0     sub { $self->_send_requests }
49 1         5 );
50              
51 0           $self->ioloop->iowatcher->change($socket, 1, 1);
52              
53             # Ping tasks
54 0           $self->{_tasks} = [];
55 0           $self->{_tasks_out} = [];
56              
57 0           return $self;
58             }
59              
60             sub ping {
61 0     0 0   my ($self, $host, $times, $cb) = @_;
62              
63 0           my $socket = $self->{_socket};
64              
65 0           my $ip = inet_aton($host);
66              
67 0           my $request = {
68             host => $host,
69             times => $times,
70             results => [],
71             cb => $cb,
72             identifier => int(rand 0x10000),
73             destination => scalar sockaddr_in(0, $ip),
74             };
75              
76 0           push @{$self->{_tasks}}, $request;
  0            
77              
78 0           push @{$self->{_tasks_out}}, $request;
  0            
79              
80 0           $self->ioloop->iowatcher->change($socket, 1, 1);
81              
82 0           return $self;
83             }
84              
85             sub start {
86 0     0 0   my ($self) = @_;
87 0           $self->ioloop->start;
88              
89 0           return $self;
90             }
91              
92             sub _send_requests {
93 0     0     my $self = shift;
94              
95 0           foreach my $request (@{$self->{_tasks_out}}) {
  0            
96 0           $self->_send_request($request);
97             }
98              
99 0           $self->{_tasks_out} = [];
100 0           $self->ioloop->iowatcher->change($self->{_socket}, 1, 0);
101             }
102              
103             sub _on_read {
104 0     0     my $self = shift;
105              
106 0           my $socket = $self->{_socket};
107 0           $socket->sysread(my $chunk, 4194304, 0);
108              
109 0           my $icmp_msg = substr $chunk, 20;
110              
111 0           my ($type, $identifier, $sequence, $data);
112              
113 0           $type = unpack 'c', $icmp_msg;
114              
115 0 0 0       if ($type == $ICMP_ECHOREPLY) {
    0          
116 0           ($type, $identifier, $sequence, $data) =
117             (unpack $ICMP_PING, $icmp_msg)[0, 3, 4, 5];
118             }
119             elsif ($type == $ICMP_DEST_UNREACH || $type == $ICMP_TIME_EXCEEDED) {
120 0           ($identifier, $sequence) = unpack('nn', substr($chunk, 52));
121             }
122             else {
123              
124             # Don't mind
125 0           return;
126             }
127              
128             # Find our task
129             my $request =
130 0     0     List::Util::first { $identifier == $_->{identifier} }
131 0           @{$self->{_tasks}};
  0            
132              
133 0 0         return unless $request;
134              
135             # Is it response to our latest message?
136 0 0         return unless $sequence == @{$request->{results}} + 1;
  0            
137              
138 0 0         if ($type == $ICMP_ECHOREPLY) {
    0          
    0          
139              
140             # Check data
141 0 0         if ($data eq $request->{data}) {
142 0           $self->_store_result($request, 'OK');
143             }
144             else {
145 0           $self->_store_result($request, 'MALFORMED');
146             }
147             }
148             elsif ($type == $ICMP_DEST_UNREACH) {
149 0           $self->_store_result($request, 'DEST_UNREACH');
150             }
151             elsif ($type == $ICMP_TIME_EXCEEDED) {
152 0           $self->_store_result($request, 'TIMEOUT');
153             }
154             }
155              
156             sub _store_result {
157 0     0     my ($self, $request, $result) = @_;
158              
159 0           my $results = $request->{results};
160              
161             # Clear request specific data
162 0 0         $self->ioloop->drop(delete $request->{timer}) if exists $request->{timer};
163              
164 0           push @$results, [$result, time - $request->{start}];
165              
166 0 0 0       if (@$results == $request->{times} || $result eq 'ERROR') {
167              
168             # Cleanup
169 0           my $tasks = $self->{_tasks};
170 0           for my $i (0 .. scalar @$tasks) {
171 0 0         if ($tasks->[$i] == $request) {
172 0           splice @$tasks, $i, 1;
173 0           last;
174             }
175             }
176              
177             # Testing done
178 0           $request->{cb}->($self, $results);
179              
180 0           undef $request;
181             }
182              
183             # Perform another check
184             else {
185              
186             # Setup interval timer before next request
187             $self->ioloop->timer(
188             $self->interval => sub {
189 0     0     push @{$self->{_tasks_out}}, $request;
  0            
190 0           $self->ioloop->iowatcher->change($self->{_socket}, 1, 1);
191             }
192 0           );
193             }
194             }
195              
196             sub _send_request {
197 0     0     my ($self, $request) = @_;
198              
199 0           my $checksum = 0x0000;
200 0           my $identifier = $request->{identifier};
201 0           my $sequence = @{$request->{results}} + 1;
  0            
202 0           my $data = 'abcdef';
203              
204 0           my $msg = pack $ICMP_PING,
205             $ICMP_ECHO, 0x00, $checksum,
206             $identifier, $sequence, $data;
207              
208 0           $checksum = $self->_icmp_checksum($msg);
209              
210 0           $msg = pack $ICMP_PING,
211             0x08, 0x00, $checksum,
212             $identifier, $sequence, $data;
213              
214 0           $request->{data} = $data;
215              
216 0           $request->{start} = time;
217              
218             $request->{timer} = $self->ioloop->timer(
219             $self->timeout => sub {
220 0     0     my ($loop) = @_;
221 0           $self->_store_result($request, 'TIMEOUT');
222             }
223 0           );
224              
225 0           my $socket = $self->{_socket};
226              
227 0 0         $socket->send($msg, 0, $request->{destination}) or die "$!";
228             }
229              
230             sub _icmp_checksum {
231 0     0     my ($self, $msg) = @_;
232              
233 0           my $res = 0;
234 0           foreach my $int (unpack "n*", $msg) {
235 0           $res += $int;
236             }
237              
238             # Add possible odd byte
239 0 0         $res += unpack('C', substr($msg, -1, 1)) << 8
240             if length($msg) % 2;
241              
242             # Fold high into low
243 0           $res = ($res >> 16) + ($res & 0xffff);
244              
245             # Two times
246 0           $res = ($res >> 16) + ($res & 0xffff);
247              
248 0           return ~$res;
249             }
250              
251             1;
252             __END__