File Coverage

blib/lib/TinyDNS/Record.pm
Criterion Covered Total %
statement 92 115 80.0
branch 26 38 68.4
condition 20 51 39.2
subroutine 13 14 92.8
pod 10 10 100.0
total 161 228 70.6


line stmt bran cond sub pod time code
1              
2             =head1 NAME
3            
4             TinyDNS::Record - Parse a single TinyDNS Record.
5            
6             =head1 DESCRIPTION
7            
8             This module provides an object API to a single TinyDNS record/line.
9            
10             It is not quite valid because:
11            
12             =over 8
13            
14             =item *
15             We ignore SOA records, which Amazon would handle for us.
16            
17             =item *
18             Our TXT records handling uses "T" not ":".
19            
20             =item *
21             Our SRV records are non-standard.
22            
23             =item *
24             Our MX record handling allows a name to be set without IP.
25            
26             =back
27            
28             There are probably other differences.
29            
30             =cut
31              
32             =head1 METHODS
33            
34             =cut
35              
36 3     3   27 use strict;
  3         6  
  3         61  
37 3     3   12 use warnings;
  3         10  
  3         115  
38              
39             package TinyDNS::Record;
40              
41              
42 3     3   18 use Carp;
  3         6  
  3         279  
43              
44              
45             #
46             # Allow our object to treated as a string.
47             #
48 3     3   4567 use overload '""' => 'stringify';
  3         3063  
  3         17  
49              
50              
51              
52             =head2 new
53            
54             Constructor, which sets the type of the object.
55            
56             The constructor is expected to be passed a valid line of text which
57             describes a single record, for example C<+example.example.com:1.2.3.4:200>.
58            
59             =cut
60              
61             sub new
62             {
63 62     62 1 38223     my ( $proto, $line ) = (@_);
64 62   33     311     my $class = ref($proto) || $proto;
65              
66 62         119     my $self = {};
67 62         126     bless( $self, $class );
68              
69             #
70             # Record the line we were created with.
71             #
72 62         229     $self->{ 'input' } = $line;
73              
74             #
75             # The record-type is the first character.
76             #
77 62         133     my $rec = substr( $line, 0, 1 );
78              
79             #
80             # Remove the record-type from the line
81             #
82 62         116     $line = substr( $line, 1 );
83              
84             #
85             # Tokenize - NOTE This is ignored for TXT records,
86             # (because a TXT record used for SPF might have an embedded
87             # ":" for example.)
88             #
89 62         216     my @data = split( /:/, $line );
90              
91             #
92             # Nasty parsing for each record type..
93             #
94             # We should do better.
95             #
96             #
97 62 100 66     489     if ( ( $rec eq '+' ) || ( $rec eq '=' ) )
    50 66        
    100 66        
    100          
    100          
    100          
    100          
    50          
98                 {
99              
100             # name : ipv4 : ttl
101 25         55         $self->{ 'type' } = "A";
102 25         58         $self->{ 'name' } = $data[0];
103 25         48         $self->{ 'value' } = $data[1];
104 25   50     91         $self->{ 'ttl' } = $data[2] || 300;
105                 }
106                 elsif ( $rec eq '_' )
107                 {
108              
109             # The long-form is:
110             # $name.$proto.$domain : $hostname : $port : $prior : $weight : $ttl
111             #
112             # The short-form is much more common and useful:
113             # $name.$proto.$domain : $hostname : $port : $ttl
114             #
115 0         0         $self->{ 'type' } = "SRV";
116 0         0         $self->{ 'name' } = "_" . $data[0];
117              
118 0 0       0         if ( scalar(@data) == 4 )
119                     {
120 0   0     0             my $host = $data[1] || 0;
121 0   0     0             my $port = $data[2] || 0;
122 0   0     0             my $ttl = $data[3] || 300;
123              
124             # Bogus priority + weight
125 0         0             $self->{ 'value' } = "1 10 $port $host";
126 0         0             $self->{ 'ttl' } = $ttl;
127                     }
128                     else
129                     {
130 0   0     0             my $host = $data[1] || 0;
131 0   0     0             my $port = $data[2] || 0;
132 0   0     0             my $priority = $data[3] || 1;
133 0   0     0             my $weight = $data[4] || 10;
134 0   0     0             my $ttl = $data[5] || 300;
135              
136             # Bogus priority + weight
137 0         0             $self->{ 'value' } = "$priority $weight $port $host";
138 0         0             $self->{ 'ttl' } = $ttl;
139                     }
140              
141                 }
142                 elsif ( $rec eq '6' )
143                 {
144              
145             # name : ipv6 : ttl
146 21         182         $self->{ 'type' } = "AAAA";
147 21         60         $self->{ 'name' } = $data[0];
148 21   50     78         $self->{ 'ttl' } = $data[2] || 300;
149              
150             #
151             # Convert an IPv6 record of the form:
152             # "200141c8010b01010000000000000010"
153             # to the expected value:
154             # "2001:41c8:010b:0101:0000:0000:0000:0010".
155             #
156 21         40         my $ipv6 = $data[1];
157 21         247         my @tmp = ( $ipv6 =~ m/..../g );
158 21         122         $self->{ 'value' } = join( ":", @tmp );
159                 }
160                 elsif ( $rec eq '@' )
161                 {
162              
163             #
164             # @xxx:name:ttl
165             # @xxx:[ip]:name:ttl
166             #
167 6 100       19         if ( scalar(@data) == 4 )
168                     {
169 3         8             $self->{ 'type' } = "MX";
170 3         10             $self->{ 'name' } = $data[0];
171 3   50     13             $self->{ 'priority' } = $data[3] || "15";
172 3   50     36             $self->{ 'ttl' } = $data[4] || 300;
173 3         13             $self->{ 'value' } = $self->{ 'priority' } . " " . $data[2];
174                     }
175 6 100       21         if ( scalar(@data) == 3 )
176                     {
177 3         7             $self->{ 'type' } = "MX";
178 3         6             $self->{ 'name' } = $data[0];
179 3   50     11             $self->{ 'priority' } = $data[2] || "15";
180 3   50     17             $self->{ 'ttl' } = $data[3] || 300;
181 3         10             $self->{ 'value' } = $self->{ 'priority' } . " " . $data[1];
182                     }
183                 }
184                 elsif ( $rec eq '&' )
185                 {
186              
187             #
188             # NS
189             # &host.example.com:IGNORED:ns1.secure.net:ttl
190             #
191 2         6         $self->{ 'type' } = "NS";
192 2         4         $self->{ 'name' } = $data[0];
193 2         6         $self->{ 'value' } = $data[2];
194 2   50     7         $self->{ 'ttl' } = $data[3] || 300;
195                 }
196                 elsif ( ( $rec eq 'c' ) || ( $rec eq 'C' ) )
197                 {
198              
199             #
200             # name : dest : [ttl]
201             #
202 2         8         $self->{ 'type' } = "CNAME";
203 2         8         $self->{ 'name' } = $data[0];
204 2         7         $self->{ 'value' } = $data[1];
205 2   50     13         $self->{ 'ttl' } = $data[2] || 300;
206                 }
207                 elsif ( ( $rec eq 't' ) || ( $rec eq 'T' ) )
208                 {
209              
210             #
211             # name : "data " : [TTL]
212             #
213 4 50       28         if ( $line =~ /([^:]+):"([^"]+)":*([0-9]*)$/ )
214                     {
215 4         47             $self->{ 'type' } = "TXT";
216 4         21             $self->{ 'name' } = $1;
217 4         17             $self->{ 'value' } = "\"$2\"";
218 4   100     25             $self->{ 'ttl' } = $3 || 3600;
219                     }
220                     else
221                     {
222 0         0             die "Invalid TXT record - $line\n";
223                     }
224                 }
225                 elsif ( $rec eq '^' )
226                 {
227              
228             #
229             # ptr : "rdns " : [TTL]
230             #
231 2         3         $self->{ 'type' } = "PTR";
232 2         5         $self->{ 'name' } = $data[0];
233 2         4         $self->{ 'value' } = $data[1];
234 2   100     9         $self->{ 'ttl' } = $data[2] || 300;
235                 }
236                 else
237                 {
238 0         0         carp "Unknown record type [$rec]: $line";
239 0         0         return;
240                 }
241 62         233     return $self;
242              
243             }
244              
245              
246             =head2 input
247            
248             Return the text that this record was created with.
249            
250             =cut
251              
252             sub input
253             {
254 0     0 1 0     my ($self) = (@_);
255              
256 0         0     return ( $self->{ 'input' } );
257             }
258              
259              
260             =head2 valid
261            
262             Is this record valid? Return 0 or 1 as appropriate.
263            
264             =cut
265              
266             sub valid
267             {
268 61     61 1 24284     my ($self) = (@_);
269              
270 61 50       304     return ( $self->{ 'type' } ? 1 : 0 );
271             }
272              
273              
274             =head2 type
275            
276             Return the type this record has, such as "A", "AAAA", "NS", etc.
277            
278             =cut
279              
280             sub type
281             {
282 166     166 1 218     my ($self) = (@_);
283              
284 166         561     return ( $self->{ 'type' } );
285             }
286              
287              
288             =head2 ttl
289            
290             Return the TTL of this recrd.
291            
292             If no TTL was explicitly specified we default to 300 seconds, or five minutes.
293            
294             =cut
295              
296             sub ttl
297             {
298 161     161 1 210     my ($self) = (@_);
299              
300 161 50 33     1586     if ( $self->{ 'ttl' } &&
301                      $self->{ 'ttl' } =~ /^([0-9]+)$/ )
302                 {
303 161         1017         return $self->{ 'ttl' };
304                 }
305              
306 0         0     return 300;
307             }
308              
309              
310             =head2 name
311            
312             Get the name of this record.
313            
314             =cut
315              
316             sub name
317             {
318 166     166 1 206     my ($self) = (@_);
319 166         641     return ( $self->{ 'name' } );
320             }
321              
322              
323             =head2 value
324            
325             Get the value of this record.
326            
327             =cut
328              
329             sub value
330             {
331 170     170 1 204     my ($self) = (@_);
332              
333 170         630     return ( $self->{ 'value' } );
334             }
335              
336              
337             =head2 add
338            
339             Add a new value to the existing record.
340            
341             This is used by the L<TinyDNS::Reader::Merged> module.
342            
343            
344             =cut
345              
346             sub add
347             {
348 1     1 1 2     my ( $self, $addition ) = (@_);
349              
350 1         2     my $value = $self->{ 'value' };
351 1 50       4     if ( ref \$value eq "SCALAR" )
352                 {
353 1         2         my $x;
354 1         2         push( @$x, $value );
355 1         3         push( @$x, $addition );
356 1         4         $self->{ 'value' } = $x;
357                 }
358                 else
359                 {
360 0         0         push( @$value, $addition );
361 0         0         $self->{ 'value' } = $value;
362                 }
363             }
364              
365              
366             =head2 stringify
367            
368             Convert the record to a string, suitable for printing.
369            
370             =cut
371              
372             sub stringify
373             {
374 73     73 1 1143     my ($self) = (@_);
375 73         107     my $txt = "";
376              
377 73 50       176     $txt .= ( "Type " . $self->type() . "\n" ) if ( $self->type() );
378 73 50       186     $txt .= ( " Name:" . $self->name() . "\n" ) if ( $self->name() );
379 73 50       175     $txt .= ( " Value:" . $self->value() . "\n" ) if ( $self->value() );
380 73 50       179     $txt .= ( " TTL:" . $self->ttl() . "\n" ) if ( $self->ttl() );
381              
382             }
383              
384              
385             =head2 hash
386            
387             Return a consistent hash of the record.
388            
389             =cut
390              
391             sub hash
392             {
393 10     10 1 11     my ($self) = (@_);
394              
395 10         11     my $hash;
396 10         16     $hash .= $self->type();
397 10         17     $hash .= $self->name();
398 10         17     $hash .= $self->value();
399 10         15     $hash .= $self->ttl();
400              
401 10         21     return ($hash);
402             }
403              
404             1;
405              
406              
407             =head1 AUTHOR
408            
409             Steve Kemp <steve@steve.org.uk>
410            
411             =cut
412              
413             =head1 COPYRIGHT AND LICENSE
414            
415             Copyright (C) 2014-2015 Steve Kemp <steve@steve.org.uk>.
416            
417             This code was developed for an online Git-based DNS hosting solution,
418             which can be found at:
419            
420             =over 8
421            
422             =item *
423             https://dns-api.com/
424            
425             =back
426            
427             This library is free software. You can modify and or distribute it under
428             the same terms as Perl itself.
429            
430             =cut
431