File Coverage

blib/lib/Net/DNS/Extlang.pm
Criterion Covered Total %
statement 228 229 99.5
branch 117 134 87.3
condition 23 35 65.7
subroutine 23 24 95.8
pod 9 9 100.0
total 400 431 92.8


line stmt bran cond sub pod time code
1             package Net::DNS::Extlang;
2              
3             our $VERSION = '0.2';
4              
5             =head1 NAME
6              
7             Net::DNS::Extlang - DNS extension language
8              
9              
10             =head1 SYNOPSIS
11              
12             use Net::DNS::Extlang;
13              
14             $extobj = new Net::DNS::Extlang(
15             domain => 'arpa',
16             file => '/etc/dnsext.txt',
17             lang => 'en',
18             resolver => $resobj
19             )
20              
21              
22             =head1 DESCRIPTION
23              
24             The Net::DNS::Extlang module reads and stores RR descriptions from files
25             or the DNS. If file is provided, it reads descriptions from that file,
26             otherwise it looks in .rrname. and .rrtype.
27             for descriptions in the desired language.
28              
29             Provide a resolver if you want other than the default resolver settings.
30              
31             =cut
32              
33              
34 3     3   95187 use strict;
  3         14  
  3         76  
35 3     3   15 use warnings;
  3         5  
  3         64  
36 3     3   896 use integer;
  3         24  
  3         12  
37 3     3   61 use Carp;
  3         4  
  3         9780  
38              
39              
40             =head1 METHODS
41              
42             =head2 new
43            
44             $extobj = new Net::DNS::Extlang(
45             domain => 'arpa',
46             file => '/etc/dnsext.txt',
47             lang => 'en',
48             resolver => new Net::DNS::Resolver()
49             )
50              
51             Create an object corresponding to a set of extension language entries
52             in a file or the DNS. Provide either a file or a domain argument.
53             If you provide a domain, the lang and resolver are optional.
54              
55             In addition to using its methods, Net::DNS::Extlang can be accessed
56             by Net::DNS to create rrtype packages automatically as required.
57              
58             =cut
59              
60             sub new {
61 6     6 1 5024 my $class = shift;
62              
63 6         21 my %args = (
64             lang => 'en', domain => 'services.net.', # defaults
65             @_
66             );
67              
68             my $self = bless {
69             domain => $args{domain},
70             file => $args{file},
71             lang => $args{lang},
72             resolver => $args{resolver},
73 6         27 rrnames => {}, # RRs by name
74             rrnums => {}, # RRs by number
75             }, $class;
76              
77 6 100       23 $self->_xlreadfile( $args{file} ) if $args{file};
78              
79 6         22 return $self;
80             }
81              
82              
83             =head2 domain, file, lang, resolver
84              
85             Access method which returns extlang configuration attribute.
86              
87             =cut
88              
89 5     5 1 37 sub domain { shift->{domain} }
90 2     2 1 9 sub file { shift->{file} }
91 2     2 1 7 sub lang { shift->{lang} }
92 2     2 1 7 sub resolver { shift->{resolver} }
93              
94              
95             # read a file, set the text parts of $self->rrnames and $self->rrnums
96              
97             sub _xlreadfile {
98 2     2   3 my ($self, $file) = @_;
99              
100 2 50       75 open(my $rrfile, "<", $file) or croak "Extlang file '$file' $!";
101 2         5 my @xllist = ();
102              
103 2         34 while(<$rrfile>) {
104 686         787 chomp;
105 686 100       1623 next if m/^\s*($|#)/; # comments or blank line
106 424 100       736 if( m/^\s/ ) {
107 310         822 push @xllist, join ' ', split;
108 310         858 next;
109             }
110             # must be a new one, store current one
111 114 100       302 $self->xlstorerecord(@xllist) if scalar @xllist;
112              
113 114         383 @xllist = ($_);
114             }
115 2 50       11 $self->xlstorerecord(@xllist) if scalar @xllist;
116              
117 2         23 close $rrfile;
118             }
119              
120              
121             =head2 xlstorerecord
122              
123             $rrr = $ext->xlstorerecord( $identifier, @field )
124              
125             Store a record with rrname/number and list of fields.
126              
127             =cut
128              
129             # only do rudimentary syntax checking here
130              
131             # match head record, $1 = name, $2 = number, $3 = description
132             # ignores I/A third subfield
133             my $headpattern = qr{^ ([A-Z][-A-Z0-9]*) : (\d+)\S* \s* (.*) $}ix;
134              
135              
136             # match a field, $1 = type, $2 = quals, $3 - name, $4 = comment
137             my $fieldpattern = qr{^(I[124]|AAAA|AA|B32|B64|T6|X[P68]|[ANRSTXZ])
138             (?:\[( (?: [CALMX]|[-A-Z0-9]+=\d+\W*)+ )\])?
139             :? ([a-z][-a-z0-9]*)? \s*(.*) $}ix;
140              
141              
142             sub xlstorerecord {
143 116     116 1 211 my ($self, $rr, @fieldlist) = @_;
144              
145 116 50       167 croak "no rr record" if !$rr;
146              
147 116         447 my ($rrname, $rrnum, $rrcomment ) = $rr =~ m{$headpattern}o;
148 116 50 33     343 croak "invalid rr record $rr" if !$rrname or !$rrnum;
149              
150             # parse each field descriptor into a hash via $fieldpattern
151 116         121 my @fieldstructs;
152 116         152 foreach (@fieldlist) {
153 318 50       1347 m{$fieldpattern}o || croak "invalid field in $rrname: $_";
154 318   100     800 my $q = $2 || '';
155 318         1601 push @fieldstructs, {
156             type => uc($1),
157             quals => join(',', sort split /,/, uc $q), # alphabetize quals
158             name => $3,
159             comment => $4
160             };
161             }
162              
163             # make up an rr thing
164 116         448 my $rrr = {
165             mnemon => $rrname,
166             number => 0 + $rrnum,
167             comment => $rrcomment,
168             fields => [@fieldstructs]
169             };
170              
171             # stash it by name and number
172 116         269 $self->{rrnames}->{$rrname} = $rrr;
173 116         261 $self->{rrnums}->{$rrnum} = $rrr;
174             }
175              
176 0     0   0 sub _xlstorerecord { &xlstorerecord } ## now a public method (used in RRTYPEgen and Net::DNS)
177              
178              
179             =head2 getrr
180              
181             $rrinfo = $ext->getrr(nameornumber)
182              
183             Retrieve the rr description by number (if the argument is all digits)
184             or name (otherwise.) $rrinfo is a reference to a hash with entries for
185             mnemon, number, comment, and fields: the lines in the description
186             stanza. Each field is a hash with entries type (field type),
187             quals (optional qualifiers), name (optional field name), and comment.
188              
189             Descriptions from a file are all loaded by new(), from the DNS
190             are fetched as needed.
191             If there's no description for that name or number it returns undef.
192              
193             =cut
194              
195             sub getrr {
196 60     60 1 105 my ($self, $rrn) = @_;
197 60         91 my $name;
198              
199 60 50       114 croak("Need rrname or rrtype in getrr") unless $rrn;
200            
201 60 100       187 if($rrn =~ m{^\d+$}) { # look up by number
202 2 100       24 return $self->{rrnums}->{$rrn} if $self->{rrnums}->{$rrn};
203 1 50       8 return undef if defined $self->{file}; # not in the file
204 1         5 $name = "$rrn.rrtype.$self->{domain}"; # try DNS
205              
206             } else { # look up by name
207 58         104 $rrn = uc $rrn; # RRTYPES are UPPER CASE
208 58 100       213 return $self->{rrnames}->{$rrn} if $self->{rrnames}->{$rrn};
209 1 50       5 return undef if defined $self->{file}; # not in the file
210 1         6 $name = "$rrn.rrname.$self->{domain}"; # try DNS
211             }
212              
213             # look it up
214 2   66     12 my $res = $self->{resolver} ||= do {
215 1         9 require Net::DNS::Resolver;
216 1         13 new Net::DNS::Resolver();
217             };
218              
219 2   50     60 my $response = $res->query($name, 'TXT') || return; # undef if nothing there
220              
221 2         223849 foreach my $rr ($response->answer) {
222 4 100       66 next if $rr->type ne 'TXT';
223              
224 2         29 my @txt = $rr->txtdata;
225              
226 2 50       379 next unless $txt[0] eq "RRTYPE=1";
227            
228 2         43 my ($trname, $trno) = $txt[1] =~ m{$headpattern}o;
229 2 50 33     22 croak "invalid description $txt[1]" if !$trname or !$trno;
230              
231             # make sure it's the right rr
232 2 50       49 croak "wrong rrtype $rrn $txt[1]" unless $txt[1] =~ m/$rrn/;
233            
234 2         9 shift @txt; # get rid of desc tag
235 2         13 return $self->xlstorerecord(@txt); # will croak if bad syntax
236             }
237             }
238              
239             =head2 compile / compilerr
240              
241             $code = $ext->compile(nameornumber)
242             $code = $ext->compilerr($rrr)
243              
244             Compile the rr description into Net::DNS::RR: and return
245             the perl code, suitable to pass to eval().
246             nameornumber is looked up, $rrr is an rr description such as getrr()
247             returns.
248              
249             If there's no description it returns null.
250              
251             Compiled methods include:
252              
253             _decode_rdata, _encode_rdata, _format_rdata, _parse_rdata, _defaults
254              
255             get/set for each field named to match the field, or fieldN if the field
256             has no name or a duplicate name.
257             If field names match built in names or perl keywords, the get/set
258             method name is prefixed with 'f'.
259              
260             =cut
261              
262              
263             # $rrr is a rrinfo hash, %pats are patterns to select from based on the
264             # type and quals where it looks for type[quals], then type, then
265             # "default". When checking for quals they are alphabetized so a query
266             # for N[C,A] will match N[A,C]
267             #my $CDEBUG = 0;
268              
269             sub _cchunk($@) {
270 1158     1158   5226 my ($rrr, %pats) = @_;
271 1158         1649 my $type = $rrr->{type};
272 1158         1391 my $qual = $rrr->{quals};
273              
274 1158 100       1672 if($qual) {
275 285         457 my $k = $type . "[$qual]";
276             # print "check $k\n" if $CDEBUG;
277 285 100       747 return $pats{$k} if exists $pats{$k};
278             }
279              
280             # print "check $type\n" if $CDEBUG;
281 1040 100       3255 return exists($pats{$type}) ? $pats{$type} : $pats{"default"};
282             }
283              
284              
285             # substitite #WORD# in the string with $p{WORD} in the list
286             # csub($string, 'FOO' => "foo", 'BAR' => "bahr", ... )
287             sub _csub($@) {
288 765     765   1520 my ($str, %subs) = @_;
289              
290 765         1104 for ($str) {
291 765         2554 s/#([A-Z]+)#/$subs{$1}/eg;
  1718         4857  
292 765         2194 return $_;
293             }
294             }
295              
296              
297             # names that conflict with RR methods
298             my %dirtywords = map { ($_, 1) } qw( new decode encode canonical print string plain token name owner next last
299             type class ttl dump rdatastr rdata rdstring rdlength destroy autoload );
300              
301             sub compile {
302 60     60 1 562085 my ($self, $rrn) = @_;
303              
304 60 50       152 croak("Need rrname or rrtype in compile") unless $rrn;
305            
306 60         121 my $rrr = $self->getrr($rrn);
307 60 50       197 $self->compilerr($rrr) if $rrr;
308             }
309              
310             sub compilerr {
311 60     60 1 82 my ($self, $rrr) = @_;
312              
313 60         123 my $rrname = uc $rrr->{mnemon};
314 60         80 my $rrnum = $rrr->{number};
315 60   50     124 my $rrcomment = $rrr->{comment} || '';
316 60         83 my $rrfields = $rrr->{fields};
317            
318 60         96 my ($usedomainname, # if there's an N field
319             $usetext, # if there's an S field
320             $usemailbox, # if theres an N[A] field
321             $usebase64, # if there's a B32 or B64 field
322             $usetime, # if there's a time field
323             $userrtype, # if there's a rrtype field
324             $usensechelp, # if there's a rrtype list field or nsec3 base32
325             %fields, # field names in use
326             $fieldno, # to generate fieldN names
327             $decode, # contents of decode routine
328             $encode, # contents of encode routine
329             $format, # contents of format routine
330             $parse, # contents of parse routine
331             $defaults, # contents of defaults routine
332             $fieldfns # functions get/set fields
333             );
334              
335 60         109 foreach my $f (@$rrfields) {
336 170         198 $fieldno++;
337 170         429 my ($type, $quals, $name) = ($f->{type}, $f->{quals}, lc $f->{name});
338              
339 170 50 0     277 carp("Unimplemented field type Z[$quals] in $rrname") && return if $type eq "Z";
340              
341             # censor dirty words
342 170 100       282 $name = $f->{name} = "f$name" if $dirtywords{$name};
343              
344             # make a name if there isn't one yet
345 170 50 33     447 $f->{name} = $name = "field$fieldno" if !$name or exists $fields{$name};
346              
347 170         299 $fields{$name} = $fieldno;
348              
349 170 100 66     580 if($type eq 'N') {
    100          
    100          
    100          
    100          
    100          
350 33         40 $usedomainname = 1;
351 33 100       94 $usemailbox = 1 if $quals =~ m{A};
352             } elsif($type eq 'S') {
353 16         26 $usetext = 1;
354             } elsif($type eq "B64") {
355 9         16 $usebase64 = 1;
356             } elsif($type eq "B32") {
357 1         2 $usensechelp = 1;
358             } elsif($type eq "T" or $type eq "T6") {
359 4         17 $usetime = 1;
360             } elsif($type eq "R" ) {
361 4 100       8 if( $quals eq "L" ) {
362 3         5 $usensechelp = 1;
363             } else {
364 1         2 $userrtype = 1;
365             }
366             }
367             }
368             # now get them in order, in a perhaps overcomplex way
369 60         99 my @fields = map { $_->{name} } @$rrfields;
  170         317  
370            
371             #print "fields are ",join(",", @fields), "\n";
372            
373             # generate per-field functions
374 60         110 $fieldfns = _perfield($rrfields);
375            
376             # default function
377 60         110 $defaults = _fielddefault($rrfields);
378 60         102 $decode = _fielddecode($rrfields);
379 60         109 $encode = _fieldencode($rrfields);
380 60         114 $parse = _fieldparse($rrfields);
381 60         128 $format = _fieldformat($rrfields);
382              
383             # other modules to include, depending on the type
384 60         84 my $uses = "";
385 60 100       96 $uses = "use Net::DNS::DomainName;\n" if $usedomainname;
386 60 100       90 $uses .= "use Net::DNS::Mailbox;\n" if $usemailbox;
387 60 100       88 $uses .= "use Net::DNS::Text;\n" if $usetext;
388 60 100       81 $uses .= "use MIME::Base64;\n" if $usebase64;
389 60 100       91 $uses .= "use Net::DNS::Extlang::Time qw(_encodetime _string2time);\n" if $usetime;
390 60 100       74 $uses .= "use Net::DNS::Parameters qw(typebyname typebyval);\n" if $userrtype;
391 60 100       80 $uses .= "use Net::DNS::Extlang::Nsechelp;\n" if $usensechelp;
392              
393              
394             # glom it all together and return string
395 60         75 my $identifier = $rrname;
396 60         129 $identifier =~ s/\W/_/g;
397            
398 60         849 return <<"CODE";
399             # generated package $rrname; $rrcomment
400             package Net::DNS::RR::TYPE$rrnum;
401             use strict;
402             use base qw(Net::DNS::RR);
403             use integer;
404             use Carp;
405             $uses
406              
407             $decode
408              
409             $encode
410              
411             $format
412              
413             $parse
414              
415             $defaults
416              
417             $fieldfns
418              
419              
420             {
421             # also make accessible by symbolic name
422             package Net::DNS::RR::$identifier;
423             our \@ISA = qw(Net::DNS::RR::TYPE$rrnum); # Avoid "use base ...;" (RT#123702)
424             }
425              
426              
427             1;
428              
429             __END__