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
('?fFhHiIqrRstT:', \
%opt);
54 my $FILESUFFIXREGEXP = '('.join('|', qw
/
55 ,v
~ .bak
.log .old
.swp
.tmp
61 # "Permission" errors with respect to what record types are allowed
66 # global location registry
67 # (reset for every zone file)
70 # NOTE : DO NOT CHANGE the id numbers
71 my %validation_msg = (
72 1001 => 'badly formed; should be just two ASCII letters',
73 1002 => 'location is not previously defined in a %-line',
74 1003 => 'invalid syntax',
75 1004 => 'invalid syntax of integer',
76 1005 => 'parts must only contain ASCII letters, digits and - characters',
77 1006 => 'parts must not begin with the - character',
78 1007 => 'parts must not end with the - character',
79 1008 => 'integer out of bounds',
80 1009 => 'must have at least three labels to be valid as mail address',
81 1010 => 'must not be 2(NS), 5(CNAME), 6(SOA), 12(PTR), 15(MX) or 252(AXFR)',
82 1011 => 'IP address found where hostname expected'
85 # NOTE : ONLY translate the right-hand part
88 'ipprefix' => 'IP prefix',
89 'fqdn' => 'Domain name',
93 'timestamp' => 'Timestamp',
98 'mname' => 'Master name',
99 'rname' => 'Role name',
100 'ser' => 'Serial number',
101 'ref' => 'Refresh time',
102 'ret' => 'Retry time',
103 'exp' => 'Expire time',
104 'min' => 'Minimum time',
105 'n' => 'Record type number',
106 'rdata' => 'Resource data',
108 'priority' => 'Priority',
120 '-' => ':disabled +',
129 # NOTE : This should NOT be translated!
131 '%' => [ ':location', 'lo:ipprefix', 'lo' ],
132 '.' => [ 'NS(+A?)', 'fqdn:ip:x:ttl:timestamp:lo', 'fqdn' ],
133 '&' => [ 'NS(+A?)', 'fqdn:ip:x:ttl:timestamp:lo', 'fqdn' ],
134 '=' => [ 'A+PTR', 'fqdn:ip:ttl:timestamp:lo', 'fqdn:ip' ],
135 '+' => [ 'A', 'fqdn:ip:ttl:timestamp:lo', 'fqdn:ip' ],
136 '@' => [ 'MX(+A?)', 'fqdn:ip:x:dist:ttl:timestamp:lo', 'fqdn' ],
137 '#' => [ ':comment', '', '' ],
138 '-' => [ ':disabled +', '', '' ],
139 "'" => [ 'TXT', 'fqdn:s:ttl:timestamp:lo', 'fqdn:s' ],
140 '^' => [ 'PTR', 'fqdn:p:ttl:timestamp:lo', 'fqdn:p' ],
141 'C' => [ 'CNAME', 'fqdn:p:ttl:timestamp:lo', 'fqdn:p' ],
142 'S' => [ 'SRV', 'fqdn:ip:x:port:weight:priority:ttl:timestamp:lo',
144 'Z' => [ 'SOA', 'fqdn:mname:rname:ser:ref:ret:exp:min:ttl:timestamp:lo',
145 'fqdn:mname:rname' ],
146 ':' => [ 'GENERIC', 'fqdn:n:rdata:ttl:timestamp:lo', 'fqdn:n:rdata' ]
152 my ($s, $boundary) = @_;
159 $result = 1008 if $boundary && ($i >= $boundary);
170 # NOTE : No translation here!
171 my %token_validator = (
175 return 1001 unless $s =~ /^[a-z][a-z]$/i;
182 return 1002 unless exists($loreg{$s});
186 'ipprefix' => [ 3, sub {
189 if ($s =~ /^(\d+)(\.(\d+)(\.(\d+)(\.(\d+))?)?)?$/)
191 my ($a, $b, $c, $d) = ($1, $3, $5, $7);
196 if (($a > 255) || ($b > 255) || ($c > 255) || ($d > 255))
210 # remove OK wildcard prefixing, to simplify test.
211 $s =~ s/^\*\.([a-z0-9].*)$/$1/i;
213 for my $hostpart (split /\./, $s)
215 return 1005 unless $hostpart =~ /^_?[-a-z0-9]+$/i;
216 return 1006 if $hostpart =~ /^-/;
217 return 1007 if $hostpart =~ /-$/;
224 if ($s =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)\.?$/)
226 my ($a, $b, $c, $d) = ($1, $3, $5, $7);
231 if (($a > 255) || ($b > 255) || ($c > 255) || ($d > 255))
246 # Check to see if someone put an IP address in a hostname
247 # field. The motivation for this was MX records where many
248 # people expect an IP address to be a valid response, but I
249 # see no harm in enforcing it elsewhere.
250 return 1011 if $s =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\.?$/;
255 return 1005 unless /^[-[a-z0-9]+$/i;
263 my $result = validate_integer
($s, 2**32);
266 'timestamp' => [ 7, sub {
268 my $result = validate_integer
($s, 2**32);
273 my $result = validate_integer
($s, 65536);
279 # TODO : Validation needed?
288 return 1005 unless /^_?[-[a-z0-9]+$/i;
294 'mname' => [ 12, sub {
300 return 1005 unless /^[-[a-z0-9]+$/i;
306 'rname' => [ 13, sub {
311 my @parts = split /\./, $s;
312 return 1009 if @parts < 3;
316 return 1005 unless /^[-[a-z0-9]+$/i;
324 my $result = validate_integer
($s, 2**32);
329 my $result = validate_integer
($s, 2**32);
334 my $result = validate_integer
($s, 2**32);
339 my $result = validate_integer
($s, 2**32);
344 my $result = validate_integer
($s, 2**32);
349 my $result = validate_integer
($s, 65535);
351 return 1010 if ($s==2)||($s==5)||($s==6)||($s==12)||($s==15)||($s==252);
355 'rdata' => [ 20, sub {
357 # TODO : Validation needed?
361 'port' => [ 21, sub {
363 my $result = validate_integer
($s, 65536);
366 'priority' => [ 22, sub {
368 my $result = validate_integer
($s, 65536);
371 'weight' => [ 23, sub {
373 my $result = validate_integer
($s, 65536);
382 sub validate_line
($)
386 my $result = [ 0, '', '', [] ];
392 my $type = substr($s, 0, 1); $$result[2] = $type;
393 my $rest = substr($s, 1);
394 if (exists($line_type{$type}))
396 my $lt = $line_type{$type};
397 my @mask = split /\:/, $line_type{$type}->[1];
398 my @mandatory = split /\:/, $line_type{$type}->[2];
403 my @tokens = split /\:/, $rest;
407 $vals = $#mandatory if $#mandatory > $vals;
411 my $token = $tokens[$t];
412 # sanity check; should not fail
415 # silently ignore excessive fields
416 # as tinydns-data does now
418 elsif (exists($token_validator{$mask[$c]}))
420 my $validator = $token_validator{$mask[$c]};
424 # Remember fqdn for later
425 if (($c eq 0) && ($mask[0] eq 'fqdn'))
429 push @{$$result[3]}, $tmp;
432 # Remember x as fqdn IF ip is specified
433 if (($mask[$c] eq 'ip') && (length($token)))
439 if (length($ip) && ($mask[$c] eq 'x'))
443 push @{$$result[3]}, $tmp;
448 my $tv = &{$$validator[1]}($type, $token);
451 $$result[0] ^= (2 ** $$validator[0]);
453 "\npos $c; $mask[$c]; $validation_msg{$tv}";
456 elsif ($mandatory[$c] eq $mask[$c])
459 $mand = 0 if ($opt{r
}) && ($mask[$c] eq 'fqdn');
460 $mand = 0 if ($opt{R
}) && ($mask[$c] eq 'mname');
461 $mand = 0 if ($opt{R
}) && ($mask[$c] eq 'p');
462 $mand = 0 if ($opt{R
}) && ($mask[$c] eq 'rdata');
463 $mand = 0 if ($opt{i
}) && ($mask[$c] eq 'ip');
467 $$result[0] ^= (2 ** $$validator[0]);
468 $$result[1] .= "\npos $c; $mask[$c]; ".
469 $token_name{$mask[$c]}.' is mandatory';
472 # else ignore nonmandatory blanks
477 # somebody has modified program in a wrong way
479 "VALIDATOR FAILS ON TOKENS OF TYPE ".$mask[$c]." $c" ];
487 $$result[1] = "expected: ".$line_type{$type}->[1]."\n".
494 $result = [ 1, sprintf("unknown record type: #%02x",
499 $$result[1] =~ s/^\n+//;
500 $$result[1] =~ s/\n+/\n/g;
502 # result is now [ iErrno, sErrtxt, sRecordType, [ sFQDN ] ]
508 my ($fhv, $line) = @_;
511 print $fh $line."\n";
519 for my $curpat (@files)
521 for my $elem (glob $curpat)
526 return [ sort keys %ufiles ];
531 my ($vfile, $cache) = @_;
535 if (exists $cache->{file
}->{$vfile})
537 $result = $cache->{file
}->{$vfile};
541 if (open(FILER
, $vfile))
553 $cache->{file
}->{$vfile} = [ sort keys %vresult ];
554 $result = $cache->{file
}->{$vfile};
564 my ($file, $cache) = @_;
567 if (open(FILEF
, $file))
575 if (/^(\w+)\s+(.+)$/)
577 my ($key, $value) = ($1, $2);
578 my (@values, @tempvalues);
579 if ($value =~ m
#^file:(.+)#)
582 @tempvalues = @{read_file
($vfile, $cache)};
586 @tempvalues = ( $value );
589 if ($key =~ /^zonefiles?$/)
591 # This is a globbing action
594 push @values, @{funiq
($_)};
599 @values = @tempvalues;
604 $f->{lc $key}->{$_}++;
612 print STDERR
"Warning: Couldn't open filterfile: $file; $!\n";
618 sub regexped_patterns
($)
623 for my $pat (keys %{$h})
625 unless ($pat =~ /^\^.+\$$/)
629 # fix a regexp for the lazy notation
630 $pat =~ s/^[\*\.]+//;
632 $pat = '^(.*\\.)?'.$pat.'\.?$';
634 push @{$result}, $pat;
640 sub check_pattern
($$)
642 my ($pattern, $fqdn) = @_;
645 if ($fqdn =~ /$pattern/)
658 sub make_char_regexp
($)
664 for (split /\s+/, $chars)
668 $regexp .= sprintf("\\%03o", $_);
681 $regexp = "[$regexp]";
692 sub do_filterfile
($$)
694 my ($filterfile, $cache) = @_;
696 my $output = [ \
*STDERR
];
699 my $f = read_filter
($filterfile, $cache);
701 $$f{allowtype
} = (keys %{$$f{allowtype
}})[0];
702 $$f{allowtype
} .= $opt{T
};
704 my $allowtyperegex = make_char_regexp
($$f{allowtype
});
708 for my $logfile (sort keys %{$$f{extralog
}})
710 my ($fname, $fhandle);
711 # open logfiles and put them int @{$output};
712 ($fhandle, $fname) = tempfile
();
715 push @{$output}, $fhandle;
716 push @extralogs, [ $fhandle, $fname, $logfile ];
720 print STDERR
"Warning: Couldn't create tempfile for ${logfile}.\n";
725 my @zonefiles = sort keys %{$$f{zonefile
}};
728 push @zonefiles, '-';
730 for my $zonefile (@zonefiles)
734 next if $zonefile =~ /$FILESUFFIXREGEXP/i;
738 my $filehandle = \
*STDIN
;
740 if ($zonefile ne '-')
742 $fopen = open( $filehandle, $zonefile );
746 my $temp = ($zonefile eq '-') ? '<STDIN>' : $zonefile;
747 p
$output, "File $temp";
752 while (<$filehandle>)
757 my $v = validate_line
($line);
768 $$v[1] =~ s/\n/\n /g;
769 p
$output, " line $lno; err $$v[0] $line\n ".$$v[1];
775 if ($$v[2] !~ /$allowtyperegex/)
778 if (($$v[2] ne '#') || ($opt{t
} == 1))
782 p
$output, " line $lno; err -1 $line";
783 p
$output, " record type $$v[2] disallowed; allowed: $$f{allowtype}";
788 # just check fqdn if record contains it
791 # Check $$v[3] against allowed fqdn:s:wq!
792 if (keys %{$$f{deny
}})
794 my $patterns = regexped_patterns
($$f{deny
});
797 $reason = 'default allow ^.*$';
799 for my $pat (@{$patterns})
803 if (check_pattern
($pat, $_))
806 $reason = 'deny '.$pat;
811 elsif (keys %{$$f{allow
}})
813 my $patterns = regexped_patterns
($$f{allow
});
816 $reason = 'default deny ^.*$';
818 for my $pat (@{$patterns})
822 if (check_pattern
($pat, $_))
834 if ($ok && length($line))
836 print STDOUT
"$line\n" unless $opt{q
};
844 p
$output, " line $lno; err -2; $line";
845 p
$output, " use of fqdn denied; $reason";
848 print STDOUT
"# line $lno; err -2; $line\n";
849 print STDOUT
"# use of fqdn denied; $reason\n";
855 } # while (<$filehandle>)
856 close $filehandle unless $zonefile eq '-';
857 my $plur = ($errs == 1) ? '' : 's';
858 p
$output, "$lno lines, $errs error${plur}.";
862 p
$output, "Warning: Trouble opening '$zonefile'; $!";
867 # Close all extra logfiles
868 for my $el (@extralogs)
872 if (move
($$el[1], $$el[2]))
874 print STDERR
"Copy of logfile portion to $$el[2]\n";
878 print STDERR
"Warning: Couldn't rename tempfile to $$el[2].\n";
884 print STDERR
"Warning: Couldn't close tempfile for $$el[2].\n";
896 my $files = funiq
(@ARGV);
899 if ($opt{h
} || $opt{H
} || $opt{'?'})
903 validates tinydns-data zone files
905 $0 [-hfFqrRiItT
] <file
(s
)>
910 -f filter
(don
't just validate) file and output accepted lines to STDOUT.
913 -F treat files as filter configuration files for more advanced filtering.
914 These filterfiles one or several of the following filter directives:
916 zonefile <zonefilepath>
917 zonefile file:<path to textfile including zonefilepaths>
918 Defines the file(s) to be filtered. Can be a globbed value, like
919 /var/zones/external/*
922 Defines an extra logfile that the STDERR output will be copied for
923 this specific filterfile. Useful if you have a lot of filterfiles
924 and want to separate the logs.
927 deny file:<path to <zonepatternfile>
928 Defines a zonepattern to explicitly DENY after implicitly allowing all.
929 (cannot be combined with allow)
932 allow file:<path to <zonepatternfile>
933 Defines a zonepattern to explicitly ALLOW after implicitly denying all.
935 allowtype <recordtype character(s)>
936 Explicitly sets the allowed recordtypes. Note that even comments
937 has to be allowed (but these will not result in errors unless -t)
938 to be copied to the output.
940 Multiple zonefile, allow- and deny-lines are allowed, but also the
941 alternative file:-line that points to a textfile containing one
945 -r allows fqdn to be empty thus denoting the root.
946 This is also allowed per default when doing implict allow - see deny,
947 or when specifying 'allow
.', i.e. explictly allowing root as such.
948 (cannot be combined with deny)
951 -R relaxes the validation and allows empty mname and p-fields.xi
952 This is probably not very useful.
955 -i allows the ip-fields to be empty as well. These will then not generate any
959 -I Include rejected lines as comments in output (valid when filtering).
962 -q Do not echo valid lines to STDOUT.
964 -s DO NOT ignore files ending with ,v ~ .bak .log .old .swp .tmp
965 which is done per default.
968 -t Give error even on #comment-lines when they are not allowed.
969 (These errors are silently ignored per default)
973 A commandline way to explicitly set the allowed recordtypes.
974 This is _concatenated_ to the allowtype-allowed recordtypes.
978 All errors in the zonefiles are sent to STDERR.
983 Example; simple filter-use;
984 valtz -f /etc/zones/zone-* \
985 >/etc/tinydns/data.filtered \
986 2>/var/log/tinydns/valtz.log
988 Example; filterfile use;
989 valtz -F /etc/zones/filter/zones-otto \
990 >/etc/tinydns/data.otto \
991 2>/var/log/tinydns/valtz.log
994 Example filterfile for using as import from primary (as above):
995 zonefile /var/zones/external/otto/zone-*
998 extralog /var/log/tinydns/external-otto.log
1000 Example #2, strict filter for a certain user editing just A-records
1002 zonefile /home/felix/zones/zone-fl3x-net
1005 extralog /var/log/tinydns/fl3x-net.log
1007 Example #3, export filter to secondary
1009 zonefile /var/zones/primary/zone-*
1010 # just allow OUR zones to be exported, not to annoy secondary partner
1011 allow file:/var/zones/primary-zones.txt
1012 # don't allow any other types than this
; e
.g
. comments won
't be exported
1014 extralog /var/log/tinydns/primary-export.log
1019 elsif (@{$files} == 0)
1023 validates tinydns-data zone files
1026 $0 [-qrRix] <zonefiles>
1028 $0 -f[qrRiItTx] <zonefiles>
1029 Extensive filtering:
1030 $0 -F[qrRiItTx] <zonefiles>
1032 More help and information about options:
1043 $cache->{file} = {};
1044 for my $file (@{$files})
1046 my $result = do_filterfile($file, $cache);
1053 my $output = [ \*STDERR ];
1055 for my $zonefile (sort @{$files})
1059 next if $zonefile =~ /$FILESUFFIXREGEXP/i;
1062 my $filehandle = \*STDIN;
1064 if ($zonefile ne '-')
1066 $fopen = open( $filehandle, $zonefile );
1073 while (<$filehandle>)
1078 my $v = validate_line($line);
1083 my $temp = ($zonefile eq '-') ? '<STDIN
>' : $zonefile;
1084 p $output, "File $temp" unless $errs;
1087 $$v[1] =~ s/\n/\n /g;
1088 p $output, " line $lno; err $$v[0] $line\n ".$$v[1];
1091 print STDOUT "# line $lno; err $$v[0] $line
1092 print STDOUT "# $$v[1]; \n";
1097 # Echo NON-ERRORS to STDOUT
1100 print STDOUT "$line\n" unless $opt{q};
1105 close $filehandle unless $zonefile eq '-';
1109 p $output, "Error: Trouble opening '$zonefile'; $!";
1114 if ($verrs_total + $perrs_total)
1116 my $exitcode = $verrs_total > 0 ? 1 : 0;
1117 $exitcode += $perrs_total > 0 ? 2 : 0;