File Coverage

blib/lib/POE/Component/Client/NTP.pm
Criterion Covered Total %
statement 101 114 88.6
branch 9 20 45.0
condition 4 8 50.0
subroutine 14 14 100.0
pod 1 1 100.0
total 129 157 82.1


line stmt bran cond sub pod time code
1             package POE::Component::Client::NTP;
2             $POE::Component::Client::NTP::VERSION = '0.12';
3             #ABSTRACT: A POE Component to query NTP servers
4              
5 2     2   264844 use strict;
  2         5  
  2         63  
6 2     2   9 use warnings;
  2         2  
  2         53  
7 2     2   7 use Carp;
  2         2  
  2         133  
8 2     2   9 use Socket qw[:all];
  2         3  
  2         2289  
9 2     2   1437 use IO::Socket::IP;
  2         9189  
  2         8  
10 2     2   1762 use POE;
  2         6  
  2         22  
11 2     2   664 use Time::HiRes qw[time];
  2         3  
  2         16  
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   613 use constant NTP_ADJ => 2208988800;
  2         4  
  2         2575  
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 504 my $package = shift;
131 2         11 my %opts = @_;
132 2         29 $opts{lc $_} = delete $opts{$_} for keys %opts;
133 2 50       9 croak "$package requires an 'event' argument\n"
134             unless $opts{event};
135 2         5 my $options = delete $opts{options};
136 2 50       7 $opts{host} = 'localhost' unless $opts{host};
137 2 50 33     12 $opts{port} = 123 unless $opts{port} and $opts{port} =~ /^\d+$/;
138 2         7 my $self = bless \%opts, $package;
139 2 50       32 $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         169 return $self;
147             }
148              
149             sub _start {
150 2     2   338 my ($kernel,$sender,$self) = @_[KERNEL,SENDER,OBJECT];
151 2         10 $self->{session_id} = $_[SESSION]->ID();
152 2 50 33     18 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         4 my $sender_id;
156 2 50       7 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         11 $kernel->refcount_increment( $sender_id, __PACKAGE__ );
168 2         58 $self->{sender_id} = $sender_id;
169 2         15 $kernel->detach_myself();
170 2         272 $kernel->yield('_socket');
171 2         169 return;
172             }
173              
174             sub _socket {
175 2     2   465 my ($kernel,$self) = @_[KERNEL,OBJECT];
176 2         22 my $socket = IO::Socket::IP->new( Proto => 'udp' );
177 2         1312 my $ai;
178             {
179 2         4 my %hints = (socktype => SOCK_DGRAM, protocol => IPPROTO_UDP);
  2         8  
180 2         18038 my ($err, @res) = getaddrinfo($self->{host}, $self->{port}, \%hints);
181 2 50       14 if ( $err ) {
182 0         0 $self->{error} = $err;
183 0         0 $kernel->yield('_dispatch');
184 0         0 return;
185             }
186 2         12 $ai = shift @res;
187             }
188 2         15 my $client_localtime = $self->{client_localtime} = time();
189 2         7 my $client_adj_localtime = $client_localtime + NTP_ADJ;
190 2         9 my $client_frac_localtime = $frac2bin->($client_adj_localtime);
191              
192 2         24 my $ntp_msg =
193             pack( "B8 C3 N10 B32", '00011011', (0) x 12, int($client_localtime),
194             $client_frac_localtime );
195              
196 2         23 $socket->socket( $ai->{family}, $ai->{socktype}, $ai->{protocol} );
197 2 50       396 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         24 $kernel->select_read( $socket, '_get_datagram' );
203 2   100     324 $kernel->delay( '_timeout', ( $self->{timeout} || 60 ), $socket );
204 2         245 return;
205             }
206              
207             sub _timeout {
208 1     1   5005124 my ($kernel,$self,$socket) = @_[KERNEL,OBJECT,ARG0];
209 1         8 $kernel->select_read( $socket );
210 1         179 $self->{error} = 'Socket timeout';
211 1         7 $kernel->yield('_dispatch');
212 1         123 return;
213             }
214              
215             sub _get_datagram {
216 1     1   4769 my ($kernel,$self,$socket) = @_[KERNEL,OBJECT,ARG0];
217 1         8 $kernel->delay( '_timeout' );
218 1         155 $kernel->select_read( $socket );
219 1         269 my $remote_address = recv( $socket, my $data = '', 960, 0 );
220 1 50       8 unless ( defined $remote_address ) {
221 0         0 $self->{error} = $!;
222 0         0 $kernel->yield('_dispatch');
223 0         0 return;
224             }
225 1         4 my $client_localtime = $self->{client_localtime};
226 1         6 my $client_recvtime = time;
227 1         94 my %tmp_pkt;
228             my %packet;
229 1         7 my @ntp_fields = qw/byte1 stratum poll precision/;
230 1         5 push @ntp_fields, qw/delay delay_fb disp disp_fb ident/;
231 1         3 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         11 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         25 @packet{@ntp_packet_fields} = (
240             (unpack( "C", $tmp_pkt{byte1} & "\xC0" ) >> 6),
241             (unpack( "C", $tmp_pkt{byte1} & "\x38" ) >> 3),
242             (unpack( "C", $tmp_pkt{byte1} & "\x07" )),
243             $tmp_pkt{stratum},
244             (sprintf("%0.4f", $tmp_pkt{poll})),
245             $tmp_pkt{precision} - 255,
246             ($bin2frac->($tmp_pkt{delay_fb})),
247             (sprintf("%0.4f", $tmp_pkt{disp})),
248             $unpack_ip->($tmp_pkt{stratum}, $tmp_pkt{ident}),
249             (($tmp_pkt{ref_time} += $bin2frac->($tmp_pkt{ref_time_fb})) -= NTP_ADJ),
250             (($tmp_pkt{org_time} += $bin2frac->($tmp_pkt{org_time_fb})) ),
251             (($tmp_pkt{recv_time} += $bin2frac->($tmp_pkt{recv_time_fb})) -= NTP_ADJ),
252             (($tmp_pkt{trans_time} += $bin2frac->($tmp_pkt{trans_time_fb})) -= NTP_ADJ)
253             );
254              
255 1         11 my $dest_org = sprintf "%0.5f", (($client_recvtime - $client_localtime));
256 1         5 my $recv_trans = sprintf "%0.5f", ($packet{'Receive Timestamp'} - $packet{'Transmit Timestamp'});
257 1         6 my $delay = sprintf "%0.5f", ($dest_org + $recv_trans);
258              
259 1         2 my $recv_org = $packet{'Receive Timestamp'} - $client_recvtime;
260 1         2 my $trans_dest = $packet{'Transmit Timestamp'} - $client_localtime;
261 1         3 my $offset = ($recv_org + $trans_dest) / 2;
262              
263             # Calculated offset / delay
264 1         2 $packet{Offset} = $offset;
265 1         1 $packet{Delay} = $delay;
266              
267 1         5 $self->{response} = \%packet;
268 1         6 $kernel->yield('_dispatch');
269 1         90 return;
270             }
271              
272             sub _dispatch {
273 2     2   546 my ($kernel,$self) = @_[KERNEL,OBJECT];
274 2         5 my $data = { };
275 2         7 $data->{$_} = $self->{$_} for grep { defined $self->{$_} } qw(response error context host);
  8         43  
276 2         14 $kernel->post( $self->{sender_id}, $self->{event}, $data );
277 2         216 $kernel->refcount_decrement( $self->{sender_id}, __PACKAGE__ );
278 2         84 return;
279             }
280              
281             }
282              
283             'What is the time, Mr Wolf?';
284              
285             __END__