-rwxr-xr-x | vhostcname/vhostcname | 744 |
1 files changed, 557 insertions, 187 deletions
diff --git a/vhostcname/vhostcname b/vhostcname/vhostcname index 359101c..fc6dd2d 100755 --- a/vhostcname/vhostcname +++ b/vhostcname/vhostcname | |||
@@ -1,3 +1,3 @@ | |||
1 | #!/usr/bin/perl | 1 | #!/usr/bin/perl |
2 | # Copyright (C) 2014 Sergey Poznyakoff <gray@gnu.org> | 2 | # Copyright (C) 2014-2016 Sergey Poznyakoff <gray@gnu.org> |
3 | # | 3 | # |
@@ -24,26 +24,31 @@ use Net::DNS; | |||
24 | 24 | ||
25 | my $script; # This script name; | 25 | my $progname; # This script name; |
26 | 26 | my $progdescr = "update DNS from Apache virtual host configuration"; | |
27 | my $config_file = "/etc/vhostcname.conf"; | 27 | my $config_file = "/etc/vhostcname.conf"; |
28 | my %config = ( | ||
29 | core => { | ||
30 | 'cache' => "/var/run/vhostcname.cache", | ||
31 | # Default TTL. | ||
32 | 'ttl' => 3600, | ||
33 | # A globbing pattern for Apache configuration files. | ||
34 | 'apache-config-pattern' => "*", | ||
35 | } | ||
36 | ); | ||
37 | |||
38 | use constant EX_OK => 0; | ||
39 | use constant EX_NOTUPDATED => 1; | ||
40 | use constant EX_USAGE => 64; | ||
41 | use constant EX_NOINPUT => 66; | ||
42 | use constant EX_CANTCREAT => 73; | ||
43 | use constant EX_CONFIG => 78; | ||
28 | 44 | ||
29 | my $cnamelist = "/var/run/vhostcname.cache"; | ||
30 | my $host; # This host name. | 45 | my $host; # This host name. |
31 | my @zone; # List of acceptable DNS zones. | ||
32 | my $nameserver; # Nameserver to use for updates. | 46 | my $nameserver; # Nameserver to use for updates. |
33 | my @tsig_args; # Arguments to sing_tsig (path to the DNSSEC key file, or | ||
34 | # the key name and hash. | ||
35 | my $ttl = 3600; # Default TTL. | ||
36 | my $confdir; # Apache configuration directory. | ||
37 | my $confpat = "*"; # A globbing pattern for Apache configuration files. | ||
38 | my $dry_run; # Dry-run mode. | 47 | my $dry_run; # Dry-run mode. |
39 | my $debug; # Debug level. | 48 | my $debug; # Debug level. |
40 | my $allow_wildcard_domains; | ||
41 | |||
42 | my $help; # Display help summary. | ||
43 | my $man; # Ditto in manpage format. | ||
44 | 49 | ||
45 | my $status = 0; # Default exit status. | 50 | my $status = EX_OK;# Default exit status. |
46 | 51 | ||
47 | sub err { | 52 | sub err { |
48 | print STDERR "$script: "; | 53 | print STDERR "$progname: "; |
49 | print STDERR $_ for (@_); | 54 | print STDERR $_ for (@_); |
@@ -58,21 +63,189 @@ sub abend { | |||
58 | 63 | ||
59 | sub read_config_file($) { | 64 | sub parse_section { |
60 | my $file = shift; | 65 | my ($conf, $input) = @_; |
61 | unless (-f $file) { | 66 | my $ref = $conf; |
62 | print STDERR "$script: configuration file $file does not exist\n" | 67 | my $quote; |
63 | if ($debug); | 68 | my $rootname; |
64 | return; | 69 | while ($input ne '') { |
70 | my $name; | ||
71 | if (!defined($quote)) { | ||
72 | if ($input =~ /^"(.*)/) { | ||
73 | $quote = ''; | ||
74 | $input = $1; | ||
75 | } elsif ($input =~ /^(.+?)(?:\s+|")(.*)/) { | ||
76 | $name = $1; | ||
77 | $input = $2; | ||
78 | } else { | ||
79 | $name = $input; | ||
80 | $input = ''; | ||
81 | } | ||
82 | } else { | ||
83 | if ($input =~ /^([^\\"]*)\\(.)(.*)/) { | ||
84 | $quote .= $1 . $2; | ||
85 | $input = $3; | ||
86 | } elsif ($input =~ /^([^\\"]*)"\s*(.*)/) { | ||
87 | $name = $quote . $1; | ||
88 | $input = $2; | ||
89 | $quote = undef; | ||
90 | } else { | ||
91 | die "unparsable input $input"; | ||
92 | } | ||
93 | } | ||
94 | |||
95 | if (defined($name)) { | ||
96 | $rootname = $name unless defined $rootname; | ||
97 | $ref->{$name} = {} unless ref($ref->{$name}) eq 'HASH'; | ||
98 | $ref = $ref->{$name}; | ||
99 | $name = undef; | ||
65 | } | 100 | } |
66 | print STDERR "$script: reading $file\n" if ($debug); | 101 | } |
67 | open(my $fd, "<", $file) or abend(1, "cannot open $file: $!"); | 102 | return ($ref, $rootname); |
103 | } | ||
104 | |||
105 | sub check_mandatory { | ||
106 | my ($section, $kw, $loc, $s) = @_; | ||
107 | my $err = 0; | ||
108 | while (my ($k, $d) = each %{$kw}) { | ||
109 | if (ref($d) eq 'HASH' | ||
110 | and $d->{mandatory} | ||
111 | and !exists($section->{$k})) { | ||
112 | if (exists($d->{section})) { | ||
113 | if ($s) { | ||
114 | err("$loc: mandatory section [$k] not present"); | ||
115 | ++$err; | ||
116 | } | ||
117 | } else { | ||
118 | err("$loc: mandatory variable \"$k\" not set"); | ||
119 | ++$err; | ||
120 | } | ||
121 | } | ||
122 | } | ||
123 | return $err; | ||
124 | } | ||
125 | |||
126 | sub readconfig { | ||
127 | my $file = shift; | ||
128 | my $conf = shift; | ||
129 | my %param = @_; | ||
130 | |||
131 | # debug(2, "reading $file"); | ||
132 | open(my $fd, "<", $file) | ||
133 | or do { | ||
134 | err("can't open configuration file $file: $!"); | ||
135 | return 1 if $param{include}; | ||
136 | exit(EX_NOINPUT); | ||
137 | }; | ||
138 | |||
139 | my $line; | ||
140 | my $err; | ||
141 | my $section = $conf; | ||
142 | my $kw = $param{kw}; | ||
143 | my $include = 0; | ||
144 | my $rootname; | ||
145 | |||
68 | while (<$fd>) { | 146 | while (<$fd>) { |
147 | ++$line; | ||
69 | chomp; | 148 | chomp; |
149 | if (/\\$/) { | ||
150 | chop; | ||
151 | $_ .= <$fd>; | ||
152 | redo; | ||
153 | } | ||
154 | |||
70 | s/^\s+//; | 155 | s/^\s+//; |
71 | s/\s+$//; | 156 | s/\s+$//; |
72 | s/\s+=\s+/=/; | ||
73 | s/#.*//; | 157 | s/#.*//; |
74 | next if ($_ eq ""); | 158 | next if ($_ eq ""); |
75 | unshift(@ARGV, "--$_"); | 159 | |
160 | if (/^\[(.+?)\]$/) { | ||
161 | $include = 0; | ||
162 | my $arg = $1; | ||
163 | $arg =~ s/^\s+//; | ||
164 | $arg =~ s/\s+$//; | ||
165 | if ($arg eq 'include') { | ||
166 | $include = 1; | ||
167 | } else { | ||
168 | ($section, $rootname) = parse_section($conf, $1); | ||
169 | if (ref($param{kw}) eq 'HASH') { | ||
170 | if (defined($rootname)) { | ||
171 | if (ref($param{kw}{$rootname}) eq 'HASH' | ||
172 | and exists($param{kw}{$rootname}{section})) { | ||
173 | $kw = $param{kw}{$rootname}{section}; | ||
174 | } else { | ||
175 | err("$file:$line: unknown section"); | ||
176 | $kw = undef; | ||
177 | } | ||
178 | } else { | ||
179 | $kw = $param{kw}; | ||
180 | } | ||
76 | } | 181 | } |
77 | close($fd); | 182 | } |
183 | } elsif (/([\w_-]+)\s*=\s*(.*)/) { | ||
184 | my ($k, $v) = ($1, $2); | ||
185 | $k = lc($k) if $param{ci}; | ||
186 | |||
187 | if ($include) { | ||
188 | if ($k eq 'path') { | ||
189 | $err += readconfig($v, $conf, include => 1, @_); | ||
190 | } elsif ($k eq 'pathopt') { | ||
191 | $err += readconfig($v, $conf, include => 1, @_) | ||
192 | if -f $v; | ||
193 | } elsif ($k eq 'glob') { | ||
194 | foreach my $file (bsd_glob($v, 0)) { | ||
195 | $err += readconfig($file, $conf, include => 1, @_); | ||
196 | } | ||
197 | } else { | ||
198 | err("$file:$line: unknown keyword"); | ||
199 | ++$err; | ||
200 | } | ||
201 | next; | ||
202 | } | ||
203 | |||
204 | if (defined($kw)) { | ||
205 | my $x = $kw->{$k}; | ||
206 | if (!defined($x)) { | ||
207 | err("$file:$line: unknown keyword $k"); | ||
208 | ++$err; | ||
209 | next; | ||
210 | } elsif (ref($x) eq 'HASH') { | ||
211 | if (exists($x->{re})) { | ||
212 | if ($v !~ /$x->{re}/) { | ||
213 | err("$file:$line: invalid value for $k"); | ||
214 | ++$err; | ||
215 | next; | ||
216 | } | ||
217 | if (exists($x->{check}) | ||
218 | and !&{$x->{check}}($k, $v, "$file:$line")) { | ||
219 | ++$err; | ||
220 | next; | ||
221 | } | ||
222 | } elsif (exists($x->{check})) { | ||
223 | if (!&{$x->{check}}($k, $v, "$file:$line")) { | ||
224 | ++$err; | ||
225 | next; | ||
226 | } | ||
227 | } elsif (!exists($x->{var}) and | ||
228 | !exists($x->{parser}) and | ||
229 | !exists($x->{mandatory})) { | ||
230 | err("$file:$line: unknown keyword $k"); | ||
231 | ++$err; | ||
232 | next; | ||
233 | } | ||
234 | if (exists($x->{parser}) | ||
235 | and !&{$x->{parser}}($k, \$v, "$file:$line")) { | ||
236 | ++$err; | ||
237 | next; | ||
238 | } | ||
239 | } | ||
240 | } | ||
241 | |||
242 | $section->{$k} = $v; | ||
243 | } else { | ||
244 | err("$file:$line: malformed line"); | ||
245 | ++$err; | ||
246 | next; | ||
247 | } | ||
248 | } | ||
249 | close $fd; | ||
250 | exit(EX_CONFIG) if $err; | ||
78 | } | 251 | } |
@@ -85,3 +258,3 @@ sub valid_domain_name { | |||
85 | my $name = shift; | 258 | my $name = shift; |
86 | $name =~ s/^\*\.// if ($allow_wildcard_domains); | 259 | $name =~ s/^\*\.// if ($config{core}{'allow-wildcards'}); |
87 | foreach my $label (split(/\./, $name)) { | 260 | foreach my $label (split(/\./, $name)) { |
@@ -99,5 +272,5 @@ sub get_cnames($) { | |||
99 | 272 | ||
100 | foreach my $file (glob "$dir/$confpat") { | 273 | foreach my $file (glob "$dir/$config{core}{'apache-config-pattern'}") { |
101 | next unless (-f $file); | 274 | next unless (-f $file); |
102 | print STDERR "$script: reading cnames from $file\n" if ($debug > 2); | 275 | print STDERR "$progname: reading cnames from $file\n" if ($debug > 2); |
103 | 276 | ||
@@ -116,9 +289,9 @@ sub get_cnames($) { | |||
116 | unless (valid_domain_name($name)) { | 289 | unless (valid_domain_name($name)) { |
117 | print STDERR "$script: $file:$line: $name: invalid domain name\n"; | 290 | print STDERR "$progname: $file:$line: $name: invalid domain name\n"; |
118 | next; | 291 | next; |
119 | } | 292 | } |
120 | foreach my $z (@zone) { | 293 | foreach my $z (keys %{$config{zone}}) { |
121 | if ($name =~ /.*\.$z$/) { | 294 | if ($name =~ /.*\.$z$/) { |
122 | if ($name =~ /^\*\.(.+)/ and $1 eq $z) { | 295 | if ($name =~ /^\*\.(.+)/ and $1 eq $z) { |
123 | print STDERR "$script: $file:$line: $name: first-level wildcard\n"; | 296 | print STDERR "$progname: $file:$line: $name: first-level wildcard\n"; |
124 | next; | 297 | next; |
@@ -143,3 +316,3 @@ sub read_cname_list($) { | |||
143 | if (-f $file) { | 316 | if (-f $file) { |
144 | open(my $fd, "<", $file) or abend(1, "cannot open $file: $!"); | 317 | open(my $fd, "<", $file) or abend(EX_NOINPUT, "cannot open $file: $!"); |
145 | while (<$fd>) { | 318 | while (<$fd>) { |
@@ -163,3 +336,4 @@ sub write_cname_list { | |||
163 | 336 | ||
164 | open(my $fd, ">", $file) or abend(1, "cannot open $file for writing: $!"); | 337 | open(my $fd, ">", $file) or |
338 | abend(EX_CANTCREAT, "cannot open $file for writing: $!"); | ||
165 | foreach my $h (sort keys %hash) { | 339 | foreach my $h (sort keys %hash) { |
@@ -171,3 +345,2 @@ sub write_cname_list { | |||
171 | sub ns_update { | 345 | sub ns_update { |
172 | my $resolver = shift; | ||
173 | my $name = shift; | 346 | my $name = shift; |
@@ -177,3 +350,5 @@ sub ns_update { | |||
177 | 350 | ||
178 | print STDERR "$script: updating $name in $domain: ". | 351 | my $resolver = get_zone_resolver($domain); |
352 | |||
353 | print STDERR "$progname: updating $name in $domain: ". | ||
179 | join(',', map { "$_ => $hash{$_}" } keys %hash) . | 354 | join(',', map { "$_ => $hash{$_}" } keys %hash) . |
@@ -191,3 +366,3 @@ sub ns_update { | |||
191 | } | 366 | } |
192 | $update->sign_tsig(@tsig_args) if ($#tsig_args >= 0); | 367 | zone_sign_tsig($update, $domain); |
193 | my $reply = $resolver->send($update); | 368 | my $reply = $resolver->send($update); |
@@ -195,5 +370,5 @@ sub ns_update { | |||
195 | if ($reply->header->rcode eq 'NOERROR') { | 370 | if ($reply->header->rcode eq 'NOERROR') { |
196 | print STDERR "$script: update successful\n" if ($debug>3); | 371 | print STDERR "$progname: update successful\n" if ($debug>3); |
197 | } elsif ($ignorerr{$reply->header->rcode}) { | 372 | } elsif ($ignorerr{$reply->header->rcode}) { |
198 | print STDERR "$script: ignoring " . $reply->header->rcode . ': ' . | 373 | print STDERR "$progname: ignoring " . $reply->header->rcode . ': ' . |
199 | join(',', map { "$_ => $hash{$_}" } keys %hash) . "\n" | 374 | join(',', map { "$_ => $hash{$_}" } keys %hash) . "\n" |
@@ -205,3 +380,3 @@ sub ns_update { | |||
205 | $reply->header->rcode); | 380 | $reply->header->rcode); |
206 | $status = 2; | 381 | $status = EX_NOTUPDATED; |
207 | return 0; | 382 | return 0; |
@@ -213,3 +388,3 @@ sub ns_update { | |||
213 | $resolver->errorstring); | 388 | $resolver->errorstring); |
214 | $status = 2; | 389 | $status = EX_NOTUPDATED; |
215 | return 0; | 390 | return 0; |
@@ -219,2 +394,33 @@ sub ns_update { | |||
219 | 394 | ||
395 | sub get_zone_resolver { | ||
396 | my $zone = shift; | ||
397 | unless (defined($config{zone}{$zone}{resolver})) { | ||
398 | my $resolver = new Net::DNS::Resolver; | ||
399 | $resolver->nameservers($config{zone}{$zone}{server}) | ||
400 | if defined $config{zone}{$zone}{server}; | ||
401 | $config{zone}{$zone}{resolver} = $resolver; | ||
402 | } | ||
403 | return $config{zone}{$zone}{resolver}; | ||
404 | } | ||
405 | |||
406 | sub zone_sign_tsig { | ||
407 | my ($update, $zone) = @_; | ||
408 | my @tsig_args; | ||
409 | |||
410 | my $zcfg = $config{zone}{$zone}; | ||
411 | if (exists($zcfg->{'ns-key-file'})) { | ||
412 | push @tsig_args, split(/\s+/, $zcfg->{'ns-key-file'}); | ||
413 | } elsif (exists($zcfg->{'ns-key'})) { | ||
414 | push @tsig_args, @{$zcfg->{'ns-key'}}; | ||
415 | } | ||
416 | if ($#tsig_args == -1) { | ||
417 | if (exists($config{core}{'ns-key-file'})) { | ||
418 | push @tsig_args, split(/\s+/, $config{core}{'ns-key-file'}); | ||
419 | } elsif (exists($config{core}{'ns-key'})) { | ||
420 | push @tsig_args, @{$config{core}{'ns-key'}}; | ||
421 | } | ||
422 | } | ||
423 | $update->sign_tsig(@tsig_args) if ($#tsig_args >= 0); | ||
424 | } | ||
425 | |||
220 | sub update_cnames_from_hash { | 426 | sub update_cnames_from_hash { |
@@ -222,8 +428,8 @@ sub update_cnames_from_hash { | |||
222 | 428 | ||
223 | print STDERR "$script: " . keys(%hash) . " names to update\n" | 429 | print STDERR "$progname: " . keys(%hash) . " names to update\n" |
224 | if ($debug > 2); | 430 | if ($debug > 2); |
225 | my %oldhash = read_cname_list($cnamelist); | 431 | my %oldhash = read_cname_list($config{core}{cache}); |
226 | my @namelist = sort(keys(%hash)); | 432 | my @namelist = sort(keys(%hash)); |
227 | if (join(",", @namelist) eq join(".", sort(keys(%oldhash)))) { | 433 | if (join(",", @namelist) eq join(",", sort(keys(%oldhash)))) { |
228 | print STDERR "$script: nothing to update\n" if ($debug); | 434 | print STDERR "$progname: nothing to update\n" if ($debug); |
229 | return; | 435 | return; |
@@ -231,5 +437,2 @@ sub update_cnames_from_hash { | |||
231 | 437 | ||
232 | my $resolver = new Net::DNS::Resolver; | ||
233 | $resolver->nameservers($nameserver) if defined($nameserver); | ||
234 | |||
235 | my $name; | 438 | my $name; |
@@ -239,3 +442,3 @@ sub update_cnames_from_hash { | |||
239 | } else { | 442 | } else { |
240 | ns_update($resolver, $name, $hash{$name}, | 443 | ns_update($name, $hash{$name}, |
241 | prereq => yxdomain($name), | 444 | prereq => yxdomain($name), |
@@ -243,6 +446,6 @@ sub update_cnames_from_hash { | |||
243 | ignore => 'NXDOMAIN'); | 446 | ignore => 'NXDOMAIN'); |
244 | print STDERR "$script: $name $ttl CNAME $host\n" if ($debug); | 447 | print STDERR "$progname: $name $config{core}{ttl} CNAME $config{core}{hostname}\n" if ($debug); |
245 | delete $hash{$name} | 448 | delete $hash{$name} |
246 | unless ns_update($resolver, $name, $hash{$name}, | 449 | unless ns_update($name, $hash{$name}, |
247 | update => rr_add("$name $ttl CNAME $host")); | 450 | update => rr_add("$name $config{core}{ttl} CNAME $config{core}{hostname}")); |
248 | } | 451 | } |
@@ -251,3 +454,3 @@ sub update_cnames_from_hash { | |||
251 | foreach $name (keys %oldhash) { | 454 | foreach $name (keys %oldhash) { |
252 | ns_update($resolver, $name, $oldhash{$name}, | 455 | ns_update($name, $oldhash{$name}, |
253 | prereq => yxrrset("$name CNAME"), | 456 | prereq => yxrrset("$name CNAME"), |
@@ -257,3 +460,3 @@ sub update_cnames_from_hash { | |||
257 | 460 | ||
258 | write_cname_list($cnamelist, %hash); | 461 | write_cname_list($config{core}{cache}, %hash); |
259 | } | 462 | } |
@@ -265,13 +468,10 @@ sub update_cnames_from_dir($) { | |||
265 | sub nscleanup { | 468 | sub nscleanup { |
266 | print STDERR "$script: Removing DNS CNAME records\n" if ($debug); | 469 | print STDERR "$progname: Removing DNS CNAME records\n" if ($debug); |
267 | |||
268 | my $resolver = new Net::DNS::Resolver; | ||
269 | $resolver->nameservers($nameserver) if defined($nameserver); | ||
270 | 470 | ||
271 | my %hash = read_cname_list($cnamelist); | 471 | my %hash = read_cname_list($config{core}{cache}); |
272 | foreach my $name (keys %hash) { | 472 | foreach my $name (keys %hash) { |
273 | print STDERR "$script: removing $name from $hash{$name}\n" | 473 | print STDERR "$progname: removing $name from $hash{$name}\n" |
274 | if ($debug); | 474 | if ($debug); |
275 | delete $hash{$name} | 475 | delete $hash{$name} |
276 | if ns_update($resolver, $name, $hash{$name}, | 476 | if ns_update($name, $hash{$name}, |
277 | prereq => yxrrset("$name CNAME"), | 477 | prereq => yxrrset("$name CNAME"), |
@@ -281,48 +481,161 @@ sub nscleanup { | |||
281 | 481 | ||
282 | write_cname_list($cnamelist, %hash); | 482 | write_cname_list($config{core}{cache}, %hash); |
483 | } | ||
484 | |||
485 | ### | ||
486 | sub com_start { | ||
487 | abend(EX_USAGE, "too many arguments") unless $#_ == 0; | ||
488 | nscleanup(); | ||
489 | com_reload(@_); | ||
490 | } | ||
491 | |||
492 | sub com_reload { | ||
493 | abend(EX_USAGE, "too many arguments") unless $#_ == 0; | ||
494 | my $confdir = -d "$config{core}{'apache-config-directory'}/sites-enabled" | ||
495 | ? "$config{core}{'apache-config-directory'}/sites-enabled" | ||
496 | : $config{core}{'apache-config-directory'}; | ||
497 | my %cnames = get_cnames($confdir); | ||
498 | update_cnames_from_hash(%cnames); | ||
499 | print STDERR "$progname: no cnames defined\n" unless (keys(%cnames) > 0); | ||
283 | } | 500 | } |
284 | 501 | ||
502 | sub com_stop { | ||
503 | abend(EX_USAGE, "too many arguments") unless $#_ == 0; | ||
504 | nscleanup; | ||
505 | } | ||
506 | |||
507 | sub com_status { | ||
508 | err("status command ignored"); | ||
509 | my %stat; | ||
510 | |||
511 | my %hash = read_cname_list($config{core}{cache}); | ||
512 | while (my ($name, $zone) = each %hash) { | ||
513 | #$name =~ s/.$zone$//; | ||
514 | push @{${stat}{$zone}}, $name; | ||
515 | } | ||
516 | |||
517 | foreach my $zone (sort(keys %stat)) { | ||
518 | print "Names in zone $zone:\n"; | ||
519 | foreach my $name (sort(@{$stat{$zone}})) { | ||
520 | print " $name\n"; | ||
521 | } | ||
522 | } | ||
523 | } | ||
285 | 524 | ||
286 | ### | 525 | ### |
287 | ($script = $0) =~ s/.*\///; | 526 | ($progname = $0) =~ s/.*\///; |
527 | |||
528 | my %comtab = ( | ||
529 | start => \&com_start, | ||
530 | restart => \&com_reload, | ||
531 | 'force-restart' => \&com_start, | ||
532 | reload => \&com_reload, | ||
533 | stop => \&com_stop, | ||
534 | status => \&com_status | ||
535 | ); | ||
536 | |||
537 | sub getcom { | ||
538 | my $com = shift; | ||
539 | |||
540 | while (defined($comtab{$com}) and ref($comtab{$com}) ne 'CODE') { | ||
541 | $com = $comtab{$com}; | ||
542 | } | ||
543 | die "internal error: unresolved command alias" unless defined $com; | ||
544 | return $comtab{$com} if defined $comtab{$com}; | ||
545 | |||
546 | my @v = map { /^$com/ ? $_ : () } sort keys %comtab; | ||
547 | if ($#v == -1) { | ||
548 | abend(EX_USAGE, "unrecognized command"); | ||
549 | } elsif ($#v > 0) { | ||
550 | abend(EX_USAGE, "ambiguous command: ".join(', ', @v)); | ||
551 | } | ||
552 | return getcom($v[0]); | ||
553 | } | ||
554 | |||
288 | 555 | ||
289 | ## Read configuration | 556 | ## Read configuration |
290 | read_config_file($ENV{'VHOSTCNAME_CONF'} ? | 557 | sub parse_ns_key { |
291 | $ENV{'VHOSTCNAME_CONF'} : $config_file); | 558 | my ($var, $ref, $loc) = @_; |
559 | my @result; | ||
560 | if ($$ref =~ /(.+?)=(.+)/) { | ||
561 | push @result, $1, $2; | ||
562 | $$ref = \@result; | ||
563 | } else { | ||
564 | err("$loc: $var argument must be must be NAME=KEY"); | ||
565 | return 0; | ||
566 | } | ||
567 | return 1; | ||
568 | } | ||
292 | 569 | ||
293 | GetOptions("help" => \$man, | 570 | sub parse_boolean { |
294 | "h" => \$help, | 571 | my ($var, $ref, $loc) = @_; |
295 | "debug|d+" => \$debug, | 572 | my %bool = ( yes => 1, |
296 | "dry-run|n" => \$dry_run, | 573 | no => 0, |
297 | "hostname|H=s" => \$host, | 574 | true => 1, |
298 | "apache-config-pattern=s" => \$confpat, | 575 | false => 0, |
299 | "apache-config-directory=s" => \$confdir, | 576 | t => 1, |
300 | "ns-key-file=s" => sub { | 577 | nil => 0, |
301 | abend(3, "NS key already set") if ($#tsig_args >= 0); | 578 | f => 0, |
302 | push @tsig_args, $_[1]; | 579 | on => 1, |
303 | }, | 580 | off => 0, |
304 | "ns-key=s" => sub { | 581 | 1 => 1, |
305 | abend(3, "NS key already set") if ($#tsig_args >= 0); | 582 | 0 => 0); |
306 | if ($_[1] =~ /(.+?)=(.+)/) { | 583 | |
307 | push @tsig_args, $1; | 584 | my $s = $$ref; |
308 | push @tsig_args, $2; | 585 | $s =~ tr/A-Z/a-z/; |
586 | if (exists($bool{$s})) { | ||
587 | $$ref = $bool{$s}; | ||
309 | } else { | 588 | } else { |
310 | abend(3, "argument to --ns-key must be NAME=KEY"); | 589 | err("$loc: argument must be boolean"); |
590 | return 0; | ||
591 | } | ||
592 | return 1; | ||
311 | } | 593 | } |
594 | |||
595 | my %kw = ( | ||
596 | core => { | ||
597 | section => { | ||
598 | 'apache-config-directory' => 1, | ||
599 | 'apache-config-pattern' => 1, | ||
600 | 'cache' => 1, | ||
601 | 'server' => 1, | ||
602 | 'ttl' => 1, | ||
603 | 'ns-key' => { parser => \&parse_ns_key }, | ||
604 | 'ns-key-file' => 1, | ||
605 | 'hostname' => 1, | ||
606 | 'allow-wildcards' => { parser => \&parse_boolean } | ||
607 | }, | ||
312 | }, | 608 | }, |
313 | "cname-file=s" => \$cnamelist, | 609 | zone => { |
314 | "zone|z=s@" => \@zone, | 610 | section => { |
315 | "ttl=i" => \$ttl, | 611 | 'server' => 1, |
316 | "server=s" => \$nameserver, | 612 | 'ttl' => 1, |
317 | "allow-wildcard-domains" => \$allow_wildcard_domains | 613 | 'ns-key' => { parser => \&parse_ns_key }, |
318 | ) or exit(3); | 614 | 'ns-key-file' => 1 |
319 | 615 | }, | |
320 | pod2usage(-message => "$script: update DNS from Apache virtual host configuration", | 616 | } |
321 | -exitstatus => 0) if $help; | 617 | ); |
322 | pod2usage(-exitstatus => 0, -verbose => 2) if $man; | 618 | |
323 | 619 | GetOptions("help" => sub { | |
324 | unless (defined($confdir)) { | 620 | pod2usage(-exitstatus => EX_OK, -verbose => 2); |
621 | }, | ||
622 | "h" => sub { | ||
623 | pod2usage(-message => "$progname: $progdescr", | ||
624 | -exitstatus => EX_OK); | ||
625 | }, | ||
626 | "usage" => sub { | ||
627 | pod2usage(-exitstatus => EX_OK, -verbose => 0); | ||
628 | }, | ||
629 | |||
630 | "debug|d+" => \$debug, | ||
631 | "dry-run|n" => \$dry_run, | ||
632 | "config|c=s" => \$config_file, | ||
633 | ) or exit(EX_USAGE); | ||
634 | |||
635 | readconfig($config_file, \%config, kw => \%kw); | ||
636 | |||
637 | unless (defined($config{core}{'apache-config-directory'})) { | ||
325 | foreach my $dir ("/etc/apache2", "/etc/httpd") { | 638 | foreach my $dir ("/etc/apache2", "/etc/httpd") { |
326 | if (-e "$dir/sites-enabled" and -e "$dir/sites-available") { | 639 | if (-e "$dir/sites-enabled" and -e "$dir/sites-available") { |
327 | $confdir = $dir; | 640 | $config{core}{'apache-config-directory'} = $dir; |
328 | last; | 641 | last; |
@@ -330,3 +643,3 @@ unless (defined($confdir)) { | |||
330 | if (-e "$dir/vhosts.d") { | 643 | if (-e "$dir/vhosts.d") { |
331 | $confdir = "$dir/vhosts.d"; | 644 | $config{core}{'apache-config-directory'} = "$dir/vhosts.d"; |
332 | last; | 645 | last; |
@@ -334,9 +647,10 @@ unless (defined($confdir)) { | |||
334 | } | 647 | } |
335 | abend(3, | 648 | abend(EX_CONFIG, |
336 | "don't know where virtual host configurations are located; use --apache-config-directory option") | 649 | "don't know where virtual host configurations are located; define apache-config-directory") |
337 | unless defined($confdir); | 650 | unless defined($config{core}{'apache-config-directory'}); |
338 | } | 651 | } |
339 | 652 | ||
340 | $host = hostname() unless defined($host); | 653 | $config{core}{hostname} = hostname() unless defined($config{core}{hostname}); |
341 | push(@zone, $host) if ($#zone == -1); | 654 | $config{zone}{$host} = {} unless exists $config{zone}; |
655 | |||
342 | $debug++ if ($dry_run); | 656 | $debug++ if ($dry_run); |
@@ -344,4 +658,4 @@ $debug++ if ($dry_run); | |||
344 | if ($#ARGV == -1) { | 658 | if ($#ARGV == -1) { |
345 | abend(3, "command not given") unless ($ENV{'DIREVENT_FILE'}); | 659 | abend(EX_USAGE, "command not given") unless ($ENV{'DIREVENT_FILE'}); |
346 | print STDERR "$script: started as direvent handler for " . | 660 | print STDERR "$progname: started as direvent handler for " . |
347 | "$ENV{'DIREVENT_GENEV_NAME'} on $ENV{'DIREVENT_FILE'}\n" | 661 | "$ENV{'DIREVENT_GENEV_NAME'} on $ENV{'DIREVENT_FILE'}\n" |
@@ -350,5 +664,6 @@ if ($#ARGV == -1) { | |||
350 | my $update_dir; | 664 | my $update_dir; |
665 | my $confdir = $config{core}{'apache-config-directory'}; | ||
351 | if (-d "$confdir/sites-available" && -d "$confdir/sites-enabled") { | 666 | if (-d "$confdir/sites-available" && -d "$confdir/sites-enabled") { |
352 | if ($cwd eq "$confdir/sites-available") { | 667 | if ($cwd eq "$confdir/sites-available") { |
353 | foreach my $file (glob "$confdir/sites-enabled/$confpat") { | 668 | foreach my $file (glob "$confdir/sites-enabled/$config{core}{'apache-config-pattern'}") { |
354 | next unless (-l $file); | 669 | next unless (-l $file); |
@@ -368,18 +683,7 @@ if ($#ARGV == -1) { | |||
368 | update_cnames_from_dir($update_dir) if defined($update_dir); | 683 | update_cnames_from_dir($update_dir) if defined($update_dir); |
369 | } elsif ($#ARGV != 0) { | ||
370 | abend(3, "too many arguments"); | ||
371 | } elsif ($ARGV[0] =~ /^start|restart|force-restart|reload$/) { | ||
372 | nscleanup if ($ARGV[0] =~ /start$/); | ||
373 | my %cnames = get_cnames(-d "$confdir/sites-enabled" ? | ||
374 | "$confdir/sites-enabled" : $confdir); | ||
375 | update_cnames_from_hash(%cnames); | ||
376 | print STDERR "$script: no cnames defined\n" unless (keys(%cnames) > 0); | ||
377 | } elsif ($ARGV[0] eq "stop") { | ||
378 | nscleanup; | ||
379 | } elsif ($ARGV[0] eq "status") { | ||
380 | err("status command ignored"); | ||
381 | } else { | ||
382 | abend(3, "invalid command, try $script --help for more info"); | ||
383 | } | 684 | } |
384 | 685 | ||
686 | my $command = getcom($ARGV[0]); | ||
687 | &{$command}(@ARGV); | ||
688 | |||
385 | exit($status); | 689 | exit($status); |
@@ -433,2 +737,8 @@ corresponding B<direvent.conf>(5) entry: | |||
433 | 737 | ||
738 | Unless the program is started as a B<direvent>(8) handler, exactly one | ||
739 | command must be given in the command line. A command may be supplied | ||
740 | in full or abbreviated form. Any unambiguous abbreviation is allowed. | ||
741 | |||
742 | Available commands are: | ||
743 | |||
434 | =over 4 | 744 | =over 4 |
@@ -437,4 +747,4 @@ corresponding B<direvent.conf>(5) entry: | |||
437 | 747 | ||
438 | Scan the apache configuration files and register all server names matching | 748 | Scan the apache configuration files and register all server names that |
439 | the supplied zones. | 749 | match the configured zones. |
440 | 750 | ||
@@ -444,5 +754,14 @@ Deregister all hostnames registered previously. | |||
444 | 754 | ||
445 | =item B<restart>, B<force-restart>, B<reload> | 755 | =item B<restart>, B<reload> |
446 | 756 | ||
447 | Same as running B<vhostcname stop; vhostcname start>. | 757 | Builds a list of names from the apache configuration (I<apache-list>) and |
758 | compares them with the names registered at the previous run (I<cache>). If | ||
759 | the two lists differ, the names present in I<apache-list>, but absent in | ||
760 | I<cache> are registered. The names present in I<cache>, but lacking in | ||
761 | I<apache-list> are deleted from the DNS. | ||
762 | |||
763 | =item B<force-restart> | ||
764 | |||
765 | Deregister all hostnames registered previously, rescan Apache files, and | ||
766 | register all names that match the configured zones. | ||
448 | 767 | ||
@@ -450,3 +769,3 @@ Same as running B<vhostcname stop; vhostcname start>. | |||
450 | 769 | ||
451 | Ignored | 770 | Displays registered host names. |
452 | 771 | ||
@@ -458,9 +777,52 @@ Ignored | |||
458 | 777 | ||
459 | =item B<--allow-wildcard-domains> | 778 | =item B<-c>, B<--config=>I<FILE> |
779 | |||
780 | Read configuration from I<FILE> instead of the default location | ||
781 | (F</etc/vhostcname.conf>). | ||
782 | |||
783 | =item B<-d>, B<--debug> | ||
784 | |||
785 | Increases the debug level. Multiple B<-d> options are allowed. | ||
786 | |||
787 | =item B<-n>, B<--dry-run>, | ||
788 | |||
789 | Enables I<dry-run> mode: print what would have been done without actually | ||
790 | doing it. | ||
791 | |||
792 | =item B<--help> | ||
793 | |||
794 | Displays B<vhostcname> man page. | ||
795 | |||
796 | =item B<-h> | ||
797 | |||
798 | Displays a short help summary and exits. | ||
799 | |||
800 | =item B<--usage> | ||
801 | |||
802 | Displays a short command line syntax reminder. | ||
803 | |||
804 | =back | ||
805 | |||
806 | =head1 CONFIGURATION FILE | ||
807 | |||
808 | Configuration is read from F</etc/vhostcname.conf> or a file specified | ||
809 | by the B<--config> (B<-c>) command line option. The file consists of | ||
810 | a number of variable assignments (I<variable> B<=> I<value>), grouped into | ||
811 | sections. Whitespace is ignored, except that it serves to separate input | ||
812 | tokens. I<value> is read verbatim, including eventual whitespace characters | ||
813 | that can appear within it. | ||
814 | |||
815 | A section begins with the line containing its name within square brackets | ||
816 | (e.g. B<[core]>). The name can be followed by one or more arguments, if | ||
817 | the section semantics requires so (e.g. B<[zone example.com]>). | ||
818 | |||
819 | The following sections are recognized: | ||
820 | |||
821 | =over 4 | ||
822 | |||
823 | =item B<[core]> | ||
460 | 824 | ||
461 | Allow the use of wildcard (B<*>). When this option is in effect, a wildcard | 825 | =over 8 |
462 | will be allowed if it is the very first label in a domain name and it is | ||
463 | separated from the base zone (see the B<--zone> option) by one or more labels. | ||
464 | 826 | ||
465 | =item B<--apache-config-directory=>I<DIR> | 827 | =item B<apache-config-directory => I<DIR> |
466 | 828 | ||
@@ -476,3 +838,3 @@ terminate if unable to do that. | |||
476 | 838 | ||
477 | =item B<--apache-config-pattern=>I<GLOB> | 839 | =item B<apache-config-pattern => I<PATTERN> |
478 | 840 | ||
@@ -480,36 +842,47 @@ Shell globbing pattern for virtual host configuration files. By default, | |||
480 | B<*> is used, meaning that B<vhostcname> will scan all files in the | 842 | B<*> is used, meaning that B<vhostcname> will scan all files in the |
481 | configuration directory. | 843 | configuration directory (note: that includes backup copies too!). |
482 | 844 | ||
483 | =item B<--cname-file=>I<NAME> | 845 | =item B<cache => I<FILE> |
484 | 846 | ||
485 | Name of the file where B<vhostcname> will keep successfully registered | 847 | Name of the cache file where B<vhostcname> keeps successfully registered |
486 | host names. Default is B</var/run/vhostcname.cache>. | 848 | host names. Default is B</var/run/vhostcname.cache>. |
487 | 849 | ||
488 | =item B<-d>, B<--debug> | 850 | =item B<hostname => I<HOSTNAME> |
489 | 851 | ||
490 | Increases the debug level. Multiple B<-d> options are allowed. | 852 | Sets the hostname. Use this if B<vhostcname> is unable to correctly |
853 | determine it. | ||
491 | 854 | ||
492 | =item B<-n>, B<--dry-run>, | 855 | =item B<allow-wildcards => I<BOOL> |
493 | 856 | ||
494 | Enables I<dry-run> mode: print what would have been done without actually | 857 | Allow the use of wildcard (B<*>) in host names. When this option is in |
495 | doing it. | 858 | effect, a wildcard will be allowed if it is the very first label in a domain |
859 | name and it is separated from the base zone (see the B<zone> section) by one | ||
860 | more labels. | ||
496 | 861 | ||
497 | =item B<--help> | 862 | I<BOOL> is one of B<yes>, B<true>, B<t>, B<on>, or B<1> to allow wildcards, |
863 | or one of B<no>, B<false>, B<f>, B<nil>, B<off>, B<0> to disallow them (the | ||
864 | default). | ||
498 | 865 | ||
499 | Displays B<vhostcname> man page. | 866 | =back |
500 | 867 | ||
501 | =item B<-h> | 868 | The following variables provide defaults for zones that lack the |
869 | corresponding settings: | ||
502 | 870 | ||
503 | Displays a short help summary and exits. | 871 | =over 8 |
504 | 872 | ||
505 | =item B<-H>, B<--hostname>=I<NAME> | 873 | =item B<server => I<HOST> |
506 | 874 | ||
507 | Sets the hostname. Use this if B<vhostcname> is unable to correctly | 875 | Name of the DNS server to use. Normally B<vhostcname> determines what server |
508 | determine it. | 876 | to use based on the B<SOA> record of the zone to be updated, so this option |
877 | is rarely needed. | ||
509 | 878 | ||
510 | =item B<--ns-key=>I<NAME>=I<KEY> | 879 | =item B<ttl => I<SECONDS> |
511 | 880 | ||
512 | Define the TSIG key. | 881 | TTL value for new DNS records. Default is 3600. |
513 | 882 | ||
514 | =item B<--ns-key-file=>I<KEYFILE> | 883 | =item B<ns-key => I<NAME>=I<HASH> |
884 | |||
885 | Defines the TSIG key. | ||
886 | |||
887 | =item B<ns-key-file => I<FILE> | ||
515 | 888 | ||
@@ -519,46 +892,41 @@ file can be used. | |||
519 | 892 | ||
520 | This option cannot be used together with B<--ns-key>. | 893 | If both <ns-key> and B<ns-key-file> are used, the latter is given preference. |
521 | 894 | ||
522 | =item B<--server=>I<NAME> | 895 | =back |
523 | 896 | ||
524 | Name of the DNS server to use. Normally B<vhostcname> determines what server | 897 | =item B<[zone I<NAME>]> |
525 | to use based on the B<SOA> record of the zone to be updated, so this option | ||
526 | is rarely needed. | ||
527 | 898 | ||
528 | =item B<--ttl=>I<TIME> | 899 | The B<zone> section informs B<vhostcname> that it should handle names |
900 | in zone I<NAME>. Any number of B<[zone]> sections can be defined. If | ||
901 | none is defined, B<vhostcname> will take hostname as the name of the zone | ||
902 | to update. | ||
529 | 903 | ||
530 | TTL value for new DNS records. Default is 3600. | 904 | The variables in a B<zone> section define parameters to be used for that |
905 | particular zone. As such, none of them is mandatory. If the zone I<NAME> | ||
906 | uses default settings (or settings, defined in the B<[core]> section), | ||
907 | the section can be empty. | ||
531 | 908 | ||
532 | =item B<--zone=>I<NAME> | 909 | =over 8 |
533 | 910 | ||
534 | Name of the zone which B<vhostcname> can update. Multiple B<--zone> options | 911 | =item B<server => I<HOST> |
535 | can be given. | ||
536 | 912 | ||
537 | If no B<--zone> option is given, B<vhostcname> will take hostname as the | 913 | Name of the DNS server to use when updating this zone. |
538 | name of the zone. | ||
539 | 914 | ||
540 | =back | 915 | =item B<ttl => I<SECONDS> |
541 | 916 | ||
542 | =head1 CONFIGURATION FILE | 917 | TTL for records in this zone. |
543 | 918 | ||
544 | If the file B<etc/vhostcname.conf> exists, the program will read its | 919 | =item B<ns-key => I<NAME>=I<HASH> |
545 | configuration from it. A familiar UNIX configuration format is used. | ||
546 | Empty lines and UNIX comments are ignored. Each non-empty line is either an | ||
547 | option name, or option assignment, i.e. B<opt>=B<val>, with any amount of | ||
548 | optional whitespace around the equals sign. Valid option names are | ||
549 | the same as the long command line options, but without the leading B<-->. | ||
550 | For example: | ||
551 | 920 | ||
552 | zone = vhost.example.com | 921 | TSIG key. |
553 | ns-key-file = /etc/bind/Kvhost+157+43558.key | ||
554 | ttl = 3600 | ||
555 | 922 | ||
556 | =head1 ENVIRONMENT | 923 | =item B<ns-key-file => I<FILE> |
557 | 924 | ||
558 | =over 4 | 925 | Name of the key file. The argument should be the name of a file |
926 | generated by the B<dnssec-keygen> utility. Either B<.key> or B<.private> | ||
927 | file can be used. | ||
559 | 928 | ||
560 | =item B<VHOSTCNAME_CONF> | 929 | If both <ns-key> and B<ns-key-file> are used, the latter is given preference. |
561 | 930 | ||
562 | The name of the configuration file to use instead of the default | 931 | =back |
563 | F</etc/vhostcname.conf>. | ||
564 | 932 | ||
@@ -576,19 +944,21 @@ Success | |||
576 | 944 | ||
577 | Operating system error (unable to open file, etc.) | 945 | Some of the host names could not be updated. |
578 | 946 | ||
579 | =item 2 | 947 | =item 64 |
580 | 948 | ||
581 | Some of the host names could not be registered. | 949 | Command line usage error. |
582 | 950 | ||
583 | =item 3 | 951 | =item 66 |
584 | 952 | ||
585 | Command line usage error | 953 | Required input file cannot be opened. |
586 | 954 | ||
587 | =back | 955 | =item 73 |
588 | 956 | ||
589 | =head1 BUGS | 957 | Required output file cannot be created or written. |
590 | 958 | ||
591 | Only one key file can be given. This means that if you use multiple | 959 | =item 78 |
592 | B<--zone> options, all zones must be configured to accept the same | 960 | |
593 | DNSSEC key. Ditto for the B<--server> option. | 961 | Configuration error. |
962 | |||
963 | =back | ||
594 | 964 | ||