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',
113 '-' => ':disabled +',
121 # NOTE : This should NOT be translated!
123 '%' => [ ':location', 'lo:ipprefix', 'lo' ],
124 '.' => [ 'NS(+A?)', 'fqdn:ip:x:ttl:timestamp:lo', 'fqdn' ],
125 '&' => [ 'NS(+A?)', 'fqdn:ip:x:ttl:timestamp:lo', 'fqdn' ],
126 '=' => [ 'A+PTR', 'fqdn:ip:ttl:timestamp:lo', 'fqdn:ip' ],
127 '+' => [ 'A', 'fqdn:ip:ttl:timestamp:lo', 'fqdn:ip' ],
128 '@' => [ 'MX(+A?)', 'fqdn:ip:x:dist:ttl:timestamp:lo', 'fqdn' ],
129 '#' => [ ':comment', '', '' ],
130 '-' => [ ':disabled +', '', '' ],
131 "'" => [ 'TXT', 'fqdn:s:ttl:timestamp:lo', 'fqdn:s' ],
132 '^' => [ 'PTR', 'fqdn:p:ttl:timestamp:lo', 'fqdn:p' ],
133 'C' => [ 'CNAME', 'fqdn:p:ttl:timestamp:lo', 'fqdn:p' ],
134 'Z' => [ 'SOA', 'fqdn:mname:rname:ser:ref:ret:exp:min:ttl:timestamp:lo',
135 'fqdn:mname:rname' ],
136 ':' => [ 'GENERIC', 'fqdn:n:rdata:ttl:timestamp:lo', 'fqdn:n:rdata' ]
142 my ($s, $boundary) = @_;
149 $result = 1008 if $boundary && ($i >= $boundary);
160 # NOTE : No translation here!
161 my %token_validator = (
165 return 1001 unless $s =~ /^[a-z][a-z]$/i;
172 return 1002 unless exists($loreg{$s});
176 'ipprefix' => [ 3, sub {
179 if ($s =~ /^(\d+)(\.(\d+)(\.(\d+)(\.(\d+))?)?)?$/)
181 my ($a, $b, $c, $d) = ($1, $3, $5, $7);
186 if (($a > 255) || ($b > 255) || ($c > 255) || ($d > 255))
200 # remove OK wildcard prefixing, to simplify test.
201 $s =~ s/^\*\.([a-z0-9].*)$/$1/i;
203 for my $hostpart (split /\./, $s)
205 return 1005 unless $hostpart =~ /^[-a-z0-9]+$/i;
206 return 1006 if $hostpart =~ /^-/;
207 return 1007 if $hostpart =~ /-$/;
214 if ($s =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)\.?$/)
216 my ($a, $b, $c, $d) = ($1, $3, $5, $7);
221 if (($a > 255) || ($b > 255) || ($c > 255) || ($d > 255))
238 return 1005 unless /^[-[a-z0-9]+$/i;
246 my $result = validate_integer
($s, 2**32);
249 'timestamp' => [ 7, sub {
251 my $result = validate_integer
($s, 2**32);
256 my $result = validate_integer
($s, 65536);
262 # TODO : Validation needed?
271 return 1005 unless /^[-[a-z0-9]+$/i;
277 'mname' => [ 12, sub {
283 return 1005 unless /^[-[a-z0-9]+$/i;
289 'rname' => [ 13, sub {
294 my @parts = split /\./, $s;
295 return 1009 if @parts < 3;
299 return 1005 unless /^[-[a-z0-9]+$/i;
307 my $result = validate_integer
($s, 2**32);
312 my $result = validate_integer
($s, 2**32);
317 my $result = validate_integer
($s, 2**32);
322 my $result = validate_integer
($s, 2**32);
327 my $result = validate_integer
($s, 2**32);
332 my $result = validate_integer
($s, 65535);
334 return 1010 if ($s==2)||($s==5)||($s==6)||($s==12)||($s==15)||($s==252);
338 'rdata' => [ 20, sub {
340 # TODO : Validation needed?
350 sub validate_line
($)
354 my $result = [ 0, '', '', [] ];
360 my $type = substr($s, 0, 1); $$result[2] = $type;
361 my $rest = substr($s, 1);
362 if (exists($line_type{$type}))
364 my $lt = $line_type{$type};
365 my @mask = split /\:/, $line_type{$type}->[1];
366 my @mandatory = split /\:/, $line_type{$type}->[2];
371 my @tokens = split /\:/, $rest;
375 $vals = $#mandatory if $#mandatory > $vals;
379 my $token = $tokens[$t];
380 # sanity check; should not fail
383 # silently ignore excessive fields
384 # as tinydns-data does now
386 elsif (exists($token_validator{$mask[$c]}))
388 my $validator = $token_validator{$mask[$c]};
392 # Remember fqdn for later
393 if (($c eq 0) && ($mask[0] eq 'fqdn'))
397 push @{$$result[3]}, $tmp;
400 # Remember x as fqdn IF ip is specified
401 if (($mask[$c] eq 'ip') && (length($token)))
407 if (length($ip) && ($mask[$c] eq 'x'))
411 push @{$$result[3]}, $tmp;
416 my $tv = &{$$validator[1]}($type, $token);
419 $$result[0] ^= (2 ** $$validator[0]);
421 "\npos $c; $mask[$c]; $validation_msg{$tv}";
424 elsif ($mandatory[$c] eq $mask[$c])
427 $mand = 0 if ($opt{r
}) && ($mask[$c] eq 'fqdn');
428 $mand = 0 if ($opt{R
}) && ($mask[$c] eq 'mname');
429 $mand = 0 if ($opt{R
}) && ($mask[$c] eq 'p');
430 $mand = 0 if ($opt{R
}) && ($mask[$c] eq 'rdata');
431 $mand = 0 if ($opt{i
}) && ($mask[$c] eq 'ip');
435 $$result[0] ^= (2 ** $$validator[0]);
436 $$result[1] .= "\npos $c; $mask[$c]; ".
437 $token_name{$mask[$c]}.' is mandatory';
440 # else ignore nonmandatory blanks
445 # somebody has modified program in a wrong way
447 "VALIDATOR FAILS ON TOKENS OF TYPE ".$mask[$c]." $c" ];
455 $$result[1] = "expected: ".$line_type{$type}->[1]."\n".
462 $result = [ 1, sprintf("unknown record type: #%02x",
467 $$result[1] =~ s/^\n+//;
468 $$result[1] =~ s/\n+/\n/g;
470 # result is now [ iErrno, sErrtxt, sRecordType, [ sFQDN ] ]
476 my ($fhv, $line) = @_;
479 print $fh $line."\n";
487 for my $curpat (@files)
489 for my $elem (glob $curpat)
494 return [ sort keys %ufiles ];
499 my ($vfile, $cache) = @_;
503 if (exists $cache->{file
}->{$vfile})
505 $result = $cache->{file
}->{$vfile};
509 if (open(FILER
, $vfile))
521 $cache->{file
}->{$vfile} = [ sort keys %vresult ];
522 $result = $cache->{file
}->{$vfile};
532 my ($file, $cache) = @_;
535 if (open(FILEF
, $file))
543 if (/^(\w+)\s+(.+)$/)
545 my ($key, $value) = ($1, $2);
546 my (@values, @tempvalues);
547 if ($value =~ m
#^file:(.+)#)
550 @tempvalues = @{read_file
($vfile, $cache)};
554 @tempvalues = ( $value );
557 if ($key =~ /^zonefiles?$/)
559 # This is a globbing action
562 push @values, @{funiq
($_)};
567 @values = @tempvalues;
572 $f->{lc $key}->{$_}++;
580 print STDERR
"Warning: Couldn't open filterfile: $file; $!\n";
586 sub regexped_patterns
($)
591 for my $pat (keys %{$h})
593 unless ($pat =~ /^\^.+\$$/)
597 # fix a regexp for the lazy notation
598 $pat =~ s/^[\*\.]+//;
600 $pat = '^(.*\\.)?'.$pat.'\.?$';
602 push @{$result}, $pat;
608 sub check_pattern
($$)
610 my ($pattern, $fqdn) = @_;
613 if ($fqdn =~ /$pattern/)
626 sub make_char_regexp
($)
632 for (split /\s+/, $chars)
636 $regexp .= sprintf("\\%03o", $_);
649 $regexp = "[$regexp]";
660 sub do_filterfile
($$)
662 my ($filterfile, $cache) = @_;
664 my $output = [ \
*STDERR
];
667 my $f = read_filter
($filterfile, $cache);
669 $$f{allowtype
} = (keys %{$$f{allowtype
}})[0];
670 $$f{allowtype
} .= $opt{T
};
672 my $allowtyperegex = make_char_regexp
($$f{allowtype
});
676 for my $logfile (sort keys %{$$f{extralog
}})
678 my ($fname, $fhandle);
679 # open logfiles and put them int @{$output};
680 ($fhandle, $fname) = tempfile
();
683 push @{$output}, $fhandle;
684 push @extralogs, [ $fhandle, $fname, $logfile ];
688 print STDERR
"Warning: Couldn't create tempfile for ${logfile}.\n";
693 my @zonefiles = sort keys %{$$f{zonefile
}};
696 push @zonefiles, '-';
698 for my $zonefile (@zonefiles)
702 next if $zonefile =~ /$FILESUFFIXREGEXP/i;
706 my $filehandle = \
*STDIN
;
708 if ($zonefile ne '-')
710 $fopen = open( $filehandle, $zonefile );
714 my $temp = ($zonefile eq '-') ? '<STDIN>' : $zonefile;
715 p
$output, "File $temp";
720 while (<$filehandle>)
725 my $v = validate_line
($line);
736 $$v[1] =~ s/\n/\n /g;
737 p
$output, " line $lno; err $$v[0] $line\n ".$$v[1];
743 if ($$v[2] !~ /$allowtyperegex/)
746 if (($$v[2] ne '#') || ($opt{t
} == 1))
750 p
$output, " line $lno; err -1 $line";
751 p
$output, " record type $$v[2] disallowed; allowed: $$f{allowtype}";
756 # just check fqdn if record contains it
759 # Check $$v[3] against allowed fqdn:s:wq!
760 if (keys %{$$f{deny
}})
763 my $patterns = regexped_patterns
($$f{deny
});
766 $reason = 'default allow ^.*$';
768 for my $pat (@{$patterns})
772 if (check_pattern
($pat, $_))
775 $reason = 'deny '.$pat;
780 elsif (keys %{$$f{allow
}})
782 my $patterns = regexped_patterns
($$f{allow
});
785 $reason = 'default deny ^.*$';
787 for my $pat (@{$patterns})
791 if (check_pattern
($pat, $_))
803 if ($ok && length($line))
805 print STDOUT
"$line\n" unless $opt{q
};
813 p
$output, " line $lno; err -2; $line";
814 p
$output, " use of fqdn denied; $reason";
817 print STDOUT
"# line $lno; err -2; $line\n";
818 print STDOUT
"# use of fqdn denied; $reason\n";
824 } # while (<$filehandle>)
825 close $filehandle unless $zonefile eq '-';
826 my $plur = ($errs == 1) ? '' : 's';
827 p
$output, "$lno lines, $errs error${plur}.";
831 p
$output, "Warning: Trouble opening '$zonefile'; $!";
836 # Close all extra logfiles
837 for my $el (@extralogs)
841 if (move
($$el[1], $$el[2]))
843 print STDERR
"Copy of logfile portion to $$el[2]\n";
847 print STDERR
"Warning: Couldn't rename tempfile to $$el[2].\n";
853 print STDERR
"Warning: Couldn't close tempfile for $$el[2].\n";
865 my $files = funiq
(@ARGV);
868 if ($opt{h
} || $opt{H
} || $opt{'?'})
871 valtz
$VERSION, $COPYRIGHT
872 validates tinydns-data zone files
874 $0 [-hfFqrRiItTx
] <file
(s
)>
879 -f filter
(don
't just validate) file and output accepted lines to STDOUT.
882 -F treat files as filter configuration files for more advanced filtering.
883 These filterfiles one or several of the following filter directives:
885 zonefile <zonefilepath>
886 zonefile file:<path to textfile including zonefilepaths>
887 Defines the file(s) to be filtered. Can be a globbed value, like
888 /var/zones/external/*
891 Defines an extra logfile that the STDERR output will be copied for
892 this specific filterfile. Useful if you have a lot of filterfiles
893 and want to separate the logs.
896 deny file:<path to <zonepatternfile>
897 Defines a zonepattern to explicitly DENY after implicitly allowing all.
898 (cannot be combined with allow)
901 allow file:<path to <zonepatternfile>
902 Defines a zonepattern to explicitly ALLOW after implicitly denying all.
904 allowtype <recordtype character(s)>
905 Explicitly sets the allowed recordtypes. Note that even comments
906 has to be allowed (but these will not result in errors unless -t)
907 to be copied to the output.
909 Multiple zonefile, allow- and deny-lines are allowed, but also the
910 alternative file:-line that points to a textfile containing one
914 -r allows fqdn to be empty thus denoting the root.
915 This is also allowed per default when doing implict allow - see deny,
916 or when specifying 'allow
.', i.e. explictly allowing root as such.
917 (cannot be combined with deny)
920 -R relaxes the validation and allows empty mname and p-fields.xi
921 This is probably not very useful.
924 -i allows the ip-fields to be empty as well. These will then not generate any
928 -I Include rejected lines as comments in output (valid when filtering).
931 -q Do not echo valid lines to STDOUT.
933 -s DO NOT ignore files ending with ,v ~ .bak .log .old .swp .tmp
934 which is done per default.
937 -t Give error even on #comment-lines when they are not allowed.
938 (These errors are silently ignored per default)
942 A commandline way to explicitly set the allowed recordtypes.
943 This is _concatenated_ to the allowtype-allowed recordtypes.
945 -x Exit with non-null exit code on errors; i.e. make errors detectable by
946 e.g. shell scripts; 1 = validation error, 2 = permission error,
947 3 = combination of 1 and 2.
951 All errors in the zonefiles are sent to STDERR.
956 Example; simple filter-use;
957 valtz -f /etc/zones/zone-* \
958 >/etc/tinydns/data.filtered \
959 2>/var/log/tinydns/valtz.log
961 Example; filterfile use;
962 valtz -F /etc/zones/filter/zones-otto \
963 >/etc/tinydns/data.otto \
964 2>/var/log/tinydns/valtz.log
967 Example filterfile for using as import from primary (as above):
968 zonefile /var/zones/external/otto/zone-*
971 extralog /var/log/tinydns/external-otto.log
973 Example #2, strict filter for a certain user editing just A-records
975 zonefile /home/felix/zones/zone-fl3x-net
978 extralog /var/log/tinydns/fl3x-net.log
980 Example #3, export filter to secondary
982 zonefile /var/zones/primary/zone-*
983 # just allow OUR zones to be exported, not to annoy secondary partner
984 allow file:/var/zones/primary-zones.txt
985 # don't allow any other types than this
; e
.g
. comments won
't be exported
987 extralog /var/log/tinydns/primary-export.log
992 elsif (@{$files} == 0)
995 valtz $VERSION, $COPYRIGHT
996 validates tinydns-data zone files
999 $0 [-qrRix] <zonefiles>
1001 $0 -f[qrRiItTx] <zonefiles>
1002 Extensive filtering:
1003 $0 -F[qrRiItTx] <zonefiles>
1005 More help and information about options:
1016 $cache->{file} = {};
1017 for my $file (@{$files})
1019 my $result = do_filterfile($file, $cache);
1026 my $output = [ \*STDERR ];
1028 for my $zonefile (sort @{$files})
1032 next if $zonefile =~ /$FILESUFFIXREGEXP/i;
1035 my $filehandle = \*STDIN;
1037 if ($zonefile ne '-')
1039 $fopen = open( $filehandle, $zonefile );
1046 while (<$filehandle>)
1051 my $v = validate_line($line);
1056 my $temp = ($zonefile eq '-') ? '<STDIN
>' : $zonefile;
1057 p $output, "File $temp" unless $errs;
1060 $$v[1] =~ s/\n/\n /g;
1061 p $output, " line $lno; err $$v[0] $line\n ".$$v[1];
1064 print STDOUT "# line $lno; err $$v[0] $line
1065 print STDOUT "# $$v[1]; \n";
1070 # Echo NON-ERRORS to STDOUT
1073 print STDOUT "$line\n" unless $opt{q};
1078 close $filehandle unless $zonefile eq '-';
1082 p $output, "Error: Trouble opening '$zonefile'; $!";
1087 if ($opt{x} && ($verrs_total + $perrs_total))
1089 my $exitcode = $verrs_total > 0 ? 1 : 0;
1090 $exitcode += $perrs_total > 0 ? 2 : 0;