3 # $Id: valtz,v 0.7 2003/07/10 16:39:30 magnus Exp $
7 # Copyright (c) 2003, Magnus Bodin, <magnus@bodin.org>, http://x42.com
10 # Redistribution and use in source and binary forms, with or without
11 # modification, are permitted provided that the following conditions are
14 # Redistributions of source code must retain the above copyright notice,
15 # this list of conditions and the following disclaimer.
17 # Redistributions in binary form must reproduce the above copyright
18 # notice, this list of conditions and the following disclaimer in the
19 # documentation and/or other materials provided with the distribution.
21 # Neither the name of Magnus Bodin, x42.com nor the names of its
22 # contributors may be used to endorse or promote products derived from
23 # this software without specific prior written permission.
25 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
26 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
27 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
28 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
29 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
30 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
31 # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
32 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
33 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
34 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
35 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42 use File
::Temp qw
/ tempfile /;
43 use File
::Copy qw
/ move /;
51 getopts
('?fFhHiIqrRtT:', \
%opt);
57 # "Permission" errors with respect to what record types are allowed
62 # global location registry
63 # (reset for every zone file)
66 # NOTE : DO NOT CHANGE the id numbers
67 my %validation_msg = (
68 1001 => 'badly formed; should be just two ASCII letters',
69 1002 => 'location is not previously defined in a %-line',
70 1003 => 'invalid syntax',
71 1004 => 'invalid syntax of integer',
72 1005 => 'parts must only contain ASCII letters, digits and - characters',
73 1006 => 'parts must not begin with the - character',
74 1007 => 'parts must not end with the - character',
75 1008 => 'integer out of bounds',
76 1009 => 'must have at least three labels to be valid as mail address',
77 1010 => 'must not be 2(NS), 5(CNAME), 6(SOA), 12(PTR), 15(MX) or 252(AXFR)',
78 1011 => 'IP address found where hostname expected'
81 # NOTE : ONLY translate the right-hand part
84 'ipprefix' => 'IP prefix',
85 'fqdn' => 'Domain name',
89 'timestamp' => 'Timestamp',
94 'mname' => 'Master name',
95 'rname' => 'Role name',
96 'ser' => 'Serial number',
97 'ref' => 'Refresh time',
98 'ret' => 'Retry time',
99 'exp' => 'Expire time',
100 'min' => 'Minimum time',
101 'n' => 'Record type number',
102 'rdata' => 'Resource data',
104 'priority' => 'Priority',
116 '-' => ':disabled +',
125 # NOTE : This should NOT be translated!
127 '%' => [ ':location', 'lo:ipprefix', 'lo' ],
128 '.' => [ 'NS(+A?)', 'fqdn:ip:x:ttl:timestamp:lo', 'fqdn' ],
129 '&' => [ 'NS(+A?)', 'fqdn:ip:x:ttl:timestamp:lo', 'fqdn' ],
130 '=' => [ 'A+PTR', 'fqdn:ip:ttl:timestamp:lo', 'fqdn:ip' ],
131 '+' => [ 'A', 'fqdn:ip:ttl:timestamp:lo', 'fqdn:ip' ],
132 '@' => [ 'MX(+A?)', 'fqdn:ip:x:dist:ttl:timestamp:lo', 'fqdn' ],
133 '#' => [ ':comment', '', '' ],
134 '-' => [ ':disabled +', '', '' ],
135 "'" => [ 'TXT', 'fqdn:s:ttl:timestamp:lo', 'fqdn:s' ],
136 '^' => [ 'PTR', 'fqdn:p:ttl:timestamp:lo', 'fqdn:p' ],
137 'C' => [ 'CNAME', 'fqdn:p:ttl:timestamp:lo', 'fqdn:p' ],
138 'S' => [ 'SRV', 'fqdn:ip:x:port:weight:priority:ttl:timestamp:lo',
140 'Z' => [ 'SOA', 'fqdn:mname:rname:ser:ref:ret:exp:min:ttl:timestamp:lo',
141 'fqdn:mname:rname' ],
142 ':' => [ 'GENERIC', 'fqdn:n:rdata:ttl:timestamp:lo', 'fqdn:n:rdata' ]
148 my ($s, $boundary) = @_;
155 $result = 1008 if $boundary && ($i >= $boundary);
166 # NOTE : No translation here!
167 my %token_validator = (
171 return 1001 unless $s =~ /^[a-z][a-z]$/i;
178 return 1002 unless exists($loreg{$s});
182 'ipprefix' => [ 3, sub {
185 if ($s =~ /^(\d+)(\.(\d+)(\.(\d+)(\.(\d+))?)?)?$/)
187 my ($a, $b, $c, $d) = ($1, $3, $5, $7);
192 if (($a > 255) || ($b > 255) || ($c > 255) || ($d > 255))
206 # remove OK wildcard prefixing, to simplify test.
207 $s =~ s/^\*\.([a-z0-9].*)$/$1/i;
209 for my $hostpart (split /\./, $s)
211 return 1005 unless $hostpart =~ /^_?[-a-z0-9]+$/i;
212 return 1006 if $hostpart =~ /^-/;
213 return 1007 if $hostpart =~ /-$/;
220 if ($s =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)\.?$/)
222 my ($a, $b, $c, $d) = ($1, $3, $5, $7);
227 if (($a > 255) || ($b > 255) || ($c > 255) || ($d > 255))
242 # Check to see if someone put an IP address in a hostname
243 # field. The motivation for this was MX records where many
244 # people expect an IP address to be a valid response, but I
245 # see no harm in enforcing it elsewhere.
246 return 1011 if $s =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\.?$/;
251 return 1005 unless /^[-[a-z0-9]+$/i;
259 my $result = validate_integer
($s, 2**32);
262 'timestamp' => [ 7, sub {
264 my $result = validate_integer
($s, 2**32);
269 my $result = validate_integer
($s, 65536);
275 # TODO : Validation needed?
284 return 1005 unless /^_?[-[a-z0-9]+$/i;
290 'mname' => [ 12, sub {
296 return 1005 unless /^[-[a-z0-9]+$/i;
302 'rname' => [ 13, sub {
307 my @parts = split /\./, $s;
308 return 1009 if @parts < 3;
312 return 1005 unless /^[-[a-z0-9]+$/i;
320 my $result = validate_integer
($s, 2**32);
325 my $result = validate_integer
($s, 2**32);
330 my $result = validate_integer
($s, 2**32);
335 my $result = validate_integer
($s, 2**32);
340 my $result = validate_integer
($s, 2**32);
345 my $result = validate_integer
($s, 65535);
347 return 1010 if ($s==2)||($s==5)||($s==6)||($s==12)||($s==15)||($s==252);
351 'rdata' => [ 20, sub {
353 # TODO : Validation needed?
357 'port' => [ 21, sub {
359 my $result = validate_integer
($s, 65536);
362 'priority' => [ 22, sub {
364 my $result = validate_integer
($s, 65536);
367 'weight' => [ 23, sub {
369 my $result = validate_integer
($s, 65536);
378 sub validate_line
($)
382 my $result = [ 0, '', '', [] ];
388 my $type = substr($s, 0, 1); $$result[2] = $type;
389 my $rest = substr($s, 1);
390 if (exists($line_type{$type}))
392 my $lt = $line_type{$type};
393 my @mask = split /\:/, $line_type{$type}->[1];
394 my @mandatory = split /\:/, $line_type{$type}->[2];
399 my @tokens = split /\:/, $rest;
403 $vals = $#mandatory if $#mandatory > $vals;
407 my $token = $tokens[$t];
408 # sanity check; should not fail
411 # silently ignore excessive fields
412 # as tinydns-data does now
414 elsif (exists($token_validator{$mask[$c]}))
416 my $validator = $token_validator{$mask[$c]};
420 # Remember fqdn for later
421 if (($c eq 0) && ($mask[0] eq 'fqdn'))
425 push @{$$result[3]}, $tmp;
428 # Remember x as fqdn IF ip is specified
429 if (($mask[$c] eq 'ip') && (length($token)))
435 if (length($ip) && ($mask[$c] eq 'x'))
439 push @{$$result[3]}, $tmp;
444 my $tv = &{$$validator[1]}($type, $token);
447 $$result[0] ^= (2 ** $$validator[0]);
449 "\npos $c; $mask[$c]; $validation_msg{$tv}";
452 elsif ($mandatory[$c] eq $mask[$c])
455 $mand = 0 if ($opt{r
}) && ($mask[$c] eq 'fqdn');
456 $mand = 0 if ($opt{R
}) && ($mask[$c] eq 'mname');
457 $mand = 0 if ($opt{R
}) && ($mask[$c] eq 'p');
458 $mand = 0 if ($opt{R
}) && ($mask[$c] eq 'rdata');
459 $mand = 0 if ($opt{i
}) && ($mask[$c] eq 'ip');
463 $$result[0] ^= (2 ** $$validator[0]);
464 $$result[1] .= "\npos $c; $mask[$c]; ".
465 $token_name{$mask[$c]}.' is mandatory';
468 # else ignore nonmandatory blanks
473 # somebody has modified program in a wrong way
475 "VALIDATOR FAILS ON TOKENS OF TYPE ".$mask[$c]." $c" ];
483 $$result[1] = "expected: ".$line_type{$type}->[1]."\n".
490 $result = [ 1, sprintf("unknown record type: #%02x",
495 $$result[1] =~ s/^\n+//;
496 $$result[1] =~ s/\n+/\n/g;
498 # result is now [ iErrno, sErrtxt, sRecordType, [ sFQDN ] ]
504 my ($fhv, $line) = @_;
507 print $fh $line."\n";
515 for my $curpat (@files)
517 for my $elem (glob $curpat)
522 return [ sort keys %ufiles ];
527 my ($vfile, $cache) = @_;
531 if (exists $cache->{file
}->{$vfile})
533 $result = $cache->{file
}->{$vfile};
537 if (open(FILER
, $vfile))
549 $cache->{file
}->{$vfile} = [ sort keys %vresult ];
550 $result = $cache->{file
}->{$vfile};
560 my ($file, $cache) = @_;
563 if (open(FILEF
, $file))
571 if (/^(\w+)\s+(.+)$/)
573 my ($key, $value) = ($1, $2);
574 my (@values, @tempvalues);
575 if ($value =~ m
#^file:(.+)#)
578 @tempvalues = @{read_file
($vfile, $cache)};
582 @tempvalues = ( $value );
585 if ($key =~ /^zonefiles?$/)
587 # This is a globbing action
590 push @values, @{funiq
($_)};
595 @values = @tempvalues;
600 $f->{lc $key}->{$_}++;
608 print STDERR
"Warning: Couldn't open filterfile: $file; $!\n";
614 sub regexped_patterns
($)
619 for my $pat (keys %{$h})
621 unless ($pat =~ /^\^.+\$$/)
625 # fix a regexp for the lazy notation
626 $pat =~ s/^[\*\.]+//;
628 $pat = '^(.*\\.)?'.$pat.'\.?$';
630 push @{$result}, $pat;
636 sub check_pattern
($$)
638 my ($pattern, $fqdn) = @_;
641 if ($fqdn =~ /$pattern/)
654 sub make_char_regexp
($)
660 for (split /\s+/, $chars)
664 $regexp .= sprintf("\\%03o", $_);
677 $regexp = "[$regexp]";
688 sub do_filterfile
($$)
690 my ($filterfile, $cache) = @_;
692 my $output = [ \
*STDERR
];
695 my $f = read_filter
($filterfile, $cache);
697 $$f{allowtype
} = (keys %{$$f{allowtype
}})[0];
698 $$f{allowtype
} .= $opt{T
};
700 my $allowtyperegex = make_char_regexp
($$f{allowtype
});
704 for my $logfile (sort keys %{$$f{extralog
}})
706 my ($fname, $fhandle);
707 # open logfiles and put them int @{$output};
708 ($fhandle, $fname) = tempfile
();
711 push @{$output}, $fhandle;
712 push @extralogs, [ $fhandle, $fname, $logfile ];
716 print STDERR
"Warning: Couldn't create tempfile for ${logfile}.\n";
721 my @zonefiles = sort keys %{$$f{zonefile
}};
724 push @zonefiles, '-';
726 for my $zonefile (@zonefiles)
729 my $filehandle = \
*STDIN
;
731 if ($zonefile ne '-')
733 $fopen = open( $filehandle, $zonefile );
737 my $temp = ($zonefile eq '-') ? '<STDIN>' : $zonefile;
738 p
$output, "File $temp";
743 while (<$filehandle>)
748 my $v = validate_line
($line);
759 $$v[1] =~ s/\n/\n /g;
760 p
$output, " line $lno; err $$v[0] $line\n ".$$v[1];
766 if ($$v[2] !~ /$allowtyperegex/)
769 if (($$v[2] ne '#') || ($opt{t
} == 1))
773 p
$output, " line $lno; err -1 $line";
774 p
$output, " record type $$v[2] disallowed; allowed: $$f{allowtype}";
779 # just check fqdn if record contains it
782 # Check $$v[3] against allowed fqdn:s:wq!
783 if (keys %{$$f{deny
}})
785 my $patterns = regexped_patterns
($$f{deny
});
788 $reason = 'default allow ^.*$';
790 for my $pat (@{$patterns})
794 if (check_pattern
($pat, $_))
797 $reason = 'deny '.$pat;
802 elsif (keys %{$$f{allow
}})
804 my $patterns = regexped_patterns
($$f{allow
});
807 $reason = 'default deny ^.*$';
809 for my $pat (@{$patterns})
813 if (check_pattern
($pat, $_))
825 if ($ok && length($line))
827 print STDOUT
"$line\n" unless $opt{q
};
835 p
$output, " line $lno; err -2; $line";
836 p
$output, " use of fqdn denied; $reason";
839 print STDOUT
"# line $lno; err -2; $line\n";
840 print STDOUT
"# use of fqdn denied; $reason\n";
846 } # while (<$filehandle>)
847 close $filehandle unless $zonefile eq '-';
848 my $plur = ($errs == 1) ? '' : 's';
849 p
$output, "$lno lines, $errs error${plur}.";
853 p
$output, "Warning: Trouble opening '$zonefile'; $!";
858 # Close all extra logfiles
859 for my $el (@extralogs)
863 if (move
($$el[1], $$el[2]))
865 print STDERR
"Copy of logfile portion to $$el[2]\n";
869 print STDERR
"Warning: Couldn't rename tempfile to $$el[2].\n";
875 print STDERR
"Warning: Couldn't close tempfile for $$el[2].\n";
887 my $files = funiq
(@ARGV);
891 valtz
$VERSION - validate tinydns-data files
894 $0 [-r
] [-R
] [-i
] tinydns-file1
[tinydns-file2
...]
896 $0 [-HiIqrRt
] [-T types
] -f tinydns-file1
[tinydns-file2
...]
898 $0 valtz
[-fHiIqrRt
] [-T types
] -F filter-file1
[filter-file2
...]
901 -h
print usage information
902 -f filter invalid lines
(filter mode
)
903 -F filter using configuration files
(advanced filter mode
)
904 -r allow
"fqdn" fields to be empty
905 -R allow
"mname" and "p" fields to be empty
906 -i allow
"ip" fields to be empty
907 -I include rejected lines as comments
(filtering only
)
908 -q don
't print valid lines to standard out (filtering only)
909 -t don't ignore comment lines
(filtering only
)
910 -T
<types
> allow additional record types
(advanced filtering only
)
912 Errors are generally printed to standard error
, and the
exit code
913 shall reflect the presense of both usage
and validation errors
. See
914 the man page
for details
.
919 if ($opt{h
} || $opt{H
} || $opt{'?'}) {
922 # If they asked for help, ignore whatever else they may have done
927 if (@{$files} == 0) {
936 for my $file (@{$files})
938 my $result = do_filterfile
($file, $cache);
945 my $output = [ \
*STDERR
];
947 for my $zonefile (sort @{$files})
949 my $filehandle = \
*STDIN
;
951 if ($zonefile ne '-')
953 $fopen = open( $filehandle, $zonefile );
960 while (<$filehandle>)
965 my $v = validate_line
($line);
970 my $temp = ($zonefile eq '-') ? '<STDIN>' : $zonefile;
971 p
$output, "File $temp" unless $errs;
974 $$v[1] =~ s/\n/\n /g;
975 p
$output, " line $lno; err $$v[0] $line\n ".$$v[1];
978 print STDOUT
"# line $lno; err $$v[0] $line
979 print STDOUT "# $$v[1]; \n";
984 # Echo NON-ERRORS to STDOUT
987 print STDOUT
"$line\n" unless $opt{q
};
992 close $filehandle unless $zonefile eq '-';
996 p
$output, "Error: Trouble opening '$zonefile'; $!";
1001 if ($verrs_total + $perrs_total)
1003 my $exitcode = $verrs_total > 0 ? 1 : 0;
1004 $exitcode += $perrs_total > 0 ? 2 : 0;