5 # Copyright (c) 2020, the valtz authors
8 # Redistribution and use in source and binary forms, with or without
9 # modification, are permitted provided that the following conditions are
12 # Redistributions of source code must retain the above copyright notice,
13 # this list of conditions and the following disclaimer.
15 # Redistributions in binary form must reproduce the above copyright
16 # notice, this list of conditions and the following disclaimer in the
17 # documentation and/or other materials provided with the distribution.
19 # Neither the name of valtz nor the names of its contributors may be
20 # used to endorse or promote products derived from this software
21 # without specific prior written permission.
23 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
24 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
25 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
26 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
27 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
28 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
29 # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
30 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
31 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
32 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
33 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
40 use File
::Temp qw
/ tempfile /;
41 use File
::Copy qw
/ move /;
49 getopts
('?fFhHiIqrRtT:', \
%opt);
55 # "Permission" errors with respect to what record types are allowed
60 # global location registry
61 # (reset for every zone file)
64 # NOTE : DO NOT CHANGE the id numbers
65 my %validation_msg = (
66 1001 => 'badly formed; should be just two ASCII letters',
67 1002 => 'location is not previously defined in a %-line',
68 1003 => 'invalid syntax',
69 1004 => 'invalid syntax of integer',
70 1005 => 'parts must only contain ASCII letters, digits and - characters',
71 1006 => 'parts must not begin with the - character',
72 1007 => 'parts must not end with the - character',
73 1008 => 'integer out of bounds',
74 1009 => 'must have at least three labels to be valid as mail address',
75 1010 => 'must not be 2(NS), 5(CNAME), 6(SOA), 12(PTR), 15(MX) or 252(AXFR)',
76 1011 => 'IP address found where hostname expected'
79 # NOTE : ONLY translate the right-hand part
82 'ipprefix' => 'IP prefix',
83 'fqdn' => 'Domain name',
87 'timestamp' => 'Timestamp',
92 'mname' => 'Master name',
93 'rname' => 'Role name',
94 'ser' => 'Serial number',
95 'ref' => 'Refresh time',
96 'ret' => 'Retry time',
97 'exp' => 'Expire time',
98 'min' => 'Minimum time',
99 'n' => 'Record type number',
100 'rdata' => 'Resource data',
102 'priority' => 'Priority',
114 '-' => ':disabled +',
123 # NOTE : This should NOT be translated!
125 '%' => [ ':location', 'lo:ipprefix', 'lo' ],
126 '.' => [ 'NS(+A?)', 'fqdn:ip:x:ttl:timestamp:lo', 'fqdn' ],
127 '&' => [ 'NS(+A?)', 'fqdn:ip:x:ttl:timestamp:lo', 'fqdn' ],
128 '=' => [ 'A+PTR', 'fqdn:ip:ttl:timestamp:lo', 'fqdn:ip' ],
129 '+' => [ 'A', 'fqdn:ip:ttl:timestamp:lo', 'fqdn:ip' ],
130 '@' => [ 'MX(+A?)', 'fqdn:ip:x:dist:ttl:timestamp:lo', 'fqdn' ],
131 '#' => [ ':comment', '', '' ],
132 '-' => [ ':disabled +', '', '' ],
133 "'" => [ 'TXT', 'fqdn:s:ttl:timestamp:lo', 'fqdn:s' ],
134 '^' => [ 'PTR', 'fqdn:p:ttl:timestamp:lo', 'fqdn:p' ],
135 'C' => [ 'CNAME', 'fqdn:p:ttl:timestamp:lo', 'fqdn:p' ],
136 'S' => [ 'SRV', 'fqdn:ip:x:port:weight:priority:ttl:timestamp:lo',
138 'Z' => [ 'SOA', 'fqdn:mname:rname:ser:ref:ret:exp:min:ttl:timestamp:lo',
139 'fqdn:mname:rname' ],
140 ':' => [ 'GENERIC', 'fqdn:n:rdata:ttl:timestamp:lo', 'fqdn:n:rdata' ]
146 my ($s, $boundary) = @_;
153 $result = 1008 if $boundary && ($i >= $boundary);
164 # NOTE : No translation here!
165 my %token_validator = (
169 return 1001 unless $s =~ /^[a-z][a-z]$/i;
176 return 1002 unless exists($loreg{$s});
180 'ipprefix' => [ 3, sub {
183 if ($s =~ /^(\d+)(\.(\d+)(\.(\d+)(\.(\d+))?)?)?$/)
185 my ($a, $b, $c, $d) = ($1, $3, $5, $7);
190 if (($a > 255) || ($b > 255) || ($c > 255) || ($d > 255))
204 # remove OK wildcard prefixing, to simplify test.
205 $s =~ s/^\*\.([a-z0-9].*)$/$1/i;
207 for my $hostpart (split /\./, $s)
209 return 1005 unless $hostpart =~ /^_?[-a-z0-9]+$/i;
210 return 1006 if $hostpart =~ /^-/;
211 return 1007 if $hostpart =~ /-$/;
218 if ($s =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)\.?$/)
220 my ($a, $b, $c, $d) = ($1, $3, $5, $7);
225 if (($a > 255) || ($b > 255) || ($c > 255) || ($d > 255))
240 # Check to see if someone put an IP address in a hostname
241 # field. The motivation for this was MX records where many
242 # people expect an IP address to be a valid response, but I
243 # see no harm in enforcing it elsewhere.
244 return 1011 if $s =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\.?$/;
249 return 1005 unless /^[-[a-z0-9]+$/i;
257 my $result = validate_integer
($s, 2**32);
260 'timestamp' => [ 7, sub {
262 my $result = validate_integer
($s, 2**32);
267 my $result = validate_integer
($s, 65536);
273 # TODO : Validation needed?
282 return 1005 unless /^_?[-[a-z0-9]+$/i;
288 'mname' => [ 12, sub {
294 return 1005 unless /^[-[a-z0-9]+$/i;
300 'rname' => [ 13, sub {
305 my @parts = split /\./, $s;
306 return 1009 if @parts < 3;
310 return 1005 unless /^[-[a-z0-9]+$/i;
318 my $result = validate_integer
($s, 2**32);
323 my $result = validate_integer
($s, 2**32);
328 my $result = validate_integer
($s, 2**32);
333 my $result = validate_integer
($s, 2**32);
338 my $result = validate_integer
($s, 2**32);
343 my $result = validate_integer
($s, 65535);
345 return 1010 if ($s==2)||($s==5)||($s==6)||($s==12)||($s==15)||($s==252);
349 'rdata' => [ 20, sub {
351 # TODO : Validation needed?
355 'port' => [ 21, sub {
357 my $result = validate_integer
($s, 65536);
360 'priority' => [ 22, sub {
362 my $result = validate_integer
($s, 65536);
365 'weight' => [ 23, sub {
367 my $result = validate_integer
($s, 65536);
376 sub validate_line
($)
380 my $result = [ 0, '', '', [] ];
386 my $type = substr($s, 0, 1); $$result[2] = $type;
387 my $rest = substr($s, 1);
388 if (exists($line_type{$type}))
390 my $lt = $line_type{$type};
391 my @mask = split /\:/, $line_type{$type}->[1];
392 my @mandatory = split /\:/, $line_type{$type}->[2];
397 my @tokens = split /\:/, $rest;
401 $vals = $#mandatory if $#mandatory > $vals;
405 my $token = $tokens[$t];
406 # sanity check; should not fail
409 # silently ignore excessive fields
410 # as tinydns-data does now
412 elsif (exists($token_validator{$mask[$c]}))
414 my $validator = $token_validator{$mask[$c]};
418 # Remember fqdn for later
419 if (($c eq 0) && ($mask[0] eq 'fqdn'))
423 push @{$$result[3]}, $tmp;
426 # Remember x as fqdn IF ip is specified
427 if (($mask[$c] eq 'ip') && (length($token)))
433 if (length($ip) && ($mask[$c] eq 'x'))
437 push @{$$result[3]}, $tmp;
442 my $tv = &{$$validator[1]}($type, $token);
445 $$result[0] ^= (2 ** $$validator[0]);
447 "\npos $c; $mask[$c]; $validation_msg{$tv}";
450 elsif ($mandatory[$c] eq $mask[$c])
453 $mand = 0 if ($opt{r
}) && ($mask[$c] eq 'fqdn');
454 $mand = 0 if ($opt{R
}) && ($mask[$c] eq 'mname');
455 $mand = 0 if ($opt{R
}) && ($mask[$c] eq 'p');
456 $mand = 0 if ($opt{R
}) && ($mask[$c] eq 'rdata');
457 $mand = 0 if ($opt{i
}) && ($mask[$c] eq 'ip');
461 $$result[0] ^= (2 ** $$validator[0]);
462 $$result[1] .= "\npos $c; $mask[$c]; ".
463 $token_name{$mask[$c]}.' is mandatory';
466 # else ignore nonmandatory blanks
471 # somebody has modified program in a wrong way
473 "VALIDATOR FAILS ON TOKENS OF TYPE ".$mask[$c]." $c" ];
481 $$result[1] = "expected: ".$line_type{$type}->[1]."\n".
488 $result = [ 1, sprintf("unknown record type: #%02x",
493 $$result[1] =~ s/^\n+//;
494 $$result[1] =~ s/\n+/\n/g;
496 # result is now [ iErrno, sErrtxt, sRecordType, [ sFQDN ] ]
502 my ($fhv, $line) = @_;
505 print $fh $line."\n";
513 for my $curpat (@files)
515 for my $elem (glob $curpat)
520 return [ sort keys %ufiles ];
525 my ($vfile, $cache) = @_;
529 if (exists $cache->{file
}->{$vfile})
531 $result = $cache->{file
}->{$vfile};
535 if (open(FILER
, $vfile))
547 $cache->{file
}->{$vfile} = [ sort keys %vresult ];
548 $result = $cache->{file
}->{$vfile};
558 my ($file, $cache) = @_;
561 if (open(FILEF
, $file))
569 if (/^(\w+)\s+(.+)$/)
571 my ($key, $value) = ($1, $2);
572 my (@values, @tempvalues);
573 if ($value =~ m
#^file:(.+)#)
576 @tempvalues = @{read_file
($vfile, $cache)};
580 @tempvalues = ( $value );
583 if ($key =~ /^zonefiles?$/)
585 # This is a globbing action
588 push @values, @{funiq
($_)};
593 @values = @tempvalues;
598 $f->{lc $key}->{$_}++;
606 print STDERR
"Warning: Couldn't open filterfile: $file; $!\n";
612 sub regexped_patterns
($)
617 for my $pat (keys %{$h})
619 unless ($pat =~ /^\^.+\$$/)
623 # fix a regexp for the lazy notation
624 $pat =~ s/^[\*\.]+//;
626 $pat = '^(.*\\.)?'.$pat.'\.?$';
628 push @{$result}, $pat;
634 sub check_pattern
($$)
636 my ($pattern, $fqdn) = @_;
639 if ($fqdn =~ /$pattern/)
652 sub make_char_regexp
($)
658 for (split /\s+/, $chars)
662 $regexp .= sprintf("\\%03o", $_);
675 $regexp = "[$regexp]";
686 sub do_filterfile
($$)
688 my ($filterfile, $cache) = @_;
690 my $output = [ \
*STDERR
];
693 my $f = read_filter
($filterfile, $cache);
695 $$f{allowtype
} = (keys %{$$f{allowtype
}})[0];
696 $$f{allowtype
} .= $opt{T
};
698 my $allowtyperegex = make_char_regexp
($$f{allowtype
});
702 for my $logfile (sort keys %{$$f{extralog
}})
704 my ($fname, $fhandle);
705 # open logfiles and put them int @{$output};
706 ($fhandle, $fname) = tempfile
();
709 push @{$output}, $fhandle;
710 push @extralogs, [ $fhandle, $fname, $logfile ];
714 print STDERR
"Warning: Couldn't create tempfile for ${logfile}.\n";
719 my @zonefiles = sort keys %{$$f{zonefile
}};
722 push @zonefiles, '-';
724 for my $zonefile (@zonefiles)
727 my $filehandle = \
*STDIN
;
729 if ($zonefile ne '-')
731 $fopen = open( $filehandle, $zonefile );
735 my $temp = ($zonefile eq '-') ? '<STDIN>' : $zonefile;
736 p
$output, "File $temp";
741 while (<$filehandle>)
746 my $v = validate_line
($line);
757 $$v[1] =~ s/\n/\n /g;
758 p
$output, " line $lno; err $$v[0] $line\n ".$$v[1];
764 if ($$v[2] !~ /$allowtyperegex/)
767 if (($$v[2] ne '#') || ($opt{t
} == 1))
771 p
$output, " line $lno; err -1 $line";
772 p
$output, " record type $$v[2] disallowed; allowed: $$f{allowtype}";
777 # just check fqdn if record contains it
780 # Check $$v[3] against allowed fqdn:s:wq!
781 if (keys %{$$f{deny
}})
783 my $patterns = regexped_patterns
($$f{deny
});
786 $reason = 'default allow ^.*$';
788 for my $pat (@{$patterns})
792 if (check_pattern
($pat, $_))
795 $reason = 'deny '.$pat;
800 elsif (keys %{$$f{allow
}})
802 my $patterns = regexped_patterns
($$f{allow
});
805 $reason = 'default deny ^.*$';
807 for my $pat (@{$patterns})
811 if (check_pattern
($pat, $_))
823 if ($ok && length($line))
825 print STDOUT
"$line\n" unless $opt{q
};
833 p
$output, " line $lno; err -2; $line";
834 p
$output, " use of fqdn denied; $reason";
837 print STDOUT
"# line $lno; err -2; $line\n";
838 print STDOUT
"# use of fqdn denied; $reason\n";
844 } # while (<$filehandle>)
845 close $filehandle unless $zonefile eq '-';
846 my $plur = ($errs == 1) ? '' : 's';
847 p
$output, "$lno lines, $errs error${plur}.";
851 p
$output, "Warning: Trouble opening '$zonefile'; $!";
856 # Close all extra logfiles
857 for my $el (@extralogs)
861 if (move
($$el[1], $$el[2]))
863 print STDERR
"Copy of logfile portion to $$el[2]\n";
867 print STDERR
"Warning: Couldn't rename tempfile to $$el[2].\n";
873 print STDERR
"Warning: Couldn't close tempfile for $$el[2].\n";
885 my $files = funiq
(@ARGV);
889 valtz
$VERSION - validate tinydns-data files
892 $0 [-r
] [-R
] [-i
] tinydns-file1
[tinydns-file2
...]
894 $0 [-HiIqrRt
] [-T types
] -f tinydns-file1
[tinydns-file2
...]
896 $0 valtz
[-fHiIqrRt
] [-T types
] -F filter-file1
[filter-file2
...]
899 -h
print usage information
900 -f filter invalid lines
(filter mode
)
901 -F filter using configuration files
(advanced filter mode
)
902 -r allow
"fqdn" fields to be empty
903 -R allow
"mname" and "p" fields to be empty
904 -i allow
"ip" fields to be empty
905 -I include rejected lines as comments
(filtering only
)
906 -q don
't print valid lines to standard out (filtering only)
907 -t don't ignore comment lines
(filtering only
)
908 -T
<types
> allow additional record types
(advanced filtering only
)
910 Errors are generally printed to standard error
, and the
exit code
911 shall reflect the presense of both usage
and validation errors
. See
912 the man page
for details
.
917 if ($opt{h
} || $opt{H
} || $opt{'?'}) {
920 # If they asked for help, ignore whatever else they may have done
925 if (@{$files} == 0) {
934 for my $file (@{$files})
936 my $result = do_filterfile
($file, $cache);
943 my $output = [ \
*STDERR
];
945 for my $zonefile (sort @{$files})
947 my $filehandle = \
*STDIN
;
949 if ($zonefile ne '-')
951 $fopen = open( $filehandle, $zonefile );
958 while (<$filehandle>)
963 my $v = validate_line
($line);
968 my $temp = ($zonefile eq '-') ? '<STDIN>' : $zonefile;
969 p
$output, "File $temp" unless $errs;
972 $$v[1] =~ s/\n/\n /g;
973 p
$output, " line $lno; err $$v[0] $line\n ".$$v[1];
976 print STDOUT
"# line $lno; err $$v[0] $line
977 print STDOUT "# $$v[1]; \n";
982 # Echo NON-ERRORS to STDOUT
985 print STDOUT
"$line\n" unless $opt{q
};
990 close $filehandle unless $zonefile eq '-';
994 p
$output, "Error: Trouble opening '$zonefile'; $!";
999 if ($verrs_total + $perrs_total)
1001 my $exitcode = $verrs_total > 0 ? 1 : 0;
1002 $exitcode += $perrs_total > 0 ? 2 : 0;