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 /;
46 my $VERSION = $1 if '$Revision: 0.7 $' =~ /(\d+\.\d+)/;
47 my $COPYRIGHT = '; (C) 2003 Magnus Bodin, http://x42.com/software/';
51 getopts
('?fFhHiIqrRstT:x', \
%opt);
54 my $FILESUFFIXREGEXP = '('.join('|', qw
/
55 ,v
~ .bak
.log .old
.swp
.tmp
63 # global location registry
64 # (reset for every zone file)
67 # NOTE : DO NOT CHANGE the id numbers
68 my %validation_msg = (
69 1001 => 'badly formed; should be just two ASCII letters',
70 1002 => 'location is not previously defined in a %-line',
71 1003 => 'invalid syntax',
72 1004 => 'invalid syntax of integer',
73 1005 => 'parts must only contain ASCII letters, digits and - characters',
74 1006 => 'parts must not begin with the - character',
75 1007 => 'parts must not end with the - character',
76 1008 => 'integer out of bounds',
77 1009 => 'must have at least three labels to be valid as mail address',
78 1010 => 'must not be 2(NS), 5(CNAME), 6(SOA), 12(PTR), 15(MX) or 252(AXFR)',
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))
244 return 1005 unless /^[-[a-z0-9]+$/i;
252 my $result = validate_integer
($s, 2**32);
255 'timestamp' => [ 7, sub {
257 my $result = validate_integer
($s, 2**32);
262 my $result = validate_integer
($s, 65536);
268 # TODO : Validation needed?
277 return 1005 unless /^_?[-[a-z0-9]+$/i;
283 'mname' => [ 12, sub {
289 return 1005 unless /^[-[a-z0-9]+$/i;
295 'rname' => [ 13, sub {
300 my @parts = split /\./, $s;
301 return 1009 if @parts < 3;
305 return 1005 unless /^[-[a-z0-9]+$/i;
313 my $result = validate_integer
($s, 2**32);
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, 65535);
340 return 1010 if ($s==2)||($s==5)||($s==6)||($s==12)||($s==15)||($s==252);
344 'rdata' => [ 20, sub {
346 # TODO : Validation needed?
350 'port' => [ 21, sub {
352 my $result = validate_integer
($s, 65536);
355 'priority' => [ 22, sub {
357 my $result = validate_integer
($s, 65536);
360 'weight' => [ 23, sub {
362 my $result = validate_integer
($s, 65536);
371 sub validate_line
($)
375 my $result = [ 0, '', '', [] ];
381 my $type = substr($s, 0, 1); $$result[2] = $type;
382 my $rest = substr($s, 1);
383 if (exists($line_type{$type}))
385 my $lt = $line_type{$type};
386 my @mask = split /\:/, $line_type{$type}->[1];
387 my @mandatory = split /\:/, $line_type{$type}->[2];
392 my @tokens = split /\:/, $rest;
396 $vals = $#mandatory if $#mandatory > $vals;
400 my $token = $tokens[$t];
401 # sanity check; should not fail
404 # silently ignore excessive fields
405 # as tinydns-data does now
407 elsif (exists($token_validator{$mask[$c]}))
409 my $validator = $token_validator{$mask[$c]};
413 # Remember fqdn for later
414 if (($c eq 0) && ($mask[0] eq 'fqdn'))
418 push @{$$result[3]}, $tmp;
421 # Remember x as fqdn IF ip is specified
422 if (($mask[$c] eq 'ip') && (length($token)))
428 if (length($ip) && ($mask[$c] eq 'x'))
432 push @{$$result[3]}, $tmp;
437 my $tv = &{$$validator[1]}($type, $token);
440 $$result[0] ^= (2 ** $$validator[0]);
442 "\npos $c; $mask[$c]; $validation_msg{$tv}";
445 elsif ($mandatory[$c] eq $mask[$c])
448 $mand = 0 if ($opt{r
}) && ($mask[$c] eq 'fqdn');
449 $mand = 0 if ($opt{R
}) && ($mask[$c] eq 'mname');
450 $mand = 0 if ($opt{R
}) && ($mask[$c] eq 'p');
451 $mand = 0 if ($opt{R
}) && ($mask[$c] eq 'rdata');
452 $mand = 0 if ($opt{i
}) && ($mask[$c] eq 'ip');
456 $$result[0] ^= (2 ** $$validator[0]);
457 $$result[1] .= "\npos $c; $mask[$c]; ".
458 $token_name{$mask[$c]}.' is mandatory';
461 # else ignore nonmandatory blanks
466 # somebody has modified program in a wrong way
468 "VALIDATOR FAILS ON TOKENS OF TYPE ".$mask[$c]." $c" ];
476 $$result[1] = "expected: ".$line_type{$type}->[1]."\n".
483 $result = [ 1, sprintf("unknown record type: #%02x",
488 $$result[1] =~ s/^\n+//;
489 $$result[1] =~ s/\n+/\n/g;
491 # result is now [ iErrno, sErrtxt, sRecordType, [ sFQDN ] ]
497 my ($fhv, $line) = @_;
500 print $fh $line."\n";
508 for my $curpat (@files)
510 for my $elem (glob $curpat)
515 return [ sort keys %ufiles ];
520 my ($vfile, $cache) = @_;
524 if (exists $cache->{file
}->{$vfile})
526 $result = $cache->{file
}->{$vfile};
530 if (open(FILER
, $vfile))
542 $cache->{file
}->{$vfile} = [ sort keys %vresult ];
543 $result = $cache->{file
}->{$vfile};
553 my ($file, $cache) = @_;
556 if (open(FILEF
, $file))
564 if (/^(\w+)\s+(.+)$/)
566 my ($key, $value) = ($1, $2);
567 my (@values, @tempvalues);
568 if ($value =~ m
#^file:(.+)#)
571 @tempvalues = @{read_file
($vfile, $cache)};
575 @tempvalues = ( $value );
578 if ($key =~ /^zonefiles?$/)
580 # This is a globbing action
583 push @values, @{funiq
($_)};
588 @values = @tempvalues;
593 $f->{lc $key}->{$_}++;
601 print STDERR
"Warning: Couldn't open filterfile: $file; $!\n";
607 sub regexped_patterns
($)
612 for my $pat (keys %{$h})
614 unless ($pat =~ /^\^.+\$$/)
618 # fix a regexp for the lazy notation
619 $pat =~ s/^[\*\.]+//;
621 $pat = '^(.*\\.)?'.$pat.'\.?$';
623 push @{$result}, $pat;
629 sub check_pattern
($$)
631 my ($pattern, $fqdn) = @_;
634 if ($fqdn =~ /$pattern/)
647 sub make_char_regexp
($)
653 for (split /\s+/, $chars)
657 $regexp .= sprintf("\\%03o", $_);
670 $regexp = "[$regexp]";
681 sub do_filterfile
($$)
683 my ($filterfile, $cache) = @_;
685 my $output = [ \
*STDERR
];
688 my $f = read_filter
($filterfile, $cache);
690 $$f{allowtype
} = (keys %{$$f{allowtype
}})[0];
691 $$f{allowtype
} .= $opt{T
};
693 my $allowtyperegex = make_char_regexp
($$f{allowtype
});
697 for my $logfile (sort keys %{$$f{extralog
}})
699 my ($fname, $fhandle);
700 # open logfiles and put them int @{$output};
701 ($fhandle, $fname) = tempfile
();
704 push @{$output}, $fhandle;
705 push @extralogs, [ $fhandle, $fname, $logfile ];
709 print STDERR
"Warning: Couldn't create tempfile for ${logfile}.\n";
714 my @zonefiles = sort keys %{$$f{zonefile
}};
717 push @zonefiles, '-';
719 for my $zonefile (@zonefiles)
723 next if $zonefile =~ /$FILESUFFIXREGEXP/i;
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);
888 if ($opt{h
} || $opt{H
} || $opt{'?'})
891 valtz
$VERSION, $COPYRIGHT
892 validates tinydns-data zone files
894 $0 [-hfFqrRiItTx
] <file
(s
)>
899 -f filter
(don
't just validate) file and output accepted lines to STDOUT.
902 -F treat files as filter configuration files for more advanced filtering.
903 These filterfiles one or several of the following filter directives:
905 zonefile <zonefilepath>
906 zonefile file:<path to textfile including zonefilepaths>
907 Defines the file(s) to be filtered. Can be a globbed value, like
908 /var/zones/external/*
911 Defines an extra logfile that the STDERR output will be copied for
912 this specific filterfile. Useful if you have a lot of filterfiles
913 and want to separate the logs.
916 deny file:<path to <zonepatternfile>
917 Defines a zonepattern to explicitly DENY after implicitly allowing all.
918 (cannot be combined with allow)
921 allow file:<path to <zonepatternfile>
922 Defines a zonepattern to explicitly ALLOW after implicitly denying all.
924 allowtype <recordtype character(s)>
925 Explicitly sets the allowed recordtypes. Note that even comments
926 has to be allowed (but these will not result in errors unless -t)
927 to be copied to the output.
929 Multiple zonefile, allow- and deny-lines are allowed, but also the
930 alternative file:-line that points to a textfile containing one
934 -r allows fqdn to be empty thus denoting the root.
935 This is also allowed per default when doing implict allow - see deny,
936 or when specifying 'allow
.', i.e. explictly allowing root as such.
937 (cannot be combined with deny)
940 -R relaxes the validation and allows empty mname and p-fields.xi
941 This is probably not very useful.
944 -i allows the ip-fields to be empty as well. These will then not generate any
948 -I Include rejected lines as comments in output (valid when filtering).
951 -q Do not echo valid lines to STDOUT.
953 -s DO NOT ignore files ending with ,v ~ .bak .log .old .swp .tmp
954 which is done per default.
957 -t Give error even on #comment-lines when they are not allowed.
958 (These errors are silently ignored per default)
962 A commandline way to explicitly set the allowed recordtypes.
963 This is _concatenated_ to the allowtype-allowed recordtypes.
965 -x Exit with non-null exit code on errors; i.e. make errors detectable by
966 e.g. shell scripts; 1 = validation error, 2 = permission error,
967 3 = combination of 1 and 2.
971 All errors in the zonefiles are sent to STDERR.
976 Example; simple filter-use;
977 valtz -f /etc/zones/zone-* \
978 >/etc/tinydns/data.filtered \
979 2>/var/log/tinydns/valtz.log
981 Example; filterfile use;
982 valtz -F /etc/zones/filter/zones-otto \
983 >/etc/tinydns/data.otto \
984 2>/var/log/tinydns/valtz.log
987 Example filterfile for using as import from primary (as above):
988 zonefile /var/zones/external/otto/zone-*
991 extralog /var/log/tinydns/external-otto.log
993 Example #2, strict filter for a certain user editing just A-records
995 zonefile /home/felix/zones/zone-fl3x-net
998 extralog /var/log/tinydns/fl3x-net.log
1000 Example #3, export filter to secondary
1002 zonefile /var/zones/primary/zone-*
1003 # just allow OUR zones to be exported, not to annoy secondary partner
1004 allow file:/var/zones/primary-zones.txt
1005 # don't allow any other types than this
; e
.g
. comments won
't be exported
1007 extralog /var/log/tinydns/primary-export.log
1012 elsif (@{$files} == 0)
1015 valtz $VERSION, $COPYRIGHT
1016 validates tinydns-data zone files
1019 $0 [-qrRix] <zonefiles>
1021 $0 -f[qrRiItTx] <zonefiles>
1022 Extensive filtering:
1023 $0 -F[qrRiItTx] <zonefiles>
1025 More help and information about options:
1036 $cache->{file} = {};
1037 for my $file (@{$files})
1039 my $result = do_filterfile($file, $cache);
1046 my $output = [ \*STDERR ];
1048 for my $zonefile (sort @{$files})
1052 next if $zonefile =~ /$FILESUFFIXREGEXP/i;
1055 my $filehandle = \*STDIN;
1057 if ($zonefile ne '-')
1059 $fopen = open( $filehandle, $zonefile );
1066 while (<$filehandle>)
1071 my $v = validate_line($line);
1076 my $temp = ($zonefile eq '-') ? '<STDIN
>' : $zonefile;
1077 p $output, "File $temp" unless $errs;
1080 $$v[1] =~ s/\n/\n /g;
1081 p $output, " line $lno; err $$v[0] $line\n ".$$v[1];
1084 print STDOUT "# line $lno; err $$v[0] $line
1085 print STDOUT "# $$v[1]; \n";
1090 # Echo NON-ERRORS to STDOUT
1093 print STDOUT "$line\n" unless $opt{q};
1098 close $filehandle unless $zonefile eq '-';
1102 p $output, "Error: Trouble opening '$zonefile'; $!";
1107 if ($opt{x} && ($verrs_total + $perrs_total))
1109 my $exitcode = $verrs_total > 0 ? 1 : 0;
1110 $exitcode += $perrs_total > 0 ? 2 : 0;