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);
900 valtz
$VERSION - validate tinydns-data files
903 $0 [-r
] [-R
] [-i
] tinydns-file1
[tinydns-file2
...]
905 $0 [-HiIqrRst
] [-T types
] -f tinydns-file1
[tinydns-file2
...]
907 $0 valtz
[-fHiIqrRst
] [-T types
] -F filter-file1
[filter-file2
...]
910 -h
print usage information
911 -f filter invalid lines
(filter mode
)
912 -F filter using configuration files
(advanced filter mode
)
913 -r allow
"fqdn" fields to be empty
914 -R allow
"mname" and "p" fields to be empty
915 -i allow
"ip" fields to be empty
916 -I include rejected lines as comments
(filtering only
)
917 -q don
't print valid lines to standard out (filtering only)
918 -s don't skip temporary
and backup files
919 -t don
't ignore comment lines (filtering only)
920 -T <types> allow additional record types (advanced filtering only)
922 Errors are generally printed to standard error, and the exit code
923 shall reflect the presense of both usage and validation errors. See
924 the man page for details.
929 if ($opt{h} || $opt{H} || $opt{'?'}) {
932 # If they asked for help, ignore whatever else they may have done
937 if (@{$files} == 0) {
946 for my $file (@{$files})
948 my $result = do_filterfile($file, $cache);
955 my $output = [ \*STDERR ];
957 for my $zonefile (sort @{$files})
961 next if $zonefile =~ /$FILESUFFIXREGEXP/i;
964 my $filehandle = \*STDIN;
966 if ($zonefile ne '-')
968 $fopen = open( $filehandle, $zonefile );
975 while (<$filehandle>)
980 my $v = validate_line($line);
985 my $temp = ($zonefile eq '-') ? '<STDIN
>' : $zonefile;
986 p $output, "File $temp" unless $errs;
989 $$v[1] =~ s/\n/\n /g;
990 p $output, " line $lno; err $$v[0] $line\n ".$$v[1];
993 print STDOUT "# line $lno; err $$v[0] $line
994 print STDOUT "# $$v[1]; \n";
999 # Echo NON-ERRORS to STDOUT
1002 print STDOUT "$line\n" unless $opt{q};
1007 close $filehandle unless $zonefile eq '-';
1011 p $output, "Error: Trouble opening '$zonefile'; $!";
1016 if ($verrs_total + $perrs_total)
1018 my $exitcode = $verrs_total > 0 ? 1 : 0;
1019 $exitcode += $perrs_total > 0 ? 2 : 0;