File Coverage

blib/lib/Net/DNS/DynDNS.pm
Criterion Covered Total %
statement 127 241 52.7
branch 33 120 27.5
condition 14 108 12.9
subroutine 19 34 55.8
pod 4 14 28.5
total 197 517 38.1


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