File Coverage

blib/lib/DNS/Oterica/RecordMaker/TinyDNS.pm
Criterion Covered Total %
statement 6 108 5.5
branch 0 34 0.0
condition 0 58 0.0
subroutine 2 22 9.0
pod 6 13 46.1
total 14 235 5.9


line stmt bran cond sub pod time code
1 1     1   4 use strict;
  1         1  
  1         24  
2 1     1   5 use warnings;
  1         1  
  1         1502  
3             package DNS::Oterica::RecordMaker::TinyDNS;
4             # ABSTRACT: a tinydns recordmaker for DNSO.
5             $DNS::Oterica::RecordMaker::TinyDNS::VERSION = '0.304';
6             #pod =head1 DESCRIPTION
7             #pod
8             #pod This role provides logic for generating lines for the F<tinydns-data> program
9             #pod to consume.
10             #pod
11             #pod =cut
12              
13 0     0     sub _default_ttl { 1800 }
14              
15             sub _serial_number {
16 0   0 0     return($ENV{DNS_OTERICA_SN} || $^T)
17             }
18              
19             #pod =method comment
20             #pod
21             #pod my $line = $rec->comment("Hello, world!");
22             #pod
23             #pod This returns a line that is a one-line commment.
24             #pod
25             #pod =cut
26              
27             sub comment {
28 0     0 1   my ($self, $comment) = @_;
29              
30 0           return "# $comment\n";
31             }
32              
33             #pod =method location
34             #pod
35             #pod This returns a location line.
36             #pod
37             #pod =cut
38              
39             sub location {
40 0     0 1   my ($self, $location) = @_;
41              
42 0 0         return if $location->code eq '';
43              
44 0 0         Carp::confess("location codes must be two-character")
45             unless length $location->code == 2;
46              
47 0           my @prefixes = $location->_class_prefixes;
48 0           map { sprintf "%%%s:%s\n", $location->code, $_ } @prefixes;
  0            
49             }
50              
51             sub __ip_locode_pairs {
52 0     0     my ($self, $rec) = @_;
53              
54 0 0         Carp::confess('no node provided') unless $rec->{node};
55              
56             return
57 0           map {; [ $_->[0] => $_->[1]->code ] }
58 0           $rec->{node}->interfaces;
59             }
60              
61             sub _generic {
62 0     0     my ($self, $op, $rec) = @_;
63              
64 0           my @lines;
65 0           for my $if ($self->__ip_locode_pairs($rec)) {
66             push @lines, sprintf "%s%s:%s:%s:%s:%s\n",
67             $op,
68             $rec->{name},
69             $if->[0],
70 0   0       $rec->{ttl} || $self->_default_ttl,
71             $self->_serial_number,
72             $if->[1],
73             ;
74             }
75              
76 0           return @lines;
77             }
78              
79             #pod =method a_and_ptr
80             #pod
81             #pod Generate an C<=> line, the bread and butter A and PTR record pair for a
82             #pod hostname and IP.
83             #pod
84             #pod =cut
85              
86             # =fqdn:ip:ttl:timestamp:lo
87             sub a_and_ptr {
88 0     0 1   my ($self, $rec) = @_;
89              
90             return (
91 0           $self->_generic(q{+}, $rec),
92             $self->ptr($rec),
93             );
94             }
95              
96             #pod =method ptr
97             #pod
98             #pod Generate an C<^> line, for the reverse DNS of an IP address.
99             #pod
100             #pod =cut
101              
102             # ^fqdn:ip:ttl:timestamp:lo
103             # can't use __generic here because it wants to look at interfaces, and we want
104             # the reverse of that
105             sub ptr {
106 0     0 1   my ($self, $rec) = @_;
107              
108 0           my @lines;
109 0           for my $if ($self->__ip_locode_pairs($rec)) {
110 0           my $ip = $if->[0];
111 0           my @bytes = reverse split /\./, $ip;
112              
113 0 0 0       splice @bytes, 1, 1, '0-24', $bytes[1]
      0        
114             if $bytes[1] eq 237 && $bytes[2] eq 72 && $bytes[3] eq 208;
115              
116 0           my $extended_arpa = join '.', @bytes, 'in-addr', 'arpa';
117             push @lines, sprintf "^%s:%s:%s:%s:%s\n",
118             $extended_arpa,
119             $rec->{name},
120 0 0 0       $rec->{ttl} || $self->_default_ttl,
121             $self->_serial_number,
122             $if->[1] eq 'FB' ? '' : $if->[1];
123             }
124              
125 0           return @lines;
126             }
127              
128             # TODO find out why we generate Z and & records for our IPs and refactor this
129             # to not duplicate effort with &ptr and the like. problem is that &a calls &ptr
130             # so having the code there means it gets called for every time we generate a +
131             # record, totally not what we want. What we want is for this to be called once
132             # for every IP address, not every hostname.
133             sub soa_and_ns_for_ip {
134 0     0 0   my ($self, $rec) = @_;
135              
136 0           my @lines;
137 0           my $node = $rec->{node};
138 0           my $ns_f = $node->hub->ns_family;
139 0           my %ns = $node->hub->node_family($ns_f)->ns_nodes;
140 0           my $ns_1 = (keys %ns)[0];
141 0           my $addr = $node->hub->soa_rname;
142 0           my $ip = $rec->{ip};
143 0           my @bytes = reverse split /\./, $ip;
144 0           my $arpa = join '.', @bytes, 'in-addr', 'arpa';
145              
146 0           push @lines, sprintf "Z%s:%s:%s::::::%s:%s:%s\n",
147             $arpa,
148             $ns_1,
149             $addr,
150             $self->_default_ttl,
151             $self->_serial_number,
152             '',
153             ;
154              
155 0           for my $ns (keys %ns) {
156 0           push @lines, $self->domain({
157             domain => $arpa,
158             ip => $ip,
159             ns => $ns,
160             });
161             }
162 0           return @lines;
163             }
164              
165             # +fqdn:ip:ttl:timestamp:lo
166             sub a {
167 0     0 0   my ($self, $rec) = @_;
168 0           my @lines = $self->_generic(q{+}, $rec);
169              
170 0           return @lines;
171             }
172              
173             # @fqdn:ip:x:dist:ttl:timestamp:lo
174             sub mx {
175 0     0 0   my ($self, $rec) = @_;
176              
177 0           my @lines;
178              
179             my $mx_name = defined $rec->{mx} ? $rec->{mx}
180             : $rec->{node} ? $rec->{node}->fqdn
181 0 0         : Carp::confess('neither mx nor node given as mx for mx record');
    0          
182              
183 0           for my $if ($self->__ip_locode_pairs($rec)) {
184             push @lines, sprintf "@%s:%s:%s:%s:%s:%s:%s\n",
185             $rec->{name},
186             ($rec->{no_ip} ? '' : $if->[0]),
187             $mx_name,
188             $rec->{dist} || 10,
189 0 0 0       $rec->{ttl} || $self->_default_ttl,
      0        
190             $self->_serial_number,
191             $if->[1],
192             ;
193             }
194              
195 0           return @lines;
196             }
197              
198             # .fqdn:ip:x:ttl:timestamp:lo
199             # This doesn't handle nodes, because I don't want to deal with ip-less records,
200             # which would cause __generic to barf. This is just a hack for now.
201             # -- rjbs, 2008-12-15
202             sub domain {
203 0     0 0   my ($self, $rec) = @_;
204              
205 0           my @lines;
206              
207             push @lines, sprintf "&%s:%s:%s:%s:%s:%s\n",
208             $rec->{domain},
209             $rec->{ip} || '',
210             $rec->{ns},
211 0   0       $rec->{ttl} || $self->_default_ttl,
      0        
212             $self->_serial_number,
213             '',
214             ;
215              
216 0           return @lines;
217             }
218              
219             sub soa_and_ns {
220 0     0 0   my ($self, $rec) = @_;
221              
222 0           my @lines;
223              
224             push @lines, sprintf "Z%s:%s:%s::::::%s:%s:%s\n",
225             $rec->{domain},
226             $rec->{ns} || '',
227             $rec->{node}->hub->soa_rname,
228 0   0       $rec->{ttl} || $self->_default_ttl,
      0        
229             $self->_serial_number,
230             '',
231             ;
232              
233 0           return @lines;
234             }
235              
236              
237             # Cfqdn:p:ttl:timestamp:lo
238             sub cname {
239 0     0 0   my ($self, $rec) = @_;
240              
241 0           my @lines;
242              
243             push @lines, sprintf "C%s:%s:%s:%s:%s\n",
244             $rec->{cname},
245             $rec->{domain} || '',
246 0   0       $rec->{ttl} || $self->_default_ttl,
      0        
247             $self->_serial_number,
248             '',
249             ;
250              
251 0           return @lines;
252             }
253              
254             sub txt {
255 0     0 0   my ($self, $rec) = @_;
256 0           my @lines;
257              
258 0           my $name = $rec->{name};
259 0 0 0       $name = $rec->{node}->fqdn if ! $name && $rec->{node};
260              
261 0 0 0       Carp::confess("no record name or node given for txt record")
262             unless defined $name and length $name;
263              
264             # 'fqdn:s:ttl:timestamp:lo
265             push @lines, sprintf qq{'%s:%s:%s:%s:%s\n},
266             $name,
267             _colon_safe($rec->{text}),
268 0   0       $rec->{ttl} || $self->_default_ttl,
269             $self->_serial_number,
270             '',
271             ;
272              
273 0           return @lines;
274             }
275              
276             sub _colon_safe {
277 0     0     my $str = $_[0];
278 0           $str =~ s/([^A-Za-z0-9=])/sprintf '\\%03o', ord $1/ge;
  0            
279 0           $str;
280             }
281              
282             sub _escaped_octals {
283 0     0     join q{}, map {; sprintf '\\%03o', ord } split //, pack 'n', $_[0];
  0            
284             }
285              
286             sub _hostname_to_labels {
287 0     0     my @labels = split /\./, $_[0];
288 0           my $str = '';
289 0           $str .= sprintf('\\%03o', length) . $_ for @labels;
290 0           $str .= '\000';
291              
292 0           return $str;
293             }
294              
295             #pod =method srv
296             #pod
297             #pod @lines = $rec->srv({
298             #pod # We want to produce _finger._tcp.example.com for port 70
299             #pod domain => 'example.com',
300             #pod service => 'finger',
301             #pod protocol => 'tcp',
302             #pod target => 'f.example.com',
303             #pod port => 70,
304             #pod
305             #pod priority => 10,
306             #pod weight => 20,
307             #pod });
308             #pod
309             #pod This returns lines for SRV records following RFC 2782. It takes the following
310             #pod special arguments:
311             #pod
312             #pod domain - the domain offering service
313             #pod service - the well-known service name (http, imaps, finger)
314             #pod protocol - tcp or udp
315             #pod
316             #pod target - the host providing service
317             #pod port - the port the service listens on
318             #pod
319             #pod priority - numeric priority; lower numbers should be used first
320             #pod weight - weight to break priority ties; higher numbers preferred
321             #pod
322             #pod =cut
323              
324             sub srv {
325 0     0 1   my ($self, $rec) = @_;
326              
327             Carp::confess("srv record with no target! use empty string for null target")
328 0 0         unless defined $rec->{target};
329              
330 0           for my $needed (qw(port service domain)) {
331             Carp::confess("tried to make srv record with no $needed!")
332 0 0         unless defined $rec->{$needed};
333             }
334              
335 0   0       my $priority = $rec->{priority} || 0;
336 0   0       my $weight = $rec->{weight} || 0;
337              
338 0           my @lines;
339             push @lines, sprintf ":_%s._%s.%s:33:%s%s%s%s:%s:%s\n",
340             $rec->{service},
341             $rec->{protocol} || 'tcp',
342             $rec->{domain},
343             _escaped_octals($priority),
344             _escaped_octals($weight),
345             _escaped_octals($rec->{port}),
346             _hostname_to_labels($rec->{target}),
347             $rec->{ttl} || $self->_default_ttl,
348 0   0       $rec->{location} || '';
      0        
      0        
349              
350 0           return @lines;
351             }
352              
353             #pod =method dkim
354             #pod
355             #pod This returns lines for TXT records for DKIM keys. It takes the following
356             #pod arguments:
357             #pod
358             #pod domain - the domain
359             #pod selector - the key selector
360             #pod
361             #pod ttl - record time to live
362             #pod
363             #pod tags - the DKIM record tags, a hashref
364             #pod
365             #pod Any tag given in the hashref will be included. C<p> is required.
366             #pod
367             #pod =cut
368              
369             sub dkim {
370 0     0 1   my ($self, $rec) = @_;
371              
372 0 0         Carp::confess("no domain for DKIM record") unless $rec->{domain};
373 0 0         Carp::confess("no selector for DKIM record") unless $rec->{selector};
374 0 0         Carp::confess("no public key for DKIM record") unless $rec->{tags}{p};
375              
376 0           my $tags = $rec->{tags};
377 0           my $name = "$rec->{selector}._domainkey.$rec->{domain}";
378 0           my $text = join q{; }, map {; "$_=$tags->{$_}" }
379 0 0         sort { $a eq 'v' ? -1 : $b eq 'v' ? 1 : ($a cmp $b) } keys %$tags;
  0 0          
380              
381             # We can't use ->txt because tinydns will split TXT records (generated by ')
382             # up into 127b chunks. DKIM doesn't let you do that. -- rjbs, 2016-10-04
383             return sprintf ":%s:16:\\%03o%s:%s\n",
384             $name,
385             length($text),
386             _colon_safe($text),
387 0   0       $rec->{ttl} || $self->_default_ttl;
388             }
389              
390             1;
391              
392             __END__
393              
394             =pod
395              
396             =encoding UTF-8
397              
398             =head1 NAME
399              
400             DNS::Oterica::RecordMaker::TinyDNS - a tinydns recordmaker for DNSO.
401              
402             =head1 VERSION
403              
404             version 0.304
405              
406             =head1 DESCRIPTION
407              
408             This role provides logic for generating lines for the F<tinydns-data> program
409             to consume.
410              
411             =head1 METHODS
412              
413             =head2 comment
414              
415             my $line = $rec->comment("Hello, world!");
416              
417             This returns a line that is a one-line commment.
418              
419             =head2 location
420              
421             This returns a location line.
422              
423             =head2 a_and_ptr
424              
425             Generate an C<=> line, the bread and butter A and PTR record pair for a
426             hostname and IP.
427              
428             =head2 ptr
429              
430             Generate an C<^> line, for the reverse DNS of an IP address.
431              
432             =head2 srv
433              
434             @lines = $rec->srv({
435             # We want to produce _finger._tcp.example.com for port 70
436             domain => 'example.com',
437             service => 'finger',
438             protocol => 'tcp',
439             target => 'f.example.com',
440             port => 70,
441              
442             priority => 10,
443             weight => 20,
444             });
445              
446             This returns lines for SRV records following RFC 2782. It takes the following
447             special arguments:
448              
449             domain - the domain offering service
450             service - the well-known service name (http, imaps, finger)
451             protocol - tcp or udp
452              
453             target - the host providing service
454             port - the port the service listens on
455              
456             priority - numeric priority; lower numbers should be used first
457             weight - weight to break priority ties; higher numbers preferred
458              
459             =head2 dkim
460              
461             This returns lines for TXT records for DKIM keys. It takes the following
462             arguments:
463              
464             domain - the domain
465             selector - the key selector
466              
467             ttl - record time to live
468              
469             tags - the DKIM record tags, a hashref
470              
471             Any tag given in the hashref will be included. C<p> is required.
472              
473             =head1 AUTHOR
474              
475             Ricardo SIGNES <rjbs@cpan.org>
476              
477             =head1 COPYRIGHT AND LICENSE
478              
479             This software is copyright (c) 2016 by Ricardo SIGNES.
480              
481             This is free software; you can redistribute it and/or modify it under
482             the same terms as the Perl 5 programming language system itself.
483              
484             =cut