File Coverage

blib/lib/Net/DNS/DynDNS.pm
Criterion Covered Total %
statement 188 249 75.5
branch 71 128 55.4
condition 38 108 35.1
subroutine 31 34 91.1
pod 4 14 28.5
total 332 533 62.2


line stmt bran cond sub pod time code
1             package Net::DNS::DynDNS;
2              
3 1     1   1002 use warnings;
  1         2  
  1         47  
4 1     1   7 use strict;
  1         2  
  1         39  
5 1     1   2331 use LWP();
  1         106941  
  1         27  
6 1     1   1583 use HTTP::Cookies();
  1         11208  
  1         28  
7 1     1   8 use HTTP::Headers();
  1         1  
  1         19  
8 1     1   5 use Carp();
  1         2  
  1         21  
9 1     1   901 use English qw(-no_match_vars);
  1         17126  
  1         6  
10             our $VERSION = '0.9993';
11              
12             our @CARP_NOT = ('Net::DNS::DynDNS');
13 4     4 0 13 sub DEFAULT_TIMEOUT { return 60 }
14 2     2 0 9 sub NUMBER_OF_OCTETS_IN_IP_ADDRESS { return 4; }
15 8     8 0 58 sub MAXIMUM_VALUE_OF_AN_OCTET { return 256; }
16 2     2 0 18 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 2     2 0 17 sub LOCALHOST_RANGE { return 127; }
22 1     1 0 7 sub MULTICAST_RESERVED_LOWEST_RANGE { return 224; }
23              
24             sub new {
25 4     4 1 2280 my ( $class, $user_name, $password, $params ) = @_;
26 4         12 my $self = {};
27 4         21 my $timeout = DEFAULT_TIMEOUT();
28 4 50 33     50 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 4 50       21 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 4         21 my $name = "Net::DNS::DynDNS $VERSION "
47             ; # a space causes the default LWP User Agent to be appended.
48 4 50       19 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 4         41 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 4         1183 $ua->env_proxy();
57 4         20676 $ua->agent($name);
58 4         321 my $cookie_jar = HTTP::Cookies->new( hide_cookie2 => 1 );
59 4         124 $ua->cookie_jar($cookie_jar);
60 4         517 $ua->requests_redirectable( ['GET'] );
61 4         67 $self->{_ua} = $ua;
62 4         19 my $headers = HTTP::Headers->new();
63              
64 4 100 66     70 if ( ($user_name) && ($password) ) {
65 3         17 $headers->authorization_basic( $user_name, $password );
66             }
67 4         315 $self->{_headers} = $headers;
68 4   50     34 $self->{server} = $params->{server} || 'dyndns.org';
69 4   50     28 $self->{dns_server} = $params->{dns_server} || 'members.dyndns.org';
70 4   50     24 $self->{check_ip} = $params->{check_ip} || 'checkip.dyndns.org';
71 4         10 bless $self, $class;
72 4         18 $self->update_allowed(1);
73 4         22 return $self;
74             }
75              
76             sub _get {
77 5     5   32 my ( $self, $uri ) = @_;
78 5         12 my $ua = $self->{_ua};
79 5         11 my $headers = $self->{_headers};
80 5         38 my $request = HTTP::Request->new( 'GET' => $uri, $headers );
81 5         9331 my $response;
82             eval {
83             local $SIG{'ALRM'} =
84 5     0   89 sub { Carp::croak "Timeout when retrieving $uri"; };
  0         0  
85 5         29 alarm $ua->timeout();
86 5         101 $response = $ua->request($request);
87 5         4267834 alarm 0;
88 5         121 1;
89 5 50       13 } or do {
90 0         0 chomp $EVAL_ERROR;
91 0         0 Carp::croak "Failed to get a response from '$uri':$EVAL_ERROR";
92             };
93 5         32 return $response;
94             }
95              
96             sub default_ip_address {
97 1     1 1 346938 my ( $proto, $params ) = @_;
98 1         5 my ($self);
99 1 50       7 if ( ref $proto ) {
100 0         0 $self = $proto;
101             }
102             else {
103 1         8 $self = $proto->new($params);
104             }
105 1         6 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         5 my ( $user_name, $password ) = $headers->authorization_basic();
112 1         13749 $headers->remove_header('Authorization');
113              
114 1         15 my ( $response, $network_error );
115 1 50       3 eval { $response = $self->_get($check_ip_uri); } or do {
  1         5  
116 0         0 $network_error = $EVAL_ERROR;
117             };
118              
119             # restore user_name / password
120              
121 1 50 33     10 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   2 my ( $self, $params ) = @_;
134 1         3 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       4 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   4 my ( $self, $check_ip_uri, $response ) = @_;
163 1         4 my $ip_address;
164 1 50       8 if ( $response->is_success() ) {
165 1         27 my $content = $response->content();
166 1 50       28 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         89 return $ip_address;
186             }
187              
188             sub _validate_update {
189 6     6   14 my ( $self, $hostnames, $ip_address, $params ) = @_;
190 6         18 my $headers = $self->{_headers};
191 6         25 my ( $user_name, $password ) = $headers->authorization_basic();
192 6 100       299 if ( not $self->update_allowed() ) {
193 1         102 Carp::croak(
194             "$self->{server} has forbidden updates until the previous error is corrected"
195             );
196             }
197 5 50 33     48 if ( not( ($user_name) && ($password) ) ) {
198 0         0 Carp::croak(q[Username and password must be supplied for an update]);
199             }
200 5 50       18 if ( not($hostnames) ) {
201 0         0 Carp::croak(q[The update method must be supplied with a hostname]);
202             }
203 5 50       49 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 5         19 $self->_validate_ip_address($ip_address);
213 4 100 66     24 if ( ( ref $params ) && ( ( ref $params ) eq 'HASH' ) ) {
    50          
214 1         6 $self->_check_wildcard($params);
215 1         6 $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 4         9 return;
230             }
231              
232             sub _validate_ip_address {
233 5     5   11 my ( $self, $ip_address ) = @_;
234 5 100       19 if ( defined $ip_address ) {
235 2         10 my @bytes = split /[.]/xsm, $ip_address;
236 2 50       9 if ( ( scalar @bytes ) != NUMBER_OF_OCTETS_IN_IP_ADDRESS() ) {
237 0         0 Carp::croak(q[Bad IP address]);
238             }
239 2         8 foreach my $byte (@bytes) {
240 8 50       39 if ( not( $byte =~ /^\d+$/xsm ) ) {
241 0         0 Carp::croak(q[Bad IP address. Each byte must be numeric]);
242             }
243 8 50 33     20 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 2 100 33     14 if (
      66        
      33        
      66        
      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 1         266 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   3 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     18 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   2 my ( $self, $params ) = @_;
293 1 50       4 if ( exists $params->{mx} ) {
294 1 50 33     9 if ( ( defined $params->{mx} ) && ( $params->{mx} ) ) {
295 1 50       9 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     7 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       5 if ( exists $params->{offline} ) {
341 1 50 33     8 if ( ( defined $params->{offline} ) && ( $params->{offline} ) ) {
342 1         2 $params->{offline} = uc( $params->{offline} );
343 1 50 33     9 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   2 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     7 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 13     13 1 1230 my ( $self, $allowed ) = @_;
376 13         19 my $old;
377 13 100 66     65 if ( ( exists $self->{update_allowed} ) && ( $self->{update_allowed} ) ) {
378 7         17 $old = $self->{update_allowed};
379             }
380 13 100       37 if ( defined $allowed ) {
381 7         17 $self->{update_allowed} = $allowed;
382             }
383 13         42 return $old;
384             }
385              
386             sub _error {
387 1     1   3 my ( $self, $code, $content ) = @_;
388 1         5 $self->update_allowed(0);
389 1         11 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     193 Carp::croak( $errors{$code} || "Unknown error:$code:$content" );
405             }
406              
407             sub update {
408 6     6 1 1707 my ( $self, $hostnames, $ip_address, $params ) = @_;
409 6 50 33     29 if ( ( ref $ip_address ) && ( ref $ip_address eq 'HASH' ) ) {
410 0         0 $params = $ip_address;
411 0         0 $ip_address = undef;
412             }
413 6         25 $self->_validate_update( $hostnames, $ip_address, $params );
414 4         8 my $protocol =
415             'https'; # default protocol is https to protect user_name / password
416 4 100       15 if ( $params->{protocol} ) {
417 1         3 $protocol = $params->{protocol};
418             }
419 4 100       12 if ( $protocol eq 'https' ) {
420 3 50       5 eval { require Net::HTTPS; } or do {
  3         42  
421 0         0 Carp::croak(q[Cannot load Net::HTTPS]);
422             };
423             }
424 4         20 my $update_uri =
425             $protocol . "://$self->{dns_server}/nic/update?hostname=" . $hostnames;
426 4 100       12 if ( defined $ip_address ) {
427 1         4 $update_uri .= '&myip=' . $ip_address;
428             }
429 4 100       14 if ( exists $params->{wildcard} ) {
430 1         3 $update_uri .= '&wildcard=' . $params->{wildcard};
431             }
432 4 100       13 if ( exists $params->{mx} ) {
433 1         3 $update_uri .= '&mx=' . $params->{mx};
434             }
435 4 100       12 if ( exists $params->{backmx} ) {
436 1         3 $update_uri .= '&backmx=' . $params->{backmx};
437             }
438 4 100       14 if ( exists $params->{offline} ) {
439 1         2 $update_uri .= '&offline=' . $params->{offline};
440             }
441 4         13 my $response = $self->_get($update_uri);
442 4         25 my $content = $response->content();
443 4         67 my $result = $self->_parse_content( $update_uri, $content );
444 3         84 return $result;
445             }
446              
447             sub _parse_content {
448 4     4   12 my ( $self, $update_uri, $content ) = @_;
449 4         23 my @lines = split /\015?\012/xsm, $content;
450 4         9 my $result;
451 4         13 foreach my $line (@lines) {
452 5 100       53 if (
    50          
453             $line =~ m{
454             ( \S + ) # response code
455             \s+
456             (\S.*) $ # ip address (possible)
457             }xsm
458             )
459             {
460 4         18 my ( $code, $additional ) = ( $1, $2 );
461 4 50 66     38 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 4 100       14 if ($result) {
469 1 50       7 if ( $result ne $additional ) {
470 0         0 Carp::croak(
471             "Could not understand multi-line response\n$content"
472             );
473             }
474             }
475             else {
476 3         11 $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         3 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 3         13 return $result;
498             }
499              
500             1;
501              
502             __END__