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)',
79 1011 => 'IP address found where hostname expected'
82 # NOTE : ONLY translate the right-hand part
85 'ipprefix' => 'IP prefix',
86 'fqdn' => 'Domain name',
90 'timestamp' => 'Timestamp',
95 'mname' => 'Master name',
96 'rname' => 'Role name',
97 'ser' => 'Serial number',
98 'ref' => 'Refresh time',
99 'ret' => 'Retry time',
100 'exp' => 'Expire time',
101 'min' => 'Minimum time',
102 'n' => 'Record type number',
103 'rdata' => 'Resource data',
105 'priority' => 'Priority',
117 '-' => ':disabled +',
126 # NOTE : This should NOT be translated!
128 '%' => [ ':location', 'lo:ipprefix', 'lo' ],
129 '.' => [ 'NS(+A?)', 'fqdn:ip:x:ttl:timestamp:lo', 'fqdn' ],
130 '&' => [ 'NS(+A?)', 'fqdn:ip:x:ttl:timestamp:lo', 'fqdn' ],
131 '=' => [ 'A+PTR', 'fqdn:ip:ttl:timestamp:lo', 'fqdn:ip' ],
132 '+' => [ 'A', 'fqdn:ip:ttl:timestamp:lo', 'fqdn:ip' ],
133 '@' => [ 'MX(+A?)', 'fqdn:ip:x:dist:ttl:timestamp:lo', 'fqdn' ],
134 '#' => [ ':comment', '', '' ],
135 '-' => [ ':disabled +', '', '' ],
136 "'" => [ 'TXT', 'fqdn:s:ttl:timestamp:lo', 'fqdn:s' ],
137 '^' => [ 'PTR', 'fqdn:p:ttl:timestamp:lo', 'fqdn:p' ],
138 'C' => [ 'CNAME', 'fqdn:p:ttl:timestamp:lo', 'fqdn:p' ],
139 'S' => [ 'SRV', 'fqdn:ip:x:port:weight:priority:ttl:timestamp:lo',
141 'Z' => [ 'SOA', 'fqdn:mname:rname:ser:ref:ret:exp:min:ttl:timestamp:lo',
142 'fqdn:mname:rname' ],
143 ':' => [ 'GENERIC', 'fqdn:n:rdata:ttl:timestamp:lo', 'fqdn:n:rdata' ]
149 my ($s, $boundary) = @_;
156 $result = 1008 if $boundary && ($i >= $boundary);
167 # NOTE : No translation here!
168 my %token_validator = (
172 return 1001 unless $s =~ /^[a-z][a-z]$/i;
179 return 1002 unless exists($loreg{$s});
183 'ipprefix' => [ 3, sub {
186 if ($s =~ /^(\d+)(\.(\d+)(\.(\d+)(\.(\d+))?)?)?$/)
188 my ($a, $b, $c, $d) = ($1, $3, $5, $7);
193 if (($a > 255) || ($b > 255) || ($c > 255) || ($d > 255))
207 # remove OK wildcard prefixing, to simplify test.
208 $s =~ s/^\*\.([a-z0-9].*)$/$1/i;
210 for my $hostpart (split /\./, $s)
212 return 1005 unless $hostpart =~ /^_?[-a-z0-9]+$/i;
213 return 1006 if $hostpart =~ /^-/;
214 return 1007 if $hostpart =~ /-$/;
221 if ($s =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)\.?$/)
223 my ($a, $b, $c, $d) = ($1, $3, $5, $7);
228 if (($a > 255) || ($b > 255) || ($c > 255) || ($d > 255))
243 # Check to see if someone put an IP address in a hostname
244 # field. The motivation for this was MX records where many
245 # people expect an IP address to be a valid response, but I
246 # see no harm in enforcing it elsewhere.
247 return 1011 if $s =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\.?$/;
252 return 1005 unless /^[-[a-z0-9]+$/i;
260 my $result = validate_integer
($s, 2**32);
263 'timestamp' => [ 7, sub {
265 my $result = validate_integer
($s, 2**32);
270 my $result = validate_integer
($s, 65536);
276 # TODO : Validation needed?
285 return 1005 unless /^_?[-[a-z0-9]+$/i;
291 'mname' => [ 12, sub {
297 return 1005 unless /^[-[a-z0-9]+$/i;
303 'rname' => [ 13, sub {
308 my @parts = split /\./, $s;
309 return 1009 if @parts < 3;
313 return 1005 unless /^[-[a-z0-9]+$/i;
321 my $result = validate_integer
($s, 2**32);
326 my $result = validate_integer
($s, 2**32);
331 my $result = validate_integer
($s, 2**32);
336 my $result = validate_integer
($s, 2**32);
341 my $result = validate_integer
($s, 2**32);
346 my $result = validate_integer
($s, 65535);
348 return 1010 if ($s==2)||($s==5)||($s==6)||($s==12)||($s==15)||($s==252);
352 'rdata' => [ 20, sub {
354 # TODO : Validation needed?
358 'port' => [ 21, sub {
360 my $result = validate_integer
($s, 65536);
363 'priority' => [ 22, sub {
365 my $result = validate_integer
($s, 65536);
368 'weight' => [ 23, sub {
370 my $result = validate_integer
($s, 65536);
379 sub validate_line
($)
383 my $result = [ 0, '', '', [] ];
389 my $type = substr($s, 0, 1); $$result[2] = $type;
390 my $rest = substr($s, 1);
391 if (exists($line_type{$type}))
393 my $lt = $line_type{$type};
394 my @mask = split /\:/, $line_type{$type}->[1];
395 my @mandatory = split /\:/, $line_type{$type}->[2];
400 my @tokens = split /\:/, $rest;
404 $vals = $#mandatory if $#mandatory > $vals;
408 my $token = $tokens[$t];
409 # sanity check; should not fail
412 # silently ignore excessive fields
413 # as tinydns-data does now
415 elsif (exists($token_validator{$mask[$c]}))
417 my $validator = $token_validator{$mask[$c]};
421 # Remember fqdn for later
422 if (($c eq 0) && ($mask[0] eq 'fqdn'))
426 push @{$$result[3]}, $tmp;
429 # Remember x as fqdn IF ip is specified
430 if (($mask[$c] eq 'ip') && (length($token)))
436 if (length($ip) && ($mask[$c] eq 'x'))
440 push @{$$result[3]}, $tmp;
445 my $tv = &{$$validator[1]}($type, $token);
448 $$result[0] ^= (2 ** $$validator[0]);
450 "\npos $c; $mask[$c]; $validation_msg{$tv}";
453 elsif ($mandatory[$c] eq $mask[$c])
456 $mand = 0 if ($opt{r
}) && ($mask[$c] eq 'fqdn');
457 $mand = 0 if ($opt{R
}) && ($mask[$c] eq 'mname');
458 $mand = 0 if ($opt{R
}) && ($mask[$c] eq 'p');
459 $mand = 0 if ($opt{R
}) && ($mask[$c] eq 'rdata');
460 $mand = 0 if ($opt{i
}) && ($mask[$c] eq 'ip');
464 $$result[0] ^= (2 ** $$validator[0]);
465 $$result[1] .= "\npos $c; $mask[$c]; ".
466 $token_name{$mask[$c]}.' is mandatory';
469 # else ignore nonmandatory blanks
474 # somebody has modified program in a wrong way
476 "VALIDATOR FAILS ON TOKENS OF TYPE ".$mask[$c]." $c" ];
484 $$result[1] = "expected: ".$line_type{$type}->[1]."\n".
491 $result = [ 1, sprintf("unknown record type: #%02x",
496 $$result[1] =~ s/^\n+//;
497 $$result[1] =~ s/\n+/\n/g;
499 # result is now [ iErrno, sErrtxt, sRecordType, [ sFQDN ] ]
505 my ($fhv, $line) = @_;
508 print $fh $line."\n";
516 for my $curpat (@files)
518 for my $elem (glob $curpat)
523 return [ sort keys %ufiles ];
528 my ($vfile, $cache) = @_;
532 if (exists $cache->{file
}->{$vfile})
534 $result = $cache->{file
}->{$vfile};
538 if (open(FILER
, $vfile))
550 $cache->{file
}->{$vfile} = [ sort keys %vresult ];
551 $result = $cache->{file
}->{$vfile};
561 my ($file, $cache) = @_;
564 if (open(FILEF
, $file))
572 if (/^(\w+)\s+(.+)$/)
574 my ($key, $value) = ($1, $2);
575 my (@values, @tempvalues);
576 if ($value =~ m
#^file:(.+)#)
579 @tempvalues = @{read_file
($vfile, $cache)};
583 @tempvalues = ( $value );
586 if ($key =~ /^zonefiles?$/)
588 # This is a globbing action
591 push @values, @{funiq
($_)};
596 @values = @tempvalues;
601 $f->{lc $key}->{$_}++;
609 print STDERR
"Warning: Couldn't open filterfile: $file; $!\n";
615 sub regexped_patterns
($)
620 for my $pat (keys %{$h})
622 unless ($pat =~ /^\^.+\$$/)
626 # fix a regexp for the lazy notation
627 $pat =~ s/^[\*\.]+//;
629 $pat = '^(.*\\.)?'.$pat.'\.?$';
631 push @{$result}, $pat;
637 sub check_pattern
($$)
639 my ($pattern, $fqdn) = @_;
642 if ($fqdn =~ /$pattern/)
655 sub make_char_regexp
($)
661 for (split /\s+/, $chars)
665 $regexp .= sprintf("\\%03o", $_);
678 $regexp = "[$regexp]";
689 sub do_filterfile
($$)
691 my ($filterfile, $cache) = @_;
693 my $output = [ \
*STDERR
];
696 my $f = read_filter
($filterfile, $cache);
698 $$f{allowtype
} = (keys %{$$f{allowtype
}})[0];
699 $$f{allowtype
} .= $opt{T
};
701 my $allowtyperegex = make_char_regexp
($$f{allowtype
});
705 for my $logfile (sort keys %{$$f{extralog
}})
707 my ($fname, $fhandle);
708 # open logfiles and put them int @{$output};
709 ($fhandle, $fname) = tempfile
();
712 push @{$output}, $fhandle;
713 push @extralogs, [ $fhandle, $fname, $logfile ];
717 print STDERR
"Warning: Couldn't create tempfile for ${logfile}.\n";
722 my @zonefiles = sort keys %{$$f{zonefile
}};
725 push @zonefiles, '-';
727 for my $zonefile (@zonefiles)
731 next if $zonefile =~ /$FILESUFFIXREGEXP/i;
735 my $filehandle = \
*STDIN
;
737 if ($zonefile ne '-')
739 $fopen = open( $filehandle, $zonefile );
743 my $temp = ($zonefile eq '-') ? '<STDIN>' : $zonefile;
744 p
$output, "File $temp";
749 while (<$filehandle>)
754 my $v = validate_line
($line);
765 $$v[1] =~ s/\n/\n /g;
766 p
$output, " line $lno; err $$v[0] $line\n ".$$v[1];
772 if ($$v[2] !~ /$allowtyperegex/)
775 if (($$v[2] ne '#') || ($opt{t
} == 1))
779 p
$output, " line $lno; err -1 $line";
780 p
$output, " record type $$v[2] disallowed; allowed: $$f{allowtype}";
785 # just check fqdn if record contains it
788 # Check $$v[3] against allowed fqdn:s:wq!
789 if (keys %{$$f{deny
}})
791 my $patterns = regexped_patterns
($$f{deny
});
794 $reason = 'default allow ^.*$';
796 for my $pat (@{$patterns})
800 if (check_pattern
($pat, $_))
803 $reason = 'deny '.$pat;
808 elsif (keys %{$$f{allow
}})
810 my $patterns = regexped_patterns
($$f{allow
});
813 $reason = 'default deny ^.*$';
815 for my $pat (@{$patterns})
819 if (check_pattern
($pat, $_))
831 if ($ok && length($line))
833 print STDOUT
"$line\n" unless $opt{q
};
841 p
$output, " line $lno; err -2; $line";
842 p
$output, " use of fqdn denied; $reason";
845 print STDOUT
"# line $lno; err -2; $line\n";
846 print STDOUT
"# use of fqdn denied; $reason\n";
852 } # while (<$filehandle>)
853 close $filehandle unless $zonefile eq '-';
854 my $plur = ($errs == 1) ? '' : 's';
855 p
$output, "$lno lines, $errs error${plur}.";
859 p
$output, "Warning: Trouble opening '$zonefile'; $!";
864 # Close all extra logfiles
865 for my $el (@extralogs)
869 if (move
($$el[1], $$el[2]))
871 print STDERR
"Copy of logfile portion to $$el[2]\n";
875 print STDERR
"Warning: Couldn't rename tempfile to $$el[2].\n";
881 print STDERR
"Warning: Couldn't close tempfile for $$el[2].\n";
893 my $files = funiq
(@ARGV);
896 if ($opt{h
} || $opt{H
} || $opt{'?'})
899 valtz
$VERSION, $COPYRIGHT
900 validates tinydns-data zone files
902 $0 [-hfFqrRiItTx
] <file
(s
)>
907 -f filter
(don
't just validate) file and output accepted lines to STDOUT.
910 -F treat files as filter configuration files for more advanced filtering.
911 These filterfiles one or several of the following filter directives:
913 zonefile <zonefilepath>
914 zonefile file:<path to textfile including zonefilepaths>
915 Defines the file(s) to be filtered. Can be a globbed value, like
916 /var/zones/external/*
919 Defines an extra logfile that the STDERR output will be copied for
920 this specific filterfile. Useful if you have a lot of filterfiles
921 and want to separate the logs.
924 deny file:<path to <zonepatternfile>
925 Defines a zonepattern to explicitly DENY after implicitly allowing all.
926 (cannot be combined with allow)
929 allow file:<path to <zonepatternfile>
930 Defines a zonepattern to explicitly ALLOW after implicitly denying all.
932 allowtype <recordtype character(s)>
933 Explicitly sets the allowed recordtypes. Note that even comments
934 has to be allowed (but these will not result in errors unless -t)
935 to be copied to the output.
937 Multiple zonefile, allow- and deny-lines are allowed, but also the
938 alternative file:-line that points to a textfile containing one
942 -r allows fqdn to be empty thus denoting the root.
943 This is also allowed per default when doing implict allow - see deny,
944 or when specifying 'allow
.', i.e. explictly allowing root as such.
945 (cannot be combined with deny)
948 -R relaxes the validation and allows empty mname and p-fields.xi
949 This is probably not very useful.
952 -i allows the ip-fields to be empty as well. These will then not generate any
956 -I Include rejected lines as comments in output (valid when filtering).
959 -q Do not echo valid lines to STDOUT.
961 -s DO NOT ignore files ending with ,v ~ .bak .log .old .swp .tmp
962 which is done per default.
965 -t Give error even on #comment-lines when they are not allowed.
966 (These errors are silently ignored per default)
970 A commandline way to explicitly set the allowed recordtypes.
971 This is _concatenated_ to the allowtype-allowed recordtypes.
973 -x Exit with non-null exit code on errors; i.e. make errors detectable by
974 e.g. shell scripts; 1 = validation error, 2 = permission error,
975 3 = combination of 1 and 2.
979 All errors in the zonefiles are sent to STDERR.
984 Example; simple filter-use;
985 valtz -f /etc/zones/zone-* \
986 >/etc/tinydns/data.filtered \
987 2>/var/log/tinydns/valtz.log
989 Example; filterfile use;
990 valtz -F /etc/zones/filter/zones-otto \
991 >/etc/tinydns/data.otto \
992 2>/var/log/tinydns/valtz.log
995 Example filterfile for using as import from primary (as above):
996 zonefile /var/zones/external/otto/zone-*
999 extralog /var/log/tinydns/external-otto.log
1001 Example #2, strict filter for a certain user editing just A-records
1003 zonefile /home/felix/zones/zone-fl3x-net
1006 extralog /var/log/tinydns/fl3x-net.log
1008 Example #3, export filter to secondary
1010 zonefile /var/zones/primary/zone-*
1011 # just allow OUR zones to be exported, not to annoy secondary partner
1012 allow file:/var/zones/primary-zones.txt
1013 # don't allow any other types than this
; e
.g
. comments won
't be exported
1015 extralog /var/log/tinydns/primary-export.log
1020 elsif (@{$files} == 0)
1023 valtz $VERSION, $COPYRIGHT
1024 validates tinydns-data zone files
1027 $0 [-qrRix] <zonefiles>
1029 $0 -f[qrRiItTx] <zonefiles>
1030 Extensive filtering:
1031 $0 -F[qrRiItTx] <zonefiles>
1033 More help and information about options:
1044 $cache->{file} = {};
1045 for my $file (@{$files})
1047 my $result = do_filterfile($file, $cache);
1054 my $output = [ \*STDERR ];
1056 for my $zonefile (sort @{$files})
1060 next if $zonefile =~ /$FILESUFFIXREGEXP/i;
1063 my $filehandle = \*STDIN;
1065 if ($zonefile ne '-')
1067 $fopen = open( $filehandle, $zonefile );
1074 while (<$filehandle>)
1079 my $v = validate_line($line);
1084 my $temp = ($zonefile eq '-') ? '<STDIN
>' : $zonefile;
1085 p $output, "File $temp" unless $errs;
1088 $$v[1] =~ s/\n/\n /g;
1089 p $output, " line $lno; err $$v[0] $line\n ".$$v[1];
1092 print STDOUT "# line $lno; err $$v[0] $line
1093 print STDOUT "# $$v[1]; \n";
1098 # Echo NON-ERRORS to STDOUT
1101 print STDOUT "$line\n" unless $opt{q};
1106 close $filehandle unless $zonefile eq '-';
1110 p $output, "Error: Trouble opening '$zonefile'; $!";
1115 if ($opt{x} && ($verrs_total + $perrs_total))
1117 my $exitcode = $verrs_total > 0 ? 1 : 0;
1118 $exitcode += $perrs_total > 0 ? 2 : 0;