File Coverage

blib/lib/Net/DNS/DynDNS.pm
Criterion Covered Total %
statement 186 249 74.7
branch 68 128 53.1
condition 35 108 32.4
subroutine 31 34 91.1
pod 4 14 28.5
total 324 533 60.7


line stmt bran cond sub pod time code
1             package Net::DNS::DynDNS;
2              
3 1     1   754 use warnings;
  1         2  
  1         42  
4 1     1   6 use strict;
  1         2  
  1         33  
5 1     1   1996 use LWP();
  1         95666  
  1         26  
6 1     1   1613 use HTTP::Cookies();
  1         9391  
  1         24  
7 1     1   7 use HTTP::Headers();
  1         2  
  1         14  
8 1     1   5 use Carp();
  1         2  
  1         19  
9 1     1   937 use English qw(-no_match_vars);
  1         5765  
  1         5  
10             our $VERSION = '0.9992';
11              
12             our @CARP_NOT = ('Net::DNS::DynDNS');
13 3     3 0 11 sub DEFAULT_TIMEOUT { return 60 }
14 1     1 0 5 sub NUMBER_OF_OCTETS_IN_IP_ADDRESS { return 4; }
15 4     4 0 23 sub MAXIMUM_VALUE_OF_AN_OCTET { return 256; }
16 1     1 0 8 sub FIRST_BYTE_OF_10_PRIVATE_RANGE { return 10; }
17 1     1 0 9 sub FIRST_BYTE_OF_172_16_PRIVATE_RANGE { return 172; }
18 0     0 0 0 sub SECOND_BYTE_OF_172_16_PRIVATE_RANGE { return 16; }
19 1     1 0 8 sub FIRST_BYTE_OF_192_168_PRIVATE_RANGE { return 192; }
20 0     0 0 0 sub SECOND_BYTE_OF_192_168_PRIVATE_RANGE { return 168; }
21 1     1 0 7 sub LOCALHOST_RANGE { return 127; }
22 1     1 0 7 sub MULTICAST_RESERVED_LOWEST_RANGE { return 224; }
23              
24             sub new {
25 3     3 1 1179 my ( $class, $user_name, $password, $params ) = @_;
26 3         11 my $self = {};
27 3         19 my $timeout = DEFAULT_TIMEOUT();
28 3 50 33     112 if ( ( ref $user_name ) && ( ref $user_name eq 'SCALAR' ) ) {
    50 33        
29 0 0 0     0 if ( not( ( ref $password ) && ( ref $password eq 'SCALAR' ) ) ) {
30 0         0 Carp::croak('No password supplied');
31             }
32             }
33             elsif ( ( ref $user_name ) && ( ( ref $user_name ) eq 'HASH' ) ) {
34 0         0 $params = $user_name;
35 0         0 $user_name = undef;
36 0         0 $password = undef;
37             }
38 3 50       17 if ( exists $params->{timeout} ) {
39 0 0 0     0 if ( ( $params->{timeout} ) && ( $params->{timeout} =~ /^\d+$/xsm ) ) {
40 0         0 $timeout = $params->{timeout};
41             }
42             else {
43 0         0 Carp::croak(q[The 'timeout' parameter must be a number]);
44             }
45             }
46 3         18 my $name = "Net::DNS::DynDNS $VERSION "
47             ; # a space causes the default LWP User Agent to be appended.
48 3 50       15 if ( exists $params->{user_agent} ) {
49 0 0 0     0 if ( ( $params->{user_agent} ) && ( $params->{user_agent} =~ /\S/xsm ) )
50             {
51 0         0 $name = $params->{user_agent};
52             }
53             }
54 3         30 my $ua = LWP::UserAgent->new( timeout => $timeout )
55             ; # no sense in using keep_alive => 1 because updates and checks are supposed to happen infrequently
56 3         997 $ua->env_proxy();
57 3         18482 $ua->agent($name);
58 3         291 my $cookie_jar = HTTP::Cookies->new( hide_cookie2 => 1 );
59 3         104 $ua->cookie_jar($cookie_jar);
60 3         459 $ua->requests_redirectable( ['GET'] );
61 3         54 $self->{_ua} = $ua;
62 3         16 my $headers = HTTP::Headers->new();
63              
64 3 100 66     112 if ( ($user_name) && ($password) ) {
65 2         14 $headers->authorization_basic( $user_name, $password );
66             }
67 3         390 $self->{_headers} = $headers;
68 3   50     29 $self->{server} = $params->{server} || 'dyndns.org';
69 3   50     22 $self->{dns_server} = $params->{dns_server} || 'members.dyndns.org';
70 3   50     20 $self->{check_ip} = $params->{check_ip} || 'checkip.dyndns.org';
71 3         11 bless $self, $class;
72 3         18 $self->update_allowed(1);
73 3         20 return $self;
74             }
75              
76             sub _get {
77 3     3   34 my ( $self, $uri ) = @_;
78 3         8 my $ua = $self->{_ua};
79 3         8 my $headers = $self->{_headers};
80 3         30 my $request = HTTP::Request->new( 'GET' => $uri, $headers );
81 3         7073 my $response;
82             eval {
83             local $SIG{'ALRM'} =
84 3     0   53 sub { Carp::croak "Timeout when retrieving $uri"; };
  0         0  
85 3         18 alarm $ua->timeout();
86 3         59 $response = $ua->request($request);
87 3         31743982 alarm 0;
88 3         75 1;
89 3 50       7 } or do {
90 0         0 chomp $EVAL_ERROR;
91 0         0 Carp::croak "Failed to get a response from '$uri':$EVAL_ERROR";
92             };
93 3         20 return $response;
94             }
95              
96             sub default_ip_address {
97 1     1 1 450680 my ( $proto, $params ) = @_;
98 1         3 my ($self);
99 1 50       8 if ( ref $proto ) {
100 0         0 $self = $proto;
101             }
102             else {
103 1         7 $self = $proto->new($params);
104             }
105 1         5 my ($check_ip_uri) = $self->_check_ip_address_uri($params);
106              
107             # user_name / password is not necessary for checkip.
108             # therefore don't send user_name / password
109              
110 1         3 my $headers = $self->{_headers};
111 1         6 my ( $user_name, $password ) = $headers->authorization_basic();
112 1         7588 $headers->remove_header('Authorization');
113              
114 1         18 my ( $response, $network_error );
115 1 50       3 eval { $response = $self->_get($check_ip_uri); } or do {
  1         6  
116 0         0 $network_error = $EVAL_ERROR;
117             };
118              
119             # restore user_name / password
120              
121 1 50 33     9 if ( ($user_name) && ($password) ) {
122 0         0 $headers->authorization_basic( $user_name, $password );
123             }
124              
125 1 50       6 if ($network_error) {
126 0         0 chomp $network_error;
127 0         0 Carp::croak($network_error);
128             }
129 1         12 return $self->_parse_ip_address( $check_ip_uri, $response );
130             }
131              
132             sub _check_ip_address_uri {
133 1     1   3 my ( $self, $params ) = @_;
134 1         2 my $protocol = 'http'
135             ; # default protocol is http because no user_name / passwords are required
136 1 50       4 if ( exists $params->{protocol} ) {
137 0 0 0     0 if ( ( defined $params->{protocol} ) && ( $params->{protocol} ) ) {
138 0         0 $params->{protocol} = lc( $params->{protocol} );
139 0 0 0     0 if ( ( $params->{protocol} ne 'http' )
140             && ( $params->{protocol} ne 'https' ) )
141             {
142 0         0 Carp::croak(
143             q[The 'protocol' parameter must be one of 'http' or 'https']
144             );
145             }
146             }
147             else {
148 0         0 Carp::croak(
149             q[The 'protocol' parameter must be one of 'http' or 'https']);
150             }
151 0         0 $protocol = $params->{protocol};
152             }
153 1 50       5 if ( $protocol eq 'https' ) {
154 0 0       0 eval { require Net::HTTPS; } or do {
  0         0  
155 0         0 Carp::croak(q[Cannot load Net::HTTPS]);
156             };
157             }
158 1         5 return $protocol . '://' . $self->{check_ip};
159             }
160              
161             sub _parse_ip_address {
162 1     1   5 my ( $self, $check_ip_uri, $response ) = @_;
163 1         3 my $ip_address;
164 1 50       8 if ( $response->is_success() ) {
165 1         28 my $content = $response->content();
166 1 50       29 if ( $content =~ /Current\sIP\sAddress:\s(\d+.\d+.\d+.\d+)/xsm ) {
167 1         6 $ip_address = $1;
168             }
169             else {
170 0         0 Carp::croak("Failed to parse response from '$check_ip_uri'");
171             }
172             }
173             else {
174 0         0 my $content = $response->content();
175 0         0 $content =~ s/\s*$//smx;
176 0 0       0 if ( $content =~ /Can't\sconnect\sto\s$self->{check_ip}/xsm ) {
177 0         0 Carp::croak("Failed to connect to '$check_ip_uri'");
178             }
179             else {
180 0         0 Carp::croak(
181             "Failed to get a success type response from '$check_ip_uri':$content"
182             );
183             }
184             }
185 1         86 return $ip_address;
186             }
187              
188             sub _validate_update {
189 2     2   6 my ( $self, $hostnames, $ip_address, $params ) = @_;
190 2         7 my $headers = $self->{_headers};
191 2         10 my ( $user_name, $password ) = $headers->authorization_basic();
192 2 50       116 if ( not $self->update_allowed() ) {
193 0         0 Carp::croak(
194             "$self->{server} has forbidden updates until the previous error is corrected"
195             );
196             }
197 2 50 33     29 if ( not( ($user_name) && ($password) ) ) {
198 0         0 Carp::croak(q[Username and password must be supplied for an update]);
199             }
200 2 50       9 if ( not($hostnames) ) {
201 0         0 Carp::croak(q[The update method must be supplied with a hostname]);
202             }
203 2 50       27 if (
204             not( $hostnames =~
205             /^(?:(?:[[:alpha:]\d\-]+[.])+[[:alpha:]\d\-]+,?)+$/xsm )
206             )
207             {
208 0         0 Carp::croak(
209             "The hostnames do not seem to be in a valid format. Try 'test.$self->{server}'"
210             );
211             }
212 2         10 $self->_validate_ip_address($ip_address);
213 2 100 66     17 if ( ( ref $params ) && ( ( ref $params ) eq 'HASH' ) ) {
    50          
214 1         6 $self->_check_wildcard($params);
215 1         4 $self->_check_mx($params);
216 1         4 $self->_check_backmx($params);
217 1         4 $self->_check_offline($params);
218 1 50       4 if ( exists $params->{protocol} ) {
219 1         4 $self->_check_protocol($params);
220             }
221             else {
222 0         0 $params->{protocol} = 'https';
223             }
224             }
225             elsif ($params) {
226 0         0 Carp::croak(
227             q[Extra parameters must be passed in as a reference to a hash]);
228             }
229 2         7 return;
230             }
231              
232             sub _validate_ip_address {
233 2     2   5 my ( $self, $ip_address ) = @_;
234 2 100       9 if ( defined $ip_address ) {
235 1         6 my @bytes = split /[.]/xsm, $ip_address;
236 1 50       5 if ( ( scalar @bytes ) != NUMBER_OF_OCTETS_IN_IP_ADDRESS() ) {
237 0         0 Carp::croak(q[Bad IP address]);
238             }
239 1         3 foreach my $byte (@bytes) {
240 4 50       18 if ( not( $byte =~ /^\d+$/xsm ) ) {
241 0         0 Carp::croak(q[Bad IP address. Each byte must be numeric]);
242             }
243 4 50 33     8 if ( ( $byte >= MAXIMUM_VALUE_OF_AN_OCTET() ) || ( $byte < 0 ) ) {
244 0         0 Carp::croak(q[Bad IP address. Each byte must be within 0-255]);
245             }
246             }
247 1 50 33     8 if (
      33        
      33        
      33        
      33        
      33        
      33        
248             ( $bytes[0] == 0 )
249             || ( $bytes[0] == LOCALHOST_RANGE() )
250             || ( $bytes[0] == FIRST_BYTE_OF_10_PRIVATE_RANGE() )
251             || ( ( $bytes[0] == FIRST_BYTE_OF_172_16_PRIVATE_RANGE() )
252             && ( $bytes[1] == SECOND_BYTE_OF_172_16_PRIVATE_RANGE() ) )
253             || # private
254             (
255             ( $bytes[0] == FIRST_BYTE_OF_192_168_PRIVATE_RANGE() )
256             && ( $bytes[1] == SECOND_BYTE_OF_192_168_PRIVATE_RANGE() )
257             )
258             || # private
259             ( $bytes[0] >= MULTICAST_RESERVED_LOWEST_RANGE() )
260             ) # multicast && reserved
261             {
262 0         0 Carp::croak(
263             q[Bad IP address. The IP address is in a range that is not publically addressable]
264             );
265             }
266             }
267             }
268              
269             sub _check_wildcard {
270 1     1   2 my ( $self, $params ) = @_;
271 1 50       5 if ( exists $params->{wildcard} ) {
272 1 50 33     7 if ( ( defined $params->{wildcard} ) && ( $params->{wildcard} ) ) {
273 1         4 $params->{wildcard} = uc( $params->{wildcard} );
274 1 0 33     13 if ( ( $params->{wildcard} ne 'ON' )
      33        
275             && ( $params->{wildcard} ne 'OFF' )
276             && ( $params->{wildcard} ne 'NOCHG' ) )
277             {
278 0         0 Carp::croak(
279             q[The 'wildcard' parameter must be one of 'ON','OFF' or 'NOCHG']
280             );
281             }
282             }
283             else {
284 0         0 Carp::croak(
285             q[The 'wildcard' parameter must be one of 'ON','OFF' or 'NOCHG']
286             );
287             }
288             }
289             }
290              
291             sub _check_mx {
292 1     1   3 my ( $self, $params ) = @_;
293 1 50       4 if ( exists $params->{mx} ) {
294 1 50 33     8 if ( ( defined $params->{mx} ) && ( $params->{mx} ) ) {
295 1 50       10 if (
296             not( $params->{mx} =~
297             /^(?:(?:[[:alpha:]\d\-]+[.])+[[:alpha:]\d\-]+,?)+$/xsm )
298             )
299             {
300 0         0 Carp::croak(
301             "The 'mx' parameter does not seem to be in a valid format. Try 'test.$self->{server}'"
302             );
303             }
304             }
305             else {
306 0         0 Carp::croak(
307             q[The 'mx' parameter must be a valid fully qualified domain name]
308             );
309             }
310             }
311             else {
312 0 0       0 if ( exists $params->{backmx} ) {
313 0         0 Carp::croak(
314             q[The 'backmx' parameter cannot be set without specifying the 'mx' parameter]
315             );
316             }
317             }
318             }
319              
320             sub _check_backmx {
321 1     1   2 my ( $self, $params ) = @_;
322 1 50       5 if ( exists $params->{backmx} ) {
323 1 50 33     8 if ( ( defined $params->{backmx} ) && ( $params->{backmx} ) ) {
324 1         3 $params->{backmx} = uc( $params->{backmx} );
325 1 50 33     5 if ( ( $params->{backmx} ne 'YES' )
326             && ( $params->{backmx} ne 'NO' ) )
327             {
328 0         0 Carp::croak(
329             q[The 'backmx' parameter must be one of 'YES' or 'NO']);
330             }
331             }
332             else {
333 0         0 Carp::croak(q[The 'backmx' parameter must be one of 'YES' or 'NO']);
334             }
335             }
336             }
337              
338             sub _check_offline {
339 1     1   2 my ( $self, $params ) = @_;
340 1 50       4 if ( exists $params->{offline} ) {
341 1 50 33     7 if ( ( defined $params->{offline} ) && ( $params->{offline} ) ) {
342 1         3 $params->{offline} = uc( $params->{offline} );
343 1 50 33     42 if ( ( $params->{offline} ne 'YES' )
344             && ( $params->{offline} ne 'NO' ) )
345             {
346 0         0 Carp::croak(
347             q[The 'offline' parameter must be one of 'YES' or 'NO']);
348             }
349             }
350             else {
351 0         0 Carp::croak(
352             q[The 'offline' parameter must be one of 'YES' or 'NO']);
353             }
354             }
355             }
356              
357             sub _check_protocol {
358 1     1   3 my ( $self, $params ) = @_;
359 1 50 33     8 if ( ( defined $params->{protocol} ) && ( $params->{protocol} ) ) {
360 1         3 $params->{protocol} = lc( $params->{protocol} );
361 1 50 33     6 if ( ( $params->{protocol} ne 'http' )
362             && ( $params->{protocol} ne 'https' ) )
363             {
364 0         0 Carp::croak(
365             q[The 'protocol' parameter must be one of 'http' or 'https']);
366             }
367             }
368             else {
369 0         0 Carp::croak(
370             q[The 'protocol' parameter must be one of 'http' or 'https']);
371             }
372             }
373              
374             sub update_allowed {
375 6     6 1 18 my ( $self, $allowed ) = @_;
376 6         11 my $old;
377 6 50 66     37 if ( ( exists $self->{update_allowed} ) && ( $self->{update_allowed} ) ) {
378 3         8 $old = $self->{update_allowed};
379             }
380 6 100       23 if ( defined $allowed ) {
381 4         11 $self->{update_allowed} = $allowed;
382             }
383 6         20 return $old;
384             }
385              
386             sub _error {
387 1     1   4 my ( $self, $code, $content ) = @_;
388 1         6 $self->update_allowed(0);
389 1         13 my %errors = (
390             'badauth' => 'The username and password pair do not match a real user',
391             '!donator' =>
392             'An option available only to credited users (such as offline URL) was specified, but the user is not a credited user',
393             'notfqdn' =>
394             'The hostname specified is not a fully-qualified domain name (not in the form hostname.dyndns.org or domain.com)',
395             'nohost' =>
396             'The hostname specified does not exist in this user account',
397             'numhost' => 'Too many hosts (more than 20) specified in an update',
398             'abuse' => 'The hostname specified is blocked for update abuse',
399             'badagent' =>
400             'The user agent was not sent or HTTP method is not permitted',
401             'dnserr' => 'DNS error encountered',
402             '911' => 'There is a problem or scheduled maintenance on our side',
403             );
404 1   33     197 Carp::croak( $errors{$code} || "Unknown error:$code:$content" );
405             }
406              
407             sub update {
408 2     2 1 752 my ( $self, $hostnames, $ip_address, $params ) = @_;
409 2 50 33     12 if ( ( ref $ip_address ) && ( ref $ip_address eq 'HASH' ) ) {
410 0         0 $params = $ip_address;
411 0         0 $ip_address = undef;
412             }
413 2         10 $self->_validate_update( $hostnames, $ip_address, $params );
414 2         5 my $protocol =
415             'https'; # default protocol is https to protect user_name / password
416 2 100       9 if ( $params->{protocol} ) {
417 1         3 $protocol = $params->{protocol};
418             }
419 2 100       10 if ( $protocol eq 'https' ) {
420 1 50       4 eval { require Net::HTTPS; } or do {
  1         20  
421 0         0 Carp::croak(q[Cannot load Net::HTTPS]);
422             };
423             }
424 2         15 my $update_uri =
425             $protocol . "://$self->{dns_server}/nic/update?hostname=" . $hostnames;
426 2 100       8 if ( defined $ip_address ) {
427 1         5 $update_uri .= '&myip=' . $ip_address;
428             }
429 2 100       8 if ( exists $params->{wildcard} ) {
430 1         4 $update_uri .= '&wildcard=' . $params->{wildcard};
431             }
432 2 100       8 if ( exists $params->{mx} ) {
433 1         3 $update_uri .= '&mx=' . $params->{mx};
434             }
435 2 100       7 if ( exists $params->{backmx} ) {
436 1         3 $update_uri .= '&backmx=' . $params->{backmx};
437             }
438 2 100       7 if ( exists $params->{offline} ) {
439 1         3 $update_uri .= '&offline=' . $params->{offline};
440             }
441 2         9 my $response = $self->_get($update_uri);
442 2         12 my $content = $response->content();
443 2         32 my $result = $self->_parse_content( $update_uri, $content );
444 1         43 return $result;
445             }
446              
447             sub _parse_content {
448 2     2   6 my ( $self, $update_uri, $content ) = @_;
449 2         12 my @lines = split /\015?\012/xsm, $content;
450 2         5 my $result;
451 2         6 foreach my $line (@lines) {
452 3 100       25 if (
    50          
453             $line =~ m{
454             ( \S + ) # response code
455             \s+
456             (\S.*) $ # ip address (possible)
457             }xsm
458             )
459             {
460 2         46 my ( $code, $additional ) = ( $1, $2 );
461 2 50 33     14 if (
      33        
462             ( $code eq 'good' )
463             || ( $code eq 'nochg' )
464             || ( $code eq '200'
465             ) # used by http://www.changeip.com/accounts/knowledgebase.php?action=displayarticle&id=47
466             )
467             {
468 2 100       6 if ($result) {
469 1 50       6 if ( $result ne $additional ) {
470 0         0 Carp::croak(
471             "Could not understand multi-line response\n$content"
472             );
473             }
474             }
475             else {
476 1         3 $result = $additional;
477             }
478             }
479             else {
480 0         0 $self->_error( $code, $content );
481             }
482             }
483             elsif (
484             $line =~ m{
485             ^ ( \S + ) $ # if this line of the response is a single code word
486             }xsm
487             )
488             {
489 1         4 my ($code) = ($1);
490 1         6 $self->_error( $code, $content );
491             }
492             else {
493 0         0 Carp::croak(
494             "Failed to parse response from '$update_uri'\n$content");
495             }
496             }
497 1         4 return $result;
498             }
499              
500             1;
501              
502             __END__