File Coverage

blib/lib/Net/DNS/CloudFlare/DDNS.pm
Criterion Covered Total %
statement 24 86 27.9
branch 0 38 0.0
condition 0 3 0.0
subroutine 8 12 66.6
pod 2 2 100.0
total 34 141 24.1


line stmt bran cond sub pod time code
1             package Net::DNS::CloudFlare::DDNS;
2              
3 1     1   32700 use v5.10;
  1         4  
  1         71  
4 1     1   6 use strict;
  1         2  
  1         38  
5 1     1   6 use warnings FATAL => 'all';
  1         12  
  1         56  
6              
7 1     1   946 use Moo;
  1         18290  
  1         7  
8 1     1   2011 use Carp;
  1         2  
  1         74  
9 1     1   1148 use LWP::UserAgent;
  1         63324  
  1         36  
10 1     1   1014 use JSON::Any;
  1         20338  
  1         8  
11 1     1   7279 use Readonly;
  1         3205  
  1         1873  
12              
13             =head1 NAME
14              
15             Net::DNS::CloudFlare::DDNS - Object orientated Dynamic DNS interface
16             for CloudFlare
17              
18             =head1 VERSION
19              
20             Version 0.06
21              
22             =cut
23              
24             our $VERSION = '0.06';
25              
26              
27             =head1 SYNOPSIS
28              
29             Provides an object orientated interface that can be used to dynamically update
30             DNS records on CloudFlare.
31              
32             use Net::DNS::CloudFlare::DDNS;
33              
34             my $ddns = Net::DNS::CloudFlare::DDNS->new(
35             user => $cloudflare_user,
36             apikey => $cloudflare_api_key,
37             zones => $zones
38             );
39             my $ddns->update();
40             ...
41              
42             =head1 METHODS
43              
44             =head2 new
45              
46             Create a new Dynamic DNS updater
47              
48             my $ddns = Net::DNS::CloudFlare::DDNS->new(
49             # Required
50             user => $cloudflare_user,
51             apikey => $cloudflare_api_key,
52             zones => $dns_zones,
53             # Optional
54             verbose => $verbosity
55             );
56              
57             The zones argument must look like the following
58              
59             [
60             {
61             zone => $zone_name_1,
62             domains => [
63             $domain_1, ..., $domain_n
64             ]
65             },
66             ...
67             {
68             zone => $zone_name_n,
69             domains => [
70             $domain_1, ..., $domain_n
71             ]
72             }
73             ]
74              
75             Each domain must be an A record within that zone, use undef for the zone itself
76              
77             =head2 update
78              
79             Updates CloudFlare DNS with the current IP address if
80             necessary
81              
82             $ddns->update();
83              
84             =cut
85              
86             # General Cloudflare API details
87             Readonly my $CLOUDFLARE_URL =>
88             'https://www.cloudflare.com/api_json.html';
89             Readonly my %CLOUDFLARE_API_PARAMS => (
90             request => 'a',
91             zone => 'z',
92             user => 'email',
93             key => 'tkn',
94             domain => 'name',
95             id => 'id',
96             ip => 'content',
97             type => 'type',
98             ttl => 'ttl'
99             );
100              
101             # This request edits a record
102             Readonly my $CLOUDFLARE_REQUEST_EDIT => 'rec_edit';
103             Readonly my $RECORD_TYPE => 'A';
104             Readonly my $TTL => '1';
105              
106             sub update {
107 0     0 1   Readonly my $self => shift;
108              
109             # Get current IP address
110 0           Readonly my $ip => $self->_getIp;
111              
112             # Don't update unless necessary
113 0 0 0       return if defined $self->_ip && $self->_ip eq $ip;
114              
115 0 0         say 'Updating IPs' if $self->verbose;
116              
117             # By default we succeed
118 0           my $succ = 1;
119             # Try to update each zone
120 0           for my $zone (@{ $self->_zones }) {
  0            
121 0 0         say "Updating IPs for $zone->{zone}" if $self->verbose;
122              
123 0           for my $dom (@{ $zone->{domains} }) {
  0            
124 0           Readonly my $IP_UPDATE_ERROR =>
125             "IP update failed for $dom->{name} in $zone->{zone} at $CLOUDFLARE_URL: ";
126              
127 0 0         say "Updating IP for $dom->{name} in $zone->{zone}" if
128             $self->verbose;
129              
130             # Update IP
131 0           Readonly my $res => $self->_ua->post($CLOUDFLARE_URL, {
132             $CLOUDFLARE_API_PARAMS{request} =>
133             $CLOUDFLARE_REQUEST_EDIT,
134             $CLOUDFLARE_API_PARAMS{type} => $RECORD_TYPE,
135             $CLOUDFLARE_API_PARAMS{ttl} => $TTL,
136             $CLOUDFLARE_API_PARAMS{domain} => $dom->{name},
137             $CLOUDFLARE_API_PARAMS{zone} => $zone->{zone},
138             $CLOUDFLARE_API_PARAMS{id} => $dom->{id},
139             $CLOUDFLARE_API_PARAMS{user} => $self->_user,
140             $CLOUDFLARE_API_PARAMS{key} => $self->_key,
141             $CLOUDFLARE_API_PARAMS{ip} => $ip
142             });
143            
144 0 0         if($res->is_success) {
145 0           Readonly my $info =>
146             JSON::Any->jsonToObj($res->decoded_content);
147            
148             # API call failed
149 0 0         if($info->{result} eq 'error') {
150 0           carp $IP_UPDATE_ERROR, $info->{msg};
151 0           $succ = 0;
152 0           next;
153             }
154              
155 0 0         say "Updated IP for $dom->{name} in $zone->{zone} successfully"
156             if $self->verbose;
157 0           next;
158             }
159            
160             # HTTP request failed
161 0           carp $IP_UPDATE_ERROR, $res->status_line;
162             # Mark as failure
163 0           $succ = 0;
164             }
165             }
166              
167             # Update IP if all updates successful, retry next time otherwise
168 0 0         $self->_ip($succ ? $ip : undef);
169             }
170              
171             =head2 verbose
172              
173             Accessor for verbose attribute, set to print status information.
174              
175             # Verbosity on
176             $ddns->verbose(1);
177              
178             # Verbosity off
179             $ddns->verbose(undef);
180              
181             # Print current verbosity
182             say $ddns->verbose;
183              
184             =cut
185              
186             has 'verbose' => (
187             is => 'rw',
188             default => sub { undef },
189             );
190              
191             =head2 _ip
192              
193             Accessor for the IP attribute.
194              
195             # Set IP
196             $ddns->_ip($ip);
197            
198             # Get IP
199             my $up = $dds->_ip;
200              
201             =cut
202              
203             =head2 _getIP
204              
205             Trys to grab the current IP from a number of web services
206              
207             # Get current IP
208             my $ip = $ddns->_getIP;
209              
210             =cut
211              
212             # List of http services returning just an IP
213             Readonly my @IP_URLS => map { "http://$_" } (
214             'icanhazip.com',
215             'ifconfig.me/ip',
216             'curlmyip.com'
217             );
218              
219             sub _getIp {
220 0     0     Readonly my $self => shift;
221 0 0         say 'Trying to get current IP' if $self->verbose;
222              
223             # Try each service till we get an IP
224 0           for my $serviceUrl (@IP_URLS) {
225 0 0         say "Trying IP lookup at $serviceUrl" if $self->verbose;
226              
227 0           Readonly my $res => $self->_ua->get($serviceUrl);
228 0 0         if($res->is_success) {
229             # Chop off the newline
230 0           my $ip = $res->decoded_content;
231 0           chomp($ip);
232              
233 0 0         say "IP lookup at $serviceUrl returned $ip"
234             if $self->verbose;
235 0           return $ip;
236             }
237              
238             # log this lookup as failing
239 0           carp "IP lookup at $serviceUrl failed: ", $res->status_line;
240             }
241              
242             # All lookups have failed
243 0           croak 'Could not lookup IP'
244             }
245              
246             =head2 _getDomainIds
247              
248             Gets and builds a map of domains to IDs for a given zone
249              
250             # Get domain IDs
251             $ddns->_getDomainIds($zone);
252              
253             =cut
254              
255             # This request loads all information on domains in a zone
256             Readonly my $CLOUDFLARE_REQUEST_LOAD_ALL => 'rec_load_all';
257              
258             sub _getDomainIds {
259 0     0     Readonly my $self => shift;
260 0           Readonly my $zone => shift;
261 0           Readonly my $IDS_LOOKUP_ERROR =>
262             "Domain IDs lookup for $zone failed: ";
263              
264 0 0         say "Trying domain IDs lookup for $zone" if $self->verbose;
265              
266             # Query CloudFlare
267 0           Readonly my $res => $self->_ua->post($CLOUDFLARE_URL, {
268             $CLOUDFLARE_API_PARAMS{request} =>
269             $CLOUDFLARE_REQUEST_LOAD_ALL,
270             $CLOUDFLARE_API_PARAMS{zone} => $zone,
271             $CLOUDFLARE_API_PARAMS{key} => $self->_key,
272             $CLOUDFLARE_API_PARAMS{user} => $self->_user
273             });
274              
275 0 0         if($res->is_success) {
276 0           Readonly my $info =>
277             JSON::Any->jsonToObj($res->decoded_content);
278            
279             # Return data unless failure
280 0 0         unless($info->{result} eq 'error') {
281             # Get a hash of domain => id
282 0 0         my %ids = map {
283 0           $_->{type} eq 'A'
284             ? ( $_->{name} => $_->{rec_id} )
285             : ()
286 0           } @{ $info->{response}{recs}{objs} };
287              
288 0 0         say "Domain IDs lookup for $zone successful"
289             if $self->verbose;
290 0           return %ids;
291             }
292              
293             # API call failed
294 0           croak $IDS_LOOKUP_ERROR, $info->{msg};
295             }
296              
297             # HTTP request failed
298 0           croak $IDS_LOOKUP_ERROR, $res->status_line;
299             }
300              
301             =head2 BUILD
302              
303             Expands subdomains to full domains and attaches domain IDs
304              
305             =cut
306              
307             sub BUILD {
308 0     0 1   my $self = shift;
309              
310 0           for my $zone (@{ $self->_zones }) {
  0            
311 0           Readonly my $name => $zone->{zone};
312 0           Readonly my $domains => $zone->{domains};
313 0           Readonly my %ids => $self->_getDomainIds($name);
314              
315             # Decorate domains
316 0           foreach (0 .. $#$domains) {
317             # Expand subdomains to full domains
318 0 0         $domains->[$_] = defined $domains->[$_] ?
319             "$domains->[$_].$name" :
320             $name;
321              
322 0           my $dom = $domains->[$_];
323              
324             # Attach domain IDs
325 0 0         croak "No domain ID found for $dom in $name"
326             unless defined $ids{$dom};
327             # Replace with a hash
328 0           $domains->[$_] = {
329             name => $dom,
330             id => $ids{$dom}
331             };
332             }
333             }
334             }
335              
336             has '_ip' => (
337             is => 'rw',
338             default => sub { undef },
339             init_arg => undef,
340             );
341              
342             # Read only attributes
343              
344             # Cloudflare credentials
345             has '_user' => (
346             is => 'ro',
347             required => 1,
348             init_arg => 'user'
349             );
350             has '_key' => (
351             is => 'ro',
352             required => 1,
353             init_arg => 'apikey'
354             );
355              
356             # Cloudflare zones to update
357             has '_zones' => (
358             is => 'ro',
359             required => 1,
360             init_arg => 'zones'
361             );
362              
363             Readonly my $USER_AGENT => "DDFlare/$VERSION";
364             has '_ua' => (
365             is => 'ro',
366             default => sub {
367             Readonly my $ua => LWP::UserAgent->new;
368             $ua->agent($USER_AGENT);
369             $ua
370             },
371             init_arg => undef
372             );
373              
374             =head1 AUTHOR
375              
376             Peter Roberts, C<< >>
377              
378             =head1 BUGS
379              
380             Please report any bugs or feature requests to C, or through
381             the web interface at L.
382             I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.
383              
384             =head1 SUPPORT
385              
386             You can find documentation for this module with the perldoc command.
387              
388             perldoc Net::DNS::CloudFlare::DDNS
389              
390              
391             You can also look for information at:
392              
393             =over 4
394              
395             =item * DDFlare
396              
397             L
398              
399             =item * RT: CPAN's request tracker (report bugs here)
400              
401             L
402              
403             =item * AnnoCPAN: Annotated CPAN documentation
404              
405             L
406              
407             =item * CPAN Ratings
408              
409             L
410              
411             =item * Search CPAN
412              
413             L
414              
415             =back
416              
417             =head1 LICENSE AND COPYRIGHT
418              
419             Copyright 2013 Peter Roberts.
420              
421             This program is distributed under the MIT (X11) License:
422             L
423              
424             Permission is hereby granted, free of charge, to any person
425             obtaining a copy of this software and associated documentation
426             files (the "Software"), to deal in the Software without
427             restriction, including without limitation the rights to use,
428             copy, modify, merge, publish, distribute, sublicense, and/or sell
429             copies of the Software, and to permit persons to whom the
430             Software is furnished to do so, subject to the following
431             conditions:
432              
433             The above copyright notice and this permission notice shall be
434             included in all copies or substantial portions of the Software.
435              
436             THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
437             EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
438             OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
439             NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
440             HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
441             WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
442             FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
443             OTHER DEALINGS IN THE SOFTWARE.
444              
445              
446             =cut
447              
448             1; # End of Net::DNS::CloudFlare::DDNS