File Coverage

blib/lib/POE/Component/Client/NTP.pm
Criterion Covered Total %
statement 88 101 87.1
branch 9 20 45.0
condition 4 8 50.0
subroutine 13 13 100.0
pod 1 1 100.0
total 115 143 80.4


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