File Coverage

blib/lib/Config/TinyDNS.pm
Criterion Covered Total %
statement 74 74 100.0
branch 13 14 92.8
condition 10 12 83.3
subroutine 19 19 100.0
pod 4 4 100.0
total 120 123 97.5


line stmt bran cond sub pod time code
1             package Config::TinyDNS;
2              
3             =head1 NAME
4              
5             Config::TinyDNS - Manipulate tinydns' data file
6              
7             =head1 SYNOPSIS
8              
9             use Config::TinyDNS qw/filter_tdns_data/;
10              
11             my $data = File::Slurp::read_file(...);
12             $data = filter_tdns_data $data, qw/include vars lresolv/;
13              
14             =head1 DESCRIPTION
15              
16             Tinydns, the DNS server in Dan Bernstein's djbdns package, uses a simple
17             line-based format instead of a zone file. The format was designed to be
18             easy for machines to parse, so it sometimes requires rather a lot of
19             repetition. This module provides functions for manipulating these files,
20             however it is primarily intended as the backend for
21             L.
22              
23             The general principle of operation is that the file is split into
24             records and fields, these records are passed through a series of
25             filters, and the results joined back up into a config file. The basic
26             file format is line-based, with each line consisting of a
27             single-character operator followed by a number of colon-separated
28             arguments. For more details on the format, see L.
29              
30             =head1 FUNCTIONS
31              
32             =cut
33              
34 10     10   467594 use 5.010;
  10         42  
  10         413  
35 10     10   55 use warnings;
  10         22  
  10         368  
36 10     10   70 use strict;
  10         31  
  10         541  
37 10     10   82 use Scalar::Util qw/reftype/;
  10         18  
  10         1747  
38 10     10   28736 use List::MoreUtils qw/natatime/;
  10         27284  
  10         1001  
39 10     10   118 use Carp;
  10         21  
  10         936  
40              
41 10     10   11240 use Exporter::NoWork;
  10         159014  
  10         86  
42              
43             our $VERSION = 1;
44              
45             my %Filters;
46              
47             =head2 C>
48              
49             Breaks the provided string up into a list of arrayrefs. Each arrayref
50             represents a line of the input; each line is broken into the initial
51             single-character operator and the subsequent colon-separated fields.
52             Trailing blank fields are removed. Blank lines are removed. Comments are
53             not broken up into fields.
54              
55             For example, an input of
56              
57             +foo.com:1.2.3.4:
58             Idynamic/bar.org
59             # some:comment
60              
61             would produce a data structure like
62              
63             ["+", "foo.com", "1.2.3.4"],
64             ["I", "dynamic/bar.org"],
65             ["#", " some:comment"],
66              
67             =cut
68              
69             sub split_tdns_data {
70 626 100       9932 map {
    100          
71 264     264 1 1394 s/(.)//
72             ? [$1, ($1 eq "#" ? $_ : split /:/)]
73             : ()
74             } split /\n/, $_[0];
75             }
76              
77             sub _strip_blank {
78 550     550   885 @_ = @{[@_]};
  550         3278  
79 550   100     14674 pop while @_ and not (defined $_[-1] and length $_[-1]);
      100        
80 550         28526 @_;
81             }
82              
83             =head2 C>
84              
85             Join the result of C> back up into a single string.
86             Undef fields are silently rendered as blanks. Trailing empty fields are
87             removed.
88              
89             =cut
90              
91             sub join_tdns_data {
92 10     10   3326 no warnings "uninitialized";
  10         27  
  10         23470  
93 550         3456 join "", map "$_\n", map {
94 261     261 1 10271 $_->[0] . join ":", _strip_blank @$_[1..$#$_]
95             } @_;
96             }
97              
98             sub _lookup_filt {
99 261     261   12230 my ($k, @args) = @_;
100 261 100       1750 my $f = $Filters{$k} or croak "bad filter: $k";
101 259         1162 given (reftype $f) {
102 259         2762 when ("CODE") { return $f }
  13         62  
103 246         387 when ("REF") { return ($$f)->(@args) }
  245         611  
104 1         2 default { die "bad \%Filters entry: $k => $f" }
  1         16  
105             }
106             }
107            
108             sub _decode_filt {
109 264     264   8171 my ($f) = @_;
110 264 100       969 defined $f or return;
111 263         731 given (reftype $f) {
112 263         1763 when ("CODE") { return $f }
  10         29  
113 253         701 when (undef) { return _lookup_filt $f }
  245         898  
114 8         13 when ("ARRAY") { return _lookup_filt @$f }
  7         20  
115 1         3 default { croak "bad filter: $f" }
  1         16  
116             }
117             }
118              
119             sub _call_filt {
120 606     606   2550 my $c = shift;
121 606 50       1657 my $r = @_ ? shift : $_;
122 606         5507 my ($f, @r) = @$r;
123 606         1237 local $_ = $f;
124 606         1667 $c->(@r);
125             }
126              
127             =head2 C, I>
128              
129             Break I up using C>, pass it through a
130             series of filters, and join it up again with C>.
131             I should be a list of the following:
132              
133             =over 4
134              
135             =item * a CODE ref
136              
137             The coderef will be called once for each line of input. C$_> will be set
138             to the initial single character and the arguments in C<@_> will be the
139             remaining fields. The return value should be a list of arrayrefs as from
140             C>. A simple filter that changes nothing looks like
141              
142             sub { return [$_, @_] }
143              
144             =item * a plain string
145              
146             This requests a filter registered with C>. See
147             L below for a list of the predefined filters.
148              
149             =item * an ARRAY ref
150              
151             The first argument will be looked up as a registered filter. If this is
152             a generator-type filter (see below), the generator will be called with
153             the rest of the contents of the arrayref as arguments.
154              
155             =back
156              
157             =cut
158              
159             sub filter_tdns_data {
160 255     255 1 279104 my @lines = split_tdns_data shift;
161 255         2586 for my $f (@_) {
162 257         1358 my $c = _decode_filt $f;
163 257         1300 @lines =
164             map _call_filt($c),
165             @lines;
166             }
167 255         1627 return join_tdns_data @lines;
168             }
169              
170             =head2 C>
171              
172             Register filters to be called by name later. I should be a list of
173             key C<< => >> value pairs, where each value is either
174              
175             =over 4
176              
177             =item * a CODE ref
178              
179             The coderef will be called as though it had been supplied to
180             C directly. Any arguments passed (using an arrayref)
181             will be ignored.
182              
183             =item * a ref to a CODE ref
184              
185             For example
186              
187             record => \sub {
188             my %vars;
189             sub {
190             /\$/ or return [$_, @_];
191             $vars{$_[0]} = $_[1];
192             };
193             },
194              
195             The coderef will be called once when C is called, and
196             the return value will be used as the filter sub. Any arguments supplied
197             will be passed to the generator sub.
198              
199             =back
200              
201             =cut
202              
203             sub register_tdns_filters {
204 69     69 1 15186 my $i = natatime 2, @_;
205 69         573 while (my ($k, $c) = $i->()) {
206 70 100       200 $Filters{$k} and croak "filter '$k' is already registered";
207 69 100 66     891 ref $c and (
      66        
208             reftype $c eq "CODE" or (
209             reftype $c eq "REF" and reftype $$c eq "CODE"
210             )
211             ) or croak "filter must be a coderef(ref)";
212 62         526 $Filters{$k} = $c;
213             }
214             }
215              
216             # just for the tests
217 2     2   30 sub _filter_hash { \%Filters }
218              
219             =head1 FILTERS
220              
221             Many of these filters introduce ordering constraints on the lines of the
222             file. Be careful about re-ordering files written for them.
223              
224             =head2 null
225              
226             Pass all lines through unchanged. Note that blank lines and trailing
227             blank fields will still be removed.
228              
229             =cut
230              
231             register_tdns_filters
232             null => sub { [$_, @_] };
233              
234             =head2 vars
235              
236             Input lines of the form
237              
238             $name:value
239              
240             are treated as variable definitions and removed from the output.
241             Variables may have any name, but only those matching C<\w+> are useful.
242             Expressions looking like C will be substituted across all
243             fields, including in variable definitions. This allows a form of symref,
244             use of which should be discouraged. Variables must be defined before
245             they are used; nonexistent variables will be silently replaced with the
246             empty string. Dollars can be escaped by doubling them.
247              
248             $foo:foo.com
249             =$foo:1.2.3.4
250             +www.$foo:1.2.3.4
251             "txt.$foo:this $$ is a dollar
252              
253             translates to
254              
255             =foo.com:1.2.3.4
256             +www.foo.com:1.2.3.4
257             "txt.foo.com:this $ is a dollar
258              
259             =cut
260              
261             register_tdns_filters
262             vars => \sub {
263             my %vars = ('$' => '$');
264             sub {
265 10     10   101 no warnings "uninitialized";
  10         23  
  10         4537  
266             s/\$(\$|\w+)/$vars{$1}/ge for @_;
267             /\$/ or return [$_, @_];
268             $_[0] eq '$' and return;
269             $vars{$_[0]} = $_[1];
270             return;
271             }
272             };
273              
274             =head2 include
275              
276             This interprets lines of the form
277              
278             Isome/file
279              
280             as a request to include the contents of F at this point. The
281             included lines are scanned for further includes but are not passed
282             through any other filters (though this may change at some point).
283              
284             =cut
285              
286             register_tdns_filters
287             include => \sub {
288             my $include;
289             $include = sub {
290             /I/ or return [$_, @_];
291             require File::Slurp;
292             return map _call_filt($include),
293             split_tdns_data scalar File::Slurp::read_file($_[0]);
294             };
295             };
296              
297             =head2 lresolv
298              
299             Resolve hostnames in IP-address slots in the configuration using the
300             information in this file. Names must be defined before they will be
301             translated. Currently only the C<+ = . & @> lines used by
302             tinydns-data(1) are recognised. If you want to run both lresolv and
303             L, you need to run lresolv first or local hostnames will
304             already have been replaced.
305              
306             For example
307              
308             =foo.com:1.2.3.4
309             +www.foo.com:foo.com
310              
311             would translate to
312              
313             =foo.com:1.2.3.4
314             +www.foo.com:1.2.3.4
315              
316             =cut
317              
318             register_tdns_filters
319             lresolv => \sub {
320 10     10   184 no warnings "uninitialized";
  10         22  
  10         17590  
321             my %hosts;
322             my $repl = sub {
323             for ((defined $_[1] ? "$_[0]:$_[1]" : ()), $_[0]) {
324             if (
325             $_[0] =~ /[^0-9.]/ and
326             defined $hosts{$_}
327             ) {
328             $_[0] = $hosts{$_};
329             last;
330             }
331             }
332             };
333             my $qual = sub { $_[0] =~ /\./ ? $_[0] : "$_[0].$_[1].$_[2]" };
334             my $lo = sub { $_[0] . (defined $_[1] ? ":$_[1]" : "") };
335             sub {
336             given ($_) {
337             when ([".", "&"]) {
338             $repl->(@_[1, 5]);
339             my $key = $lo->($qual->($_[2], "ns", $_[0]), $_[5]);
340             $hosts{$key} = $_[1];
341             }
342             when (["=", "+"]) {
343             $repl->(@_[1, 4]);
344             $hosts{$lo->($_[0], $_[4])} = $_[1];
345             }
346             when (["@"]) {
347             $repl->(@_[1, 6]);
348             $hosts{$lo->($qual->($_[2], "mx", $_[0]), $_[6])} = $_[1];
349             }
350             }
351             [$_, @_];
352             };
353             };
354              
355             =head2 rresolv
356              
357             Resolve hostnames in IP-address slots in the configuration by looking
358             them up in the current DNS. This assumes anything which doesn't match
359             C is a hostname, and any hostname that doesn't resolve is
360             replaced with C<0.0.0.0>. Currently this only recognises the standard
361             C<+=.&@> lines.
362              
363             =cut
364              
365             register_tdns_filters
366             rresolv => \sub {
367             require Socket;
368             my $repl = sub {
369             if ($_[0] =~ /[^0-9.]/) {
370             $_[0] = Socket::inet_ntoa(
371             gethostbyname($_[0]) //
372             Socket::inet_aton("0.0.0.0")
373             );
374             }
375             };
376             sub { /[.&+=\@]/ and $repl->($_[1]); [$_, @_]; };
377             };
378              
379             =head2 site I
380              
381             This adds an extra field to C<%> lines, so they look like
382              
383             %lo:ipprefix:site
384              
385             If I is in the list of I provided, the I field will
386             be removed and the line left in the output. Otherwise, the line will be
387             removed entirely. This makes it possible to build data files for several
388             different views on the DNS from one master file.
389              
390             =cut
391              
392             register_tdns_filters
393             site => \sub {
394             my %sites = map +($_, 1), @_;
395             sub {
396             /%/ or return [$_, @_];
397             @_ > 2 or return [$_, @_];
398             my $site = pop;
399             $sites{$site} or return;
400             return [$_, @_];
401             };
402             };
403              
404             1;
405              
406             =head1 SEE ALSO
407              
408             L, L.
409              
410             =head1 AUTHOR
411              
412             Ben Morrow
413              
414             =head1 COPYRIGHT
415              
416             Copyright 2010 Ben Morrow.
417              
418             Redistribution and use in source and binary forms, with or without
419             modification, are permitted provided that the following conditions are met:
420              
421             =over 4
422              
423             =item *
424              
425             Redistributions of source code must retain the above copyright
426             notice, this list of conditions and the following disclaimer.
427              
428             =item *
429              
430             Redistributions in binary form must reproduce the above copyright
431             notice, this list of conditions and the following disclaimer in the
432             documentation and/or other materials provided with the distribution.
433              
434             =back
435              
436             THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
437             ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
438             WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
439             DISCLAIMED. IN NO EVENT SHALL BEN MORROW BE LIABLE FOR ANY
440             DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
441             (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
442             LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
443             ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
444             (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
445             SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
446              
447             =cut
448              
449             1;