File Coverage

blib/lib/TinyDNS/Record.pm
Criterion Covered Total %
statement 93 116 80.1
branch 26 38 68.4
condition 19 49 38.7
subroutine 13 14 92.8
pod 10 10 100.0
total 161 227 70.9


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