File Coverage

blib/lib/POE/Component/Client/NTP.pm
Criterion Covered Total %
statement 104 117 88.8
branch 9 20 45.0
condition 4 8 50.0
subroutine 14 14 100.0
pod 1 1 100.0
total 132 160 82.5


line stmt bran cond sub pod time code
1             package POE::Component::Client::NTP;
2             $POE::Component::Client::NTP::VERSION = '0.14';
3             #ABSTRACT: A POE Component to query NTP servers
4              
5 2     2   225434 use strict;
  2         5  
  2         70  
6 2     2   9 use warnings;
  2         4  
  2         56  
7 2     2   6 use Carp;
  2         2  
  2         102  
8 2     2   8 use Socket qw[:all];
  2         2  
  2         2449  
9 2     2   1142 use IO::Socket::IP;
  2         9165  
  2         8  
10 2     2   1595 use POE;
  2         3  
  2         19  
11 2     2   553 use Time::HiRes qw[time];
  2         3  
  2         15  
12              
13             our %MODE = (
14             '0' => 'reserved',
15             '1' => 'symmetric active',
16             '2' => 'symmetric passive',
17             '3' => 'client',
18             '4' => 'server',
19             '5' => 'broadcast',
20             '6' => 'reserved for NTP control message',
21             '7' => 'reserved for private use'
22             );
23              
24             our %STRATUM = (
25             '0' => 'unspecified or unavailable',
26             '1' => 'primary reference (e.g., radio clock)',
27             );
28              
29             for(2 .. 15){
30             $STRATUM{$_} = 'secondary reference (via NTP or SNTP)';
31             }
32              
33             for(16 .. 255){
34             $STRATUM{$_} = 'reserved';
35             }
36              
37             our %STRATUM_ONE_TEXT = (
38             'LOCL' => 'uncalibrated local clock used as a primary reference for a subnet without external means of synchronization',
39             'PPS' => 'atomic clock or other pulse-per-second source individually calibrated to national standards',
40             'ACTS' => 'NIST dialup modem service',
41             'USNO' => 'USNO modem service',
42             'PTB' => 'PTB (Germany) modem service',
43             'TDF' => 'Allouis (France) Radio 164 kHz',
44             'DCF' => 'Mainflingen (Germany) Radio 77.5 kHz',
45             'MSF' => 'Rugby (UK) Radio 60 kHz',
46             'WWV' => 'Ft. Collins (US) Radio 2.5, 5, 10, 15, 20 MHz',
47             'WWVB' => 'Boulder (US) Radio 60 kHz',
48             'WWVH' => 'Kaui Hawaii (US) Radio 2.5, 5, 10, 15 MHz',
49             'CHU' => 'Ottawa (Canada) Radio 3330, 7335, 14670 kHz',
50             'LORC' => 'LORAN-C radionavigation system',
51             'OMEG' => 'OMEGA radionavigation system',
52             'GPS' => 'Global Positioning Service',
53             'GOES' => 'Geostationary Orbit Environment Satellite',
54             );
55              
56             our %LEAP_INDICATOR = (
57             '0' => 'no warning',
58             '1' => 'last minute has 61 seconds',
59             '2' => 'last minute has 59 seconds)',
60             '3' => 'alarm condition (clock not synchronized)'
61             );
62              
63             {
64              
65 2     2   575 use constant NTP_ADJ => 2208988800;
  2         4  
  2         2833  
66              
67             my @ntp_packet_fields =
68             (
69             'Leap Indicator',
70             'Version Number',
71             'Mode',
72             'Stratum',
73             'Poll Interval',
74             'Precision',
75             'Root Delay',
76             'Root Dispersion',
77             'Reference Clock Identifier',
78             'Reference Timestamp',
79             'Originate Timestamp',
80             'Receive Timestamp',
81             'Transmit Timestamp',
82             );
83              
84             my $frac2bin = sub {
85             my $bin = '';
86             my $frac = shift;
87             while ( length($bin) < 32 ) {
88             $bin = $bin . int( $frac * 2 );
89             $frac = ( $frac * 2 ) - ( int( $frac * 2 ) );
90             }
91             return $bin;
92             };
93              
94             my $bin2frac = sub {
95             my @bin = split '', shift;
96             my $frac = 0;
97             while (@bin) {
98             $frac = ( $frac + pop @bin ) / 2;
99             }
100             return $frac;
101             };
102              
103             my $percision = sub{
104             my $number = shift;
105             if($number > 127){
106             $number -= 255;
107             }
108             return sprintf("%1.4e", 2**$number);
109             };
110              
111             my $unpack_ip = sub {
112             my $ip;
113             my $stratum = shift;
114             my $tmp_ip = shift;
115             if($stratum < 2){
116             $ip = unpack("A4",
117             pack("H8", $tmp_ip)
118             );
119             }else{
120             $ip = sprintf("%d.%d.%d.%d",
121             unpack("C4",
122             pack("H8", $tmp_ip)
123             )
124             );
125             }
126             return $ip;
127             };
128              
129             sub get_ntp_response {
130 2     2 1 478 my $package = shift;
131 2         8 my %opts = @_;
132 2         14 $opts{lc $_} = delete $opts{$_} for keys %opts;
133 2 50       9 croak "$package requires an 'event' argument\n"
134             unless $opts{event};
135 2         3 my $options = delete $opts{options};
136 2 50       6 $opts{host} = 'localhost' unless $opts{host};
137 2 50 33     11 $opts{port} = 123 unless $opts{port} and $opts{port} =~ /^\d+$/;
138 2         6 my $self = bless \%opts, $package;
139 2 50       40 $self->{session_id} = POE::Session->create(
140             object_states => [
141             $self => [qw(_start _socket _dispatch _get_datagram _timeout)],
142             ],
143             heap => $self,
144             ( ref($options) eq 'HASH' ? ( options => $options ) : () ),
145             )->ID();
146 2         214 return $self;
147             }
148              
149             sub _start {
150 2     2   336 my ($kernel,$sender,$self) = @_[KERNEL,SENDER,OBJECT];
151 2         7 $self->{session_id} = $_[SESSION]->ID();
152 2 50 33     27 if ( $kernel == $sender and !$self->{session} ) {
153 0         0 croak "Not called from another POE session and 'session' wasn't set\n";
154             }
155 2         3 my $sender_id;
156 2 50       8 if ( $self->{session} ) {
157 0 0       0 if ( my $ref = $kernel->alias_resolve( $self->{session} ) ) {
158 0         0 $sender_id = $ref->ID();
159             }
160             else {
161 0         0 croak "Could not resolve 'session' to a valid POE session\n";
162             }
163             }
164             else {
165 2         6 $sender_id = $sender->ID();
166             }
167 2         14 $kernel->refcount_increment( $sender_id, __PACKAGE__ );
168 2         59 $self->{sender_id} = $sender_id;
169 2         8 $kernel->detach_myself();
170 2         232 $kernel->yield('_socket');
171 2         185 return;
172             }
173              
174             sub _socket {
175 2     2   479 my ($kernel,$self) = @_[KERNEL,OBJECT];
176 2         24 my $socket = IO::Socket::IP->new( Proto => 'udp' );
177 2         1190 my $ai;
178             {
179 2         2 my %hints = (socktype => SOCK_DGRAM, protocol => IPPROTO_UDP);
  2         6  
180 2         52955 my ($err, @res) = getaddrinfo($self->{host}, $self->{port}, \%hints);
181 2 50       21 if ( $err ) {
182 0         0 $self->{error} = $err;
183 0         0 $kernel->yield('_dispatch');
184 0         0 return;
185             }
186 2         15 $ai = shift @res;
187             }
188 2         28 my $client_localtime = $self->{client_localtime} = time();
189 2         19 my $client_adj_localtime = $client_localtime + NTP_ADJ;
190 2         12 my $client_frac_localtime = $frac2bin->($client_adj_localtime);
191              
192 2         27 my $ntp_msg =
193             pack( "B8 C3 N10 B32", '00011011', (0) x 12, int($client_localtime),
194             $client_frac_localtime );
195              
196 2         43 $socket->socket( $ai->{family}, $ai->{socktype}, $ai->{protocol} );
197 2 50       453 unless ( send( $socket, $ntp_msg, 0, $ai->{addr} ) == length($ntp_msg) ) {
198 0         0 $self->{error} = $!;
199 0         0 $kernel->yield('_dispatch');
200 0         0 return;
201             }
202 2         23 $kernel->select_read( $socket, '_get_datagram' );
203 2   100     235 $kernel->delay( '_timeout', ( $self->{timeout} || 60 ), $socket );
204 2         225 return;
205             }
206              
207             sub _timeout {
208 1     1   5005102 my ($kernel,$self,$socket) = @_[KERNEL,OBJECT,ARG0];
209 1         8 $kernel->select_read( $socket );
210 1         125 $self->{error} = 'Socket timeout';
211 1         5 $kernel->yield('_dispatch');
212 1         65 return;
213             }
214              
215             sub _get_datagram {
216 1     1   11273 my ($kernel,$self,$socket) = @_[KERNEL,OBJECT,ARG0];
217 1         5 $kernel->delay( '_timeout' );
218 1         116 $kernel->select_read( $socket );
219 1         135 my $remote_address = recv( $socket, my $data = '', 960, 0 );
220 1 50       4 unless ( defined $remote_address ) {
221 0         0 $self->{error} = $!;
222 0         0 $kernel->yield('_dispatch');
223 0         0 return;
224             }
225 1         2 my $client_localtime = $self->{client_localtime};
226 1         3 my $client_recvtime = time;
227 1         1 my %tmp_pkt;
228             my %packet;
229 1         3 my @ntp_fields = qw/byte1 stratum poll precision/;
230 1         4 push @ntp_fields, qw/delay delay_fb disp disp_fb ident/;
231 1         4 push @ntp_fields, qw/ref_time ref_time_fb/;
232 1         3 push @ntp_fields, qw/org_time org_time_fb/;
233 1         3 push @ntp_fields, qw/recv_time recv_time_fb/;
234 1         3 push @ntp_fields, qw/trans_time trans_time_fb/;
235              
236 1         28 @tmp_pkt{@ntp_fields} =
237             unpack( "a C3 n B16 n B16 H8 N B32 N B32 N B32 N B32", $data );
238              
239 1         7 $packet{hex_ref_time} = sprintf '%x.%x', $tmp_pkt{ref_time}, substr +( split m!\.!, $bin2frac->($tmp_pkt{ref_time_fb}) )[1], 0, 9;
240 1         5 $packet{hex_org_time} = sprintf '%x.%x', ( $tmp_pkt{org_time} + NTP_ADJ ), substr +( split m!\.!, $bin2frac->($tmp_pkt{org_time_fb}) )[1], 0, 9;
241 1         5 $packet{hex_trans_time} = sprintf '%x.%x', $tmp_pkt{trans_time}, substr +( split m!\.!, $bin2frac->($tmp_pkt{trans_time_fb}) )[1], 0, 9;
242              
243 1         12 @packet{@ntp_packet_fields} = (
244             (unpack( "C", $tmp_pkt{byte1} & "\xC0" ) >> 6),
245             (unpack( "C", $tmp_pkt{byte1} & "\x38" ) >> 3),
246             (unpack( "C", $tmp_pkt{byte1} & "\x07" )),
247             $tmp_pkt{stratum},
248             (sprintf("%0.4f", $tmp_pkt{poll})),
249             $tmp_pkt{precision} - 255,
250             ($bin2frac->($tmp_pkt{delay_fb})),
251             (sprintf("%0.4f", $tmp_pkt{disp})),
252             $unpack_ip->($tmp_pkt{stratum}, $tmp_pkt{ident}),
253             (($tmp_pkt{ref_time} += $bin2frac->($tmp_pkt{ref_time_fb})) -= NTP_ADJ),
254             (($tmp_pkt{org_time} += $bin2frac->($tmp_pkt{org_time_fb})) ),
255             (($tmp_pkt{recv_time} += $bin2frac->($tmp_pkt{recv_time_fb})) -= NTP_ADJ),
256             (($tmp_pkt{trans_time} += $bin2frac->($tmp_pkt{trans_time_fb})) -= NTP_ADJ)
257             );
258              
259 1         9 my $dest_org = sprintf "%0.5f", (($client_recvtime - $client_localtime));
260 1         4 my $recv_trans = sprintf "%0.5f", ($packet{'Receive Timestamp'} - $packet{'Transmit Timestamp'});
261 1         5 my $delay = sprintf "%0.5f", ($dest_org + $recv_trans);
262              
263 1         3 my $recv_org = $packet{'Receive Timestamp'} - $client_recvtime;
264 1         2 my $trans_dest = $packet{'Transmit Timestamp'} - $client_localtime;
265 1         2 my $offset = ($recv_org + $trans_dest) / 2;
266              
267             # Calculated offset / delay
268 1         1 $packet{Offset} = $offset;
269 1         2 $packet{Delay} = $delay;
270              
271 1         13 $self->{response} = \%packet;
272 1         5 $kernel->yield('_dispatch');
273 1         72 return;
274             }
275              
276             sub _dispatch {
277 2     2   323 my ($kernel,$self) = @_[KERNEL,OBJECT];
278 2         4 my $data = { };
279 2         5 $data->{$_} = $self->{$_} for grep { defined $self->{$_} } qw(response error context host);
  8         31  
280 2         14 $kernel->post( $self->{sender_id}, $self->{event}, $data );
281 2         196 $kernel->refcount_decrement( $self->{sender_id}, __PACKAGE__ );
282 2         71 return;
283             }
284              
285             }
286              
287             'What is the time, Mr Wolf?';
288              
289             __END__