File Coverage

blib/lib/DNS/WorldWideDns.pm
Criterion Covered Total %
statement 43 140 30.7
branch 7 72 9.7
condition 1 17 5.8
subroutine 11 15 73.3
pod 7 7 100.0
total 69 251 27.4


line stmt bran cond sub pod time code
1             package DNS::WorldWideDns;
2              
3             BEGIN {
4 1     1   43015 use vars qw($VERSION);
  1         3  
  1         46  
5 1     1   16 $VERSION = '0.0102';
6             }
7              
8              
9 1     1   5 use strict;
  1         3  
  1         32  
10 1     1   1008 use Class::InsideOut qw(readonly private id register);
  1         8448  
  1         8  
11             use Exception::Class (
12 1         16 'Exception' => {
13             description => 'A general error.',
14             },
15              
16             'MissingParam' => {
17             isa => 'Exception',
18             description => 'Expected a parameter that was not specified.',
19             },
20              
21             'InvalidParam' => {
22             isa => 'Exception',
23             description => 'A parameter passed in did not match what it was supposed to be.',
24             fields => [qw(got)],
25             },
26              
27             'InvalidAccount' => {
28             isa => 'RequestError',
29             description => 'Authentication failed.',
30             },
31              
32             'RequestError' => {
33             isa => 'Exception',
34             description => 'Something bad happened during the request.',
35             fields => [qw(url response code)],
36             },
37              
38 1     1   162 );
  1         2  
39 1     1   3633 use HTTP::Request;
  1         24898  
  1         37  
40 1     1   845 use HTTP::Request::Common qw(POST);
  1         2271  
  1         80  
41 1     1   1108 use LWP::UserAgent;
  1         26727  
  1         6953  
42              
43             readonly username => my %username;
44             readonly password => my %password;
45              
46              
47              
48             =head1 NAME
49              
50             DNS::WorldWideDns - An interface to the worldwidedns.net service.
51              
52             =head1 SYNOPSIS
53              
54             use DNS::WorldWideDns;
55            
56             $dns = DNS::WorldWideDns->new($user, $pass);
57            
58             $hashRef = $dns->getDomains;
59             $hashRef = $dns->getDomain($domain);
60            
61             $dns->addDomain($domain);
62             $dns->updateDomain($domain, $properties);
63             $dns->deleteDomain($domain);
64              
65             =head1 DESCRIPTION
66              
67             This module allows you to dynamically create, remove, update, delete, and report on domains hosted at L. It makes working with their sometimes obtuse, but very useful, DNS API protocol (L) a breeze.
68              
69             =head1 USAGE
70              
71             The following methods are available from this class:
72              
73             =cut
74              
75              
76             ###############################################################
77              
78             =head2 addDomain ( domain, [ isPrimary, isDynamic ] )
79              
80             Adds a domain to your account. Throws MissingParam, InvalidParam, InvalidAccount and RequestError.
81              
82             B You should use updateDomain() directly after adding a domain to give it some properties and records.
83              
84             Returns a 1 on success.
85              
86             =head3 domain
87              
88             A domain to add.
89              
90             =head3 isPrimary
91              
92             A boolean indicating if this is a primary domain, or a secondary. Defaults to 1.
93              
94             B This module currently only supports primary domains.
95              
96             =head3 isDynamic
97              
98             A boolean indicating whether this domain is to allow Dynamic DNS ip updating. Defaults to 0.
99              
100             =cut
101              
102             sub addDomain {
103 0     0 1 0 my ($self, $domain, $isPrimary, $isDynamic) = @_;
104 0 0       0 unless (defined $domain) {
105 0         0 MissingParam->throw(error=>'Need a domain.');
106             }
107 0 0       0 unless ($domain =~ m{^[\w\-\.]+$}xms) {
108 0         0 InvalidParam->throw(error=>'Domain is improperly formatted.', got=>$domain);
109             }
110 0 0 0     0 my $primary = ($isPrimary eq "" || $isPrimary == 1) ? 0 : 1;
111 0 0 0     0 my $dynamic = ($isDynamic eq "" || $isDynamic == 0) ? 1 : 2;
112 0         0 my $url = 'https://www.worldwidedns.net/api_dns_new_domain.asp?NAME='.$self->username.'&PASSWORD='.$self->password.'&DOMAIN='.$domain.'&DYN='.$dynamic.'&TYPE='.$primary;
113 0         0 my $response = $self->makeRequest($url);
114 0         0 my $content = $response->content;
115 0         0 chomp $content;
116 0 0       0 if ($content eq "200") {
    0          
    0          
    0          
    0          
117 0         0 return 1;
118             }
119             elsif ($content eq "407") {
120 0         0 RequestError->throw(
121             error => 'Account domain limit exceeded.',
122             url => $url,
123             code => $content,
124             response => $response,
125             );
126             }
127             elsif ($content eq "408") {
128 0         0 RequestError->throw(
129             error => 'Domain already exists.',
130             url => $url,
131             code => $content,
132             response => $response,
133             );
134             }
135             elsif ($content eq "409") {
136 0         0 RequestError->throw(
137             error => 'Domain banned by DNSBL.',
138             url => $url,
139             code => $content,
140             response => $response,
141             );
142             }
143             elsif ($content eq "410") {
144 0         0 RequestError->throw(
145             error => 'Invalid domain name.',
146             url => $url,
147             code => $content,
148             response => $response,
149             );
150             }
151             RequestError->throw(
152 0         0 error => 'Got back an invalid response.',
153             url => $url,
154             response => $response,
155             );
156             }
157              
158              
159             ###############################################################
160              
161             =head2 deleteDomain ( domain )
162              
163             Removes a domain from your account. Throws MissingParam, InvalidParam, InvalidAccount and RequestError.
164              
165             Returns a 1 on success.
166              
167             =head3 domain
168              
169             A domain to delete.
170              
171             =cut
172              
173             sub deleteDomain {
174 0     0 1 0 my ($self, $domain) = @_;
175 0 0       0 unless (defined $domain) {
176 0         0 MissingParam->throw(error=>'Need a domain.');
177             }
178 0 0       0 unless ($domain =~ m{^[\w\-\.]+$}xms) {
179 0         0 InvalidParam->throw(error=>'Domain is improperly formatted.', got=>$domain);
180             }
181 0         0 my $url = 'https://www.worldwidedns.net/api_dns_delete_domain.asp?NAME='.$self->username.'&PASSWORD='.$self->password.'&DOMAIN='.$domain;
182 0         0 my $response = $self->makeRequest($url);
183 0         0 my $content = $response->content;
184 0         0 chomp $content;
185 0 0       0 if ($content eq "200") {
    0          
    0          
186 0         0 return 1;
187             }
188             elsif ($content eq "405") {
189 0         0 RequestError->throw(
190             error => 'Domain not in account.',
191             url => $url,
192             code => $content,
193             response => $response,
194             );
195             }
196             elsif ($content eq "406") {
197 0         0 RequestError->throw(
198             error => 'Could not remove domain. Try again.',
199             url => $url,
200             code => $content,
201             response => $response,
202             );
203             }
204             RequestError->throw(
205 0         0 error => 'Got back an invalid response.',
206             url => $url,
207             response => $response,
208             );
209             }
210              
211              
212             ###############################################################
213              
214             =head2 getDomain ( domain, [ nameServer ] )
215              
216             Retrieves the zone information about the domain. Throws MissingParam, InvalidParam, InvalidAccount and RequestError.
217              
218             Returns a hash reference structure that looks like this:
219              
220             {
221             hostmaster => "you.example.com",
222             refresh => "86400",
223             retry => "1200",
224             expire => "186400",
225             ttl => "3600",
226             secureTransfer => "*",
227             records => []
228             }
229              
230             The hostmaster field is the email address of the person in charge of this domain. Note that it should use dot notation, so don't use an at (@) sign.
231              
232             The refresh field tells a cache name server how often (in seconds) to request fresh data from the authoratative name server. Minimum 3600.
233              
234             The retry field tells a cache name server how long to wait (in seconds) before attempting to retry a failed refresh. Minimum 3600.
235              
236             The expire field tells a cache name server how old (in seconds) to let data become before discarding it. Minimum 3600.
237              
238             The ttl (Time To Live) is the default value for records that don't have a TTL specified.
239              
240             The secureTransfer parameter is an access control list for zone transfers. Asterisk (*) implies that anyone can do zone transfers. Otherwise it could be a list of IP addresses separated by spaces. Setting it to an empty string means no servers may do zone transfers on the domain.
241              
242             The records field is an array reference of records attached to this domain. It looks something like this:
243              
244             [
245             {
246             name => "smtp",
247             ttl => 3600,
248             type => "A",
249             data => "1.1.1.1"
250             },
251             {
252             name => "@",
253             ttl => 3600,
254             type => "MX",
255             data => "10 smtp.example.com"
256             },
257             ]
258              
259             The name field is the subdomain or host name that will be prepended on to the domain. For example the "www" in "www.example.com". The at (@) symbol means the domain itself, which is why you can type google.com not just www.google.com. The asterisk (*) is a wildcard, which means if no matching records are found, use this record to service the request.
260              
261             The ttl field tells a caching name server how long (in seconds) it may use this record before it has to fetch new information about it. Minimum 3600.
262              
263             The type field is the domain record type defined in RFC1035. Common record types are A, CNAME, an MX.
264              
265             The data field holds the data of the record. It's usually an ip address or a fully qualified host name.
266              
267              
268             =head3 domain
269              
270             A domain to request information about.
271              
272             =head3 nameServer
273              
274             Defaults to 1. Choose from 1, 2, or 3. The number of the primary, secondary or tertiary name server.
275              
276             =cut
277              
278             sub getDomain {
279 0     0 1 0 my ($self, $domain, $nameServer) = @_;
280 0 0       0 unless (defined $domain) {
281 0         0 MissingParam->throw(error=>'Need a domain.');
282             }
283 0 0       0 unless ($domain =~ m{^[\w\-\.]+$}xms) {
284 0         0 InvalidParam->throw(error=>'Domain is improperly formatted.', got=>$domain);
285             }
286 0   0     0 $nameServer ||= 1;
287 0 0 0     0 if ($nameServer =~ m/^\D+$/ || $nameServer > 3 || $nameServer < 0) {
      0        
288 0         0 InvalidParam->throw(error=>'Name server must be a number between 1 and 3.', got=>$nameServer);
289             }
290 0         0 my $url = 'https://www.worldwidedns.net/api_dns_viewzone.asp?NAME='.$self->username.'&PASSWORD='.$self->password.'&DOMAIN='.$domain.'&NS='.$nameServer;
291 0         0 my $response = $self->makeRequest($url);
292 0         0 my $content = $response->content;
293 0         0 chomp $content;
294 0 0       0 if ($content eq "405") {
    0          
    0          
295 0         0 RequestError->throw(
296             error => 'Domain name could not be found.',
297             url => $url,
298             code => 405,
299             response => $response,
300             );
301             }
302             elsif ($content eq "450") {
303 0         0 RequestError->throw(
304             error => 'Could not reach the name server.',
305             url => $url,
306             code => 450,
307             response => $response,
308             );
309             }
310             elsif ($content eq "451") {
311 0         0 RequestError->throw(
312             error => 'No zone file for this domain on this name server.',
313             url => $url,
314             code => 451,
315             response => $response,
316             );
317             }
318 0         0 my %domain;
319            
320             # secure zone transfer
321 0 0       0 if ($content =~ m{^;\s+SecureZT((?:\s?\d+\.\d+\.\d+\.\d+){0,})$}xmsi) {
322 0         0 $domain{secureTransfer} = $1;
323             }
324             else {
325 0         0 $domain{secureTransfer} = '*';
326             }
327              
328             # hostmaster, refresh, retry, expires, ttl
329 0 0       0 if ($content =~ m{^\@\s+IN\s+SOA\s+[\w\.\-]+\.\s+([\w\.\-]+)\.\s+\d+\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s*$}xmsi) {
330 0         0 $domain{hostmaster} = $1;
331 0         0 $domain{refresh} = $2;
332 0         0 $domain{retry} = $3;
333 0         0 $domain{expire} = $4;
334 0         0 $domain{ttl} = $5;
335             }
336            
337             # records
338 0         0 while ($content =~ m{^(\@|\*|[\w\.\-]+)?\s+(\d+)?\s*(?:IN)?\s+(A|A6|AAAA|AFSDB|CNAME|DNAME|HINFO|ISDN|MB|MG|MINFO|MR|MX|NS|NSAP|PTR|RP|RT|SRV|TXT|X25)\s+(.*?)\s*$}xmsig) {
339 0         0 push @{$domain{records}}, {
  0         0  
340             name => $1,
341             ttl => $2,
342             type => $3,
343             data => $4,
344             };
345             }
346            
347 0         0 return \%domain;
348             }
349              
350              
351             ###############################################################
352              
353             =head2 getDomains ( )
354              
355             Returns a hash reference where the key is the domain and the value is either a 'Primary' or an 'Secondary'. Throws InvalidAccount and RequestError.
356              
357             B This module does not currently handle creating, reading, or updating secondary domains, but it may in the future, so this indicator is left in.
358              
359             =cut
360              
361             sub getDomains {
362 1     1 1 769 my $self = shift;
363 1         5 my $url = 'https://www.worldwidedns.net/api_dns_list.asp?NAME='.$self->username.'&PASSWORD='.$self->password;
364 1         19 my $content = $self->makeRequest($url)->content;
365 0         0 my %domains;
366 0         0 while ($content =~ m{([\w+\.\-]+)\x1F(P|S)}xmsig) {
367 0 0       0 my $type = ($2 eq 'P') ? 'Primary' : 'Secondary';
368 0         0 $domains{$1} = $type;
369             }
370 0         0 return \%domains;
371             }
372              
373              
374             ###############################################################
375              
376             =head2 makeRequest ( url, [ request ] )
377              
378             Makes a GET request. Returns the HTTP::Response from the request. Throws MissingParam, InvalidParam, InvalidAccount and RequestError.
379              
380             B Normally you never need to use this method, it's used by the other methods in this class. However, it may be useful in subclassing this module.
381              
382             =head3 url
383              
384             The URL to request.
385              
386             =head3 request
387              
388             Normally an HTTP::Request object is created for you on the fly. But if you want to make your own and pass it in you are welcome to do so.
389              
390             =cut
391              
392             sub makeRequest {
393 1     1 1 4 my ($self, $url, $request) = @_;
394 1 50       5 unless (defined $url) {
395 0         0 MissingParam->throw(error=>'Need a url.');
396             }
397 1 50       9 unless ($url =~ m{^https://www.worldwidedns.net/.*$}xms) {
398 0         0 InvalidParam->throw(error=>'URL is improperly formatted.', got=>$url);
399             }
400 1   33     14 $request ||= HTTP::Request->new(GET => $url);
401 1         11356 my $ua = LWP::UserAgent->new;
402 1         119804 my $response = $ua->request($request);
403            
404             # request is good
405 1 50       1857 if ($response->is_success) {
406 0         0 my $content = $response->content;
407 0         0 chomp $content;
408            
409             # is our account still active
410 0 0       0 if ($content eq "401") {
    0          
411 0         0 InvalidAccount->throw(
412             error => 'Login suspended.',
413             url => $url,
414             code => 401,
415             response => $response,
416             );
417             }
418            
419             # is our user/pass good
420             elsif ($content eq "403") {
421 0         0 InvalidAccount->throw(
422             error => 'Invalid user/pass combination.',
423             url => $url,
424             code => 403,
425             response => $response,
426             );
427             }
428            
429             # we're good, let's get back to work
430 0         0 return $response;
431             }
432            
433             # the request went totally off the reservation
434             RequestError->throw(
435 1         18 error => $response->message,
436             url => $url,
437             response => $response,
438             );
439              
440             }
441              
442             ###############################################################
443              
444             =head2 new ( username, password )
445              
446             Constructor. Throws MissingParam.
447              
448             =head3 username
449              
450             Your worldwidedns.net username.
451              
452             =head3 password
453              
454             The password to go with username.
455              
456             =cut
457              
458             sub new {
459 3     3 1 2903 my ($class, $username, $password) = @_;
460              
461             # validate
462 3 100       9 unless (defined $username) {
463 1         5 MissingParam->throw(error=>'Need a username.');
464             }
465 2 100       7 unless (defined $password) {
466 1         19 MissingParam->throw(error=>'Need a password.');
467             }
468              
469             # set up object
470 1         7 my $self = register($class);
471 1         25 my $refId = id $self;
472 1         4 $username{$refId} = $username;
473 1         2 $password{$refId} = $password;
474 1         4 return $self;
475             }
476              
477             ###############################################################
478              
479             =head2 password ()
480              
481             Returns the password set in the constructor.
482              
483             =cut
484              
485             ###############################################################
486              
487             =head2 updateDomain ( domain, params )
488              
489             Updates a domain in your account. Throws MissingParam, InvalidParam, InvalidAccount and RequestError.
490              
491             Returns a 1 on success.
492              
493             =head3 domain
494              
495             A domain to update.
496              
497             =head3 params
498              
499             A hash reference identical to the one returned by getDomain().
500              
501             =cut
502              
503             sub updateDomain {
504 0     0 1   my ($self, $domain, $params) = @_;
505            
506             # validate inputs
507 0 0         unless (defined $domain) {
508 0           MissingParam->throw(error=>'Need a domain.');
509             }
510 0 0         unless ($domain =~ m{^[\w\-\.]+$}xms) {
511 0           InvalidParam->throw(error=>'Domain is improperly formatted.', got=>$domain);
512             }
513 0 0         unless (defined $params) {
514 0           MissingParam->throw(error=>'Need parameters hash ref to set on the domain.');
515             }
516 0 0         unless (ref $params eq 'HASH') {
517 0           InvalidParam->throw(error=>'Expected a params hash reference.', got=>$params);
518             }
519              
520             # zone data
521 0           my $zoneData;
522 0           foreach my $record (@{$params->{records}}) {
  0            
523 0           $zoneData .= join(" ", $record->{name}, $record->{ttl}, 'IN', $record->{type}, $record->{data})."\r\n";
524             }
525              
526             # make request
527 0           my $url = 'https://www.worldwidedns.net/api_dns_modify_raw.asp';
528 0           my $request = POST $url, [
529             NAME => $self->username,
530             PASSWORD => $self->password,
531             DOMAIN => $domain,
532             HOSTMASTER => $params->{hostmaster},
533             REFRESH => $params->{refresh},
534             RETRY => $params->{retry},
535             SECURE => $params->{secureTransfer},
536             EXPIRE => $params->{expire},
537             TTL => $params->{ttl},
538             FOLDER => '',
539             ZONENS => 'ns1.worldwidedns.net',
540             ZONEDATA => $zoneData,
541             ];
542            
543 0           my $response = $self->makeRequest($url, $request);
544 0           my $content = $response->content;
545 0           chomp $content;
546            
547             # interpret results
548 0 0         if ($content =~ m{211\s*212\s*213}xmsi) {
    0          
549 0           return 1;
550             }
551             elsif ($content eq "405") {
552 0           RequestError->throw(
553             error => 'Domain not in account.',
554             url => $url,
555             code => $content,
556             response => $response,
557             );
558             }
559             RequestError->throw(
560 0           error => 'Updating one of the name servers failed.',
561             url => $url,
562             code => $content,
563             response => $response,
564             );
565             }
566              
567             ###############################################################
568              
569             =head2 username ()
570              
571             Returns the username set in the constructor.
572              
573             =cut
574              
575              
576             =head1 EXCEPTIONS
577              
578             This module uses L for exception handling. Each method is capable of throwing one or more of the following exceptions:
579              
580             =head2 Exception
581            
582             A general undefined error.
583              
584             =head2 MissingParam
585              
586             An expected parameter to a method was not passed.
587              
588             =head2 InvalidParam
589              
590             A parameter passed in doesn't match what was expected. This add a "got" field to the exception which contains what was received.
591              
592             =head2 InvalidAccount
593              
594             Authentication to worldwidedns.net failed.
595              
596             =head2 RequestError
597              
598             Some part of the request/response to worldwidedns.net did not go as expected. This adds url, response, and code fields to the exception.
599              
600             The url field is the URL that was requested. This can be very helpful when debugging a problem.
601              
602             The response field is the L object that was returned from the request.
603              
604             The code field is the error code number or numbers that were returned by the worldwidedns.net API. More informationa about them can be found in the DNS API protocol documentation pages (L).
605              
606             =head1 BUGS
607              
608             None known.
609              
610             =head1 CAVEATS
611              
612             This module is not feature complete with the API worldwidedns.net provides. It does your basic CRUD and that's it. They have other methods this doesn't use, and they have a whole reseller API which this doesn't support. If you need those features, patches are welcome.
613              
614             =head1 AUTHOR
615              
616             JT Smith
617             CPAN ID: RIZEN
618             Plain Black Corporation
619             jt_at_plainblack_com
620             http://www.plainblack.com/
621              
622             =head1 COPYRIGHT
623              
624             This program is free software; you can redistribute
625             it and/or modify it under the same terms as Perl itself.
626              
627             The full text of the license can be found in the
628             LICENSE file included with this module.
629              
630             =cut
631              
632             1;
633             # The preceding line will help the module return a true value
634