aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Changes29
-rw-r--r--Makefile.PL7
-rwxr-xr-xacmeman165
-rw-r--r--lib/App/Acmeman.pm33
-rw-r--r--lib/App/Acmeman/Config.pm4
-rw-r--r--lib/App/Acmeman/Domain.pm1
-rw-r--r--lib/App/Acmeman/Log.pm2
-rw-r--r--lib/App/Acmeman/Source.pm3
-rw-r--r--lib/App/Acmeman/Source/Apache.pm13
-rw-r--r--lib/App/Acmeman/Source/Pound.pm225
10 files changed, 462 insertions, 20 deletions
diff --git a/Changes b/Changes
index c89c932..c66f050 100644
--- a/Changes
+++ b/Changes
@@ -1,3 +1,32 @@
+3.09 2023-01-22
+
+ - New domain source: pound.
+
+3.08 2021-06-11
+
+ - Allow for multiple per-domain postrenew statements.
+
+3.07 2021-02-12
+
+ - Change bugtracker address.
+ - Change root certificate URL and make it configurable.
+
+3.06 2020-06-15
+
+ - Improve error reporting
+
+3.05 2020-06-14
+
+ - Fix manifest
+
+3.04 2020-06-14
+
+ - Rewrite Apache configuration layout support.
+ - Improve Apache setup procedure.
+ - Make sure the generated cert.pem file is terminated with a
+ newline.
+ - Accept ServerName value with the scheme prefix.
+
3.03 2019-12-23
- The following environment variables are set when running
diff --git a/Makefile.PL b/Makefile.PL
index ccc6a27..8ad492a 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -33,7 +33,8 @@ my %makefile_args = (
'Sys::Hostname' => 1.16,
'Apache::Defaults' => 1.02,
'Apache::Config::Preproc' => 1.04,
- 'Config::Parser' => 1.03
+ 'Config::Parser' => 1.03,
+ 'File::BackupCopy' => 1.00
},
MIN_PERL_VERSION => 5.016001,
META_MERGE => {
@@ -44,6 +45,10 @@ my %makefile_args = (
url => 'git://git.gnu.org.ua/acmeman.git',
web => 'http://git.gnu.org.ua/cgit/acmeman.git/',
},
+ bugtracker => {
+ web => 'https://puszcza.gnu.org.ua/bugs/?group=acmeman',
+ mailto => 'gray+acmeman@gnu.org.ua'
+ }
},
provides => Module::Metadata->provides(version => '1.4',
dir => 'lib')
diff --git a/acmeman b/acmeman
index 92fdc55..5225751 100755
--- a/acmeman
+++ b/acmeman
@@ -106,7 +106,7 @@ which have the form B<I<KW>=I<VAL>>, grouped into sections, declared
as B<[I<NAME>]> (square brackets are part of the syntax). Empty lines
and comments (introduced by a hash sign) are ignored.
-There are three main use cases
+There are four main use cases:
=head2 APACHE
@@ -269,6 +269,57 @@ Finally, define the backends.
=back
+=head2 Pound
+
+B<Pound> is a light-weight proxy server available from L<https://github.com/graygnuorg/pound>. It is supported by the B<acmeman> source B<pound>, which
+scans the B<pound> configuration file F</etc/pound.cfg>, and extracts domain
+names from the B<Host> directives in B<ListenHTTP> sections that contain
+the B<ACME> statement. Below is a short usage instruction for this source
+module. For a detailed discussion, refer to the section B<SOURCE>, subsection
+B<pound>.
+
+=over 4
+
+=item 1. Add the B<ACME> directive to the B<ListenHTTP> section of your
+F</etc/pound.cfg> file. Its argument is a directory on local file system
+where ACME challenge files will be stored. Make sure that this directory
+is the F<.well-known/acme-challenge> subdirectory of the B<rootdir> in your
+B<acmeman> configuration file.
+
+=item 2. In the same B<ListenHTTP> section, define hostnames which will obtain
+ACME certificates. Make sure to use B<Host> statements with exact string
+matching algorithm. If serving several host names, use the B<Match OR> block.
+
+=back
+
+After these two steps, your listener section will look like:
+
+ ListenHTTP
+ Address 0.0.0.0
+ Port 80
+ ACME "/var/lib/pound/acme/.well-known/acme-challenge"
+ Service
+ Match OR
+ Host "www.example.org"
+ Host "example.org"
+ End
+ ...
+ End
+ End
+
+=over 4
+
+=item 3. Configure B<acmeman>. Use the B<pound> source and make sure
+B<rootdir> is synchronized with the B<ACME> statement in F<pound.cfg>, as
+described in point 1. E.g.:
+
+ [core]
+ source = pound
+ rootdir = /var/lib/pound/acme
+ postrenew = /usr/bin/systemctl restart pound
+
+=back
+
=head2 Direct configuration
Use direct configuration if none of the provided source types can
@@ -426,6 +477,8 @@ it defines the apache configuration layout. Allowed values are: B<debian>,
B<slackware>, B<suse> and B<rh> (for Red Hat). Without arguments, the layout
will be autodetected.
+The B<pound> source gathers host names from B<pound> configuration file.
+
The B<file> source reads domain names from one or more disk files. A
mandatory argument specifies the name of the directory where the files
are located. This mode is suitable for use with B<haproxy> pattern files.
@@ -531,6 +584,9 @@ setting is used.
Run I<CMD> after successful update. If not given, the B<core.postrenew>
commands will be run.
+If more than one B<postrenew> statements are defined, they will be run in
+sequence, in the same order as they appeared in the configuration file.
+
I<CMD> is run in the environment inherited from the calling B<acmeman>
process with the following additional variables defined:
@@ -763,6 +819,111 @@ If the B<--host> (B<-h>) option is used, only one certificate will be
issued. The I<HOST> will be used as its B<CN>. All the domain names read
from the input files will form the list of its alternative names.
+=head2 pound
+
+ [core]
+ source = pound [--config=FILE] [--host=HOST] \
+ [--type=http|https] [--listener=NAME] \
+ [--comment=TEXT]
+
+Domain names will be read from I<FILE> or, if it is not supplied, from
+the default B<pound> configuration file F</etc/pound.cfg>. By default,
+the module will scan B<ListenHTTP> sections that have B<ACME> directive
+set and will extract domain names from B<Host> statements in its services.
+
+The B<--type> and B<--listener> options modify this behavior. The B<--type>
+option instructs B<pound> which type of listeners to scan. Setting it to
+B<http> means scanning only B<ListenHTTP> listeners (the default) and
+setting it to B<https> means scanning B<ListenHTTPS> listeners. To
+enable scanning both types of listeners, use B<--type=http --type=https>.
+When scanning B<ListenHTTPS>, the module collects all domain names that
+appear as arguments to B<Host> statements.
+
+If B<--listener> option is used, module will scan only the named listener.
+To select multiple listeners, use several B<--listener> options.
+
+The B<--comment> option defines a text, which, when appearing at the
+start of a comment line, enables host name collection. Such I<pragmatical>
+comments may appear anywhere within listener and service sections and their
+scope is limited by the corresponding section. When this option is used,
+host collection is disabled by default. For example, assuming
+B<--comment=acme>, the following configuration snippet (with irrelevant
+statements replaced by ellipses) will result in issuing certificate for
+C<example.org> and C<www.example.org>:
+
+ ListenerHTTP
+ # acme
+ Service
+ Host -exact "example.org"
+ ...
+ End
+
+ Service
+ Host -exact "www.example.org"
+ ...
+ End
+ End
+
+In contrast, when processing the following snippet, B<acmeman> will issue
+certificate for C<example.org> only:
+
+ ListenerHTTP
+ Service
+ # acme
+ Host -exact "example.org"
+ ...
+ End
+
+ Service
+ Host -exact "www.example.org"
+ ...
+ End
+ End
+
+Furthermore, using B<no-I<TEXT>> at the start of a comment cancels
+the effect of the previous pragmatic comment. This can be used for
+better control of host selection:
+
+ ListenerHTTP
+ Service
+ Match OR
+ # acme
+ Host -exact "example.org"
+ Host -exact "www.example.org"
+ # no-acme
+ Host -exact "test.example.org"
+ End
+ ...
+ End
+ End
+
+
+If the B<--host> (B<-h>) option is used, only one certificate will be
+issued. The I<HOST> will be used as its B<CN>. All the domain names read
+from the input files will form the list of its alternative names.
+
+Notice the limitations of this module:
+
+=over 4
+
+=item 1. Only B<Host> statements with exact string matching can be used.
+When declaring multiple hosts it might be tempting to use regular expression
+matching instead. Due to obvious reasons, the module won't be able to
+cope with it. When declaring multiple hosts, always use the B<Match OR>
+section, like this:
+
+ Match OR
+ Host -exact "host1"
+ Host -exact "host2"
+ Host -exact "host3"
+ End
+
+=item 2. These B<Host> statements (or the enclosing B<Match OR> section)
+must be declared in the B<Service> sections located under the B<ListenHTTP>
+or B<ListenHTTPS> sections. Global B<Service> sections are not scanned.
+
+=back
+
=head1 OPTIONS
=over 4
@@ -863,5 +1024,5 @@ GPLv3+: GNU GPL version 3 or later, see L<http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
-
+
=cut
diff --git a/lib/App/Acmeman.pm b/lib/App/Acmeman.pm
index fafcb09..a4cea29 100644
--- a/lib/App/Acmeman.pm
+++ b/lib/App/Acmeman.pm
@@ -8,6 +8,7 @@ use Crypt::OpenSSL::RSA;
use Crypt::OpenSSL::X509;
use File::Basename;
use File::Path qw(make_path);
+use File::Spec;
use DateTime::Format::Strptime;
use LWP::UserAgent;
use LWP::Protocol::https;
@@ -24,12 +25,14 @@ use Text::ParseWords;
use App::Acmeman::Log qw(:all :sysexits);
use feature 'state';
-our $VERSION = '3.03';
+our $VERSION = '3.09';
my $progdescr = "manages ACME certificates";
-my $letsencrypt_root_cert_url =
- 'https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem';
+our $acme_dir = '/etc/ssl/acme';
+our $letsencrypt_root_cert_basename = 'lets-encrypt-root.pem';
+our $letsencrypt_root_cert_url =
+ 'https://letsencrypt.org/certs/lets-encrypt-r3-cross-signed.pem';
sub new {
my $class = shift;
@@ -151,6 +154,7 @@ sub host_ns_ok {
foreach my $ip ($self->resolve($host)) {
return 1 if $self->myip($ip);
}
+ error("$host does not resolve to our IP");
return 0
}
@@ -203,7 +207,8 @@ sub setup {
$self->prep_dir($self->cf->get(qw(core rootdir)).'/file');
- $self->get_root_cert('/etc/ssl/acme/lets-encrypt-x3-cross-signed.pem');
+ $self->get_root_cert(File::Spec->catfile($acme_dir,
+ $letsencrypt_root_cert_basename));
foreach my $src ($self->cf->get(qw(core source))) {
unless ($src->setup(dry_run => $self->dry_run_option,
@@ -228,7 +233,11 @@ sub collect {
|| $self->host_ns_ok($_) }
($k, ($v->{alt} ? @{$v->{alt}} : ()))];
if (@$alt) {
- $k = shift @$alt;
+ my $name = shift @$alt;
+ if ($name ne $k) {
+ error("$k: CN changed to $name, update your configuration");
+ }
+ $k = $name;
$alt = undef unless @$alt;
} else {
error("ignoring $k: none of its names resolves to our IP");
@@ -312,12 +321,14 @@ sub renew {
foreach my $vhost ($self->selected_domains) {
if ($self->force_option || $self->domain_cert_expires($vhost)) {
if ($self->register_domain_certificate($vhost)) {
- if (my $cmd = $vhost->postrenew) {
+ if (my $postrenew = $vhost->postrenew) {
local $ENV{ACMEMAN_CERTIFICATE_FILE} =
$vhost->certificate_file;
local $ENV{ACMEMAN_DOMAIN_NAME} = $vhost;
local $ENV{ACMEMAN_ALT_NAMES} = join(' ', $vhost->alt);
- $self->runcmd($cmd, $vhost);
+ foreach my $cmd (@$postrenew) {
+ $self->runcmd($cmd, $vhost);
+ }
} else {
push @renewed, $vhost;
}
@@ -557,7 +568,13 @@ sub register_domain_certificate {
sleep 1
}
if ($ret ne 'valid') {
- error("$domain: can't renew certificate: authorization: $ret");
+ my $text = "authorization $ret";
+ if (my ($ch) = grep { $_->type() eq 'http-01' } $authz->challenges()) {
+ if (my $err = $ch->error()) {
+ $text .= ': ' . $err->to_string;
+ }
+ }
+ error("$domain: can't renew certificate: $text");
return 0;
}
}
diff --git a/lib/App/Acmeman/Config.pm b/lib/App/Acmeman/Config.pm
index 7c05985..f5bce2a 100644
--- a/lib/App/Acmeman/Config.pm
+++ b/lib/App/Acmeman/Config.pm
@@ -52,7 +52,7 @@ sub mangle {
if (my $fnode = $self->getnode('files')) {
while (my ($k, $v) = each %{$fnode->subtree}) {
$v->set('files', $k, 'type', 'split')
- unless $v->has_key('type', 'split');
+ unless $v->has_key('type');
if ($v->subtree('type') eq 'single') {
unless ($v->has_key('certificate-file')) {
$self->error("files.$k.certificate-file not defined");
@@ -143,7 +143,7 @@ __DATA__
alt = STRING :array
files = STRING
key-size = NUMBER
- postrenew = STRING
+ postrenew = STRING :array
diff --git a/lib/App/Acmeman/Domain.pm b/lib/App/Acmeman/Domain.pm
index 46c3f1d..97ec5ca 100644
--- a/lib/App/Acmeman/Domain.pm
+++ b/lib/App/Acmeman/Domain.pm
@@ -9,7 +9,6 @@ require Exporter;
our @ISA = qw(Exporter);
our @EXPORT_OK = qw(CERT_FILE KEY_FILE CA_FILE);
our %EXPORT_TAGS = ( files => [ qw(CERT_FILE KEY_FILE CA_FILE) ] );
-our $VERSION = '1.00';
use constant {
CERT_FILE => 0,
diff --git a/lib/App/Acmeman/Log.pm b/lib/App/Acmeman/Log.pm
index 72242c0..02f5d65 100644
--- a/lib/App/Acmeman/Log.pm
+++ b/lib/App/Acmeman/Log.pm
@@ -1,4 +1,6 @@
package App::Acmeman::Log;
+use strict;
+use warnings;
use File::Basename;
use parent 'Exporter';
diff --git a/lib/App/Acmeman/Source.pm b/lib/App/Acmeman/Source.pm
index abae342..95e7c03 100644
--- a/lib/App/Acmeman/Source.pm
+++ b/lib/App/Acmeman/Source.pm
@@ -35,7 +35,8 @@ sub is_set {
sub define_domain {
my $self = shift;
my $cn = shift || croak "domain name must be given";
- $self->set('domain', $cn, 'files', 'default');
+ $self->set('domain', $cn, 'files', 'default')
+ unless $self->is_set('domain', $cn, 'files');
}
sub define_alias {
diff --git a/lib/App/Acmeman/Source/Apache.pm b/lib/App/Acmeman/Source/Apache.pm
index a9a6195..1f5f7ac 100644
--- a/lib/App/Acmeman/Source/Apache.pm
+++ b/lib/App/Acmeman/Source/Apache.pm
@@ -66,7 +66,8 @@ sub examine_http_config {
foreach my $sect ($app->section(-name => "macro")) {
if ($sect->value =~ m{^(?ix)letsencryptssl
\s+
- (.+)}) {
+ (.+)}
+ && ! $self->is_set(qw(files apache))) {
$self->set(qw(files apache argument), $1);
map {
if ($_->name =~ m{^(?ix)
@@ -93,8 +94,10 @@ sub examine_http_config {
}
foreach my $sect ($app->section(-name => "virtualhost")) {
- my ($server_name) = (map { $self->dequote($_->value) }
- $sect->directive('servername'));
+ my ($server_name) = (map {
+ (my $s = $self->dequote($_->value)) =~ s{^https?://}{};
+ $s
+ } $sect->directive('servername'));
my @server_aliases = map { quotewords('\s+', 0,
$self->dequote($_->value)) }
$sect->directive('serveralias');
@@ -167,7 +170,7 @@ sub setup {
debug(2, "writing $filename");
unless ($args{dry_run}) {
my $challenge_dir = "$www_root/.well-known/acme-challenge";
- my $acme_dir = "/etc/ssl/acme";
+ my $acme_dir = $App::Acmeman::acme_dir;
foreach my $dir ($self->layout->incdir(), $challenge_dir, $acme_dir) {
unless ($self->mkpath($dir)) {
@@ -204,7 +207,7 @@ sub setup {
SSLCipherSuite ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:!DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA
SSLCertificateFile $acme_dir/\$domain/cert.pem
SSLCertificateKeyFile $acme_dir/\$domain/privkey.pem
- SSLCACertificateFile $acme_dir/lets-encrypt-x3-cross-signed.pem
+ SSLCACertificateFile $acme_dir/$App::Acmeman::letsencrypt_root_cert_basename
</Macro>
<Macro LetsEncryptServer \$domain>
diff --git a/lib/App/Acmeman/Source/Pound.pm b/lib/App/Acmeman/Source/Pound.pm
new file mode 100644
index 0000000..daf5373
--- /dev/null
+++ b/lib/App/Acmeman/Source/Pound.pm
@@ -0,0 +1,225 @@
+package App::Acmeman::Source::Pound;
+use strict;
+use warnings;
+use parent 'App::Acmeman::Source';
+use Getopt::Long qw(GetOptionsFromArray :config gnu_getopt no_ignore_case);
+use App::Acmeman::Log qw(:all);
+
+sub new {
+ my $class = shift;
+ my $cfgname = '/etc/pound.cfg';
+ my $host;
+ my @listener;
+ my @types;
+ my $comment;
+ GetOptionsFromArray(\@_,
+ 'config|f=s' => \$cfgname,
+ 'host|h=s' => \$host,
+ 'listener=s@' => \@listener,
+ 'type=s@' => \@types,
+ 'comment=s' => \$comment
+ );
+ my $self = bless {
+ cfgname => $cfgname,
+ host => $host,
+ }, $class;
+ if ($comment) {
+ $self->{comment} = qr($comment)
+ }
+ if (@listener) {
+ $self->{listener} = { map { $_ => 1 } @listener };
+ }
+ if (!@types) {
+ @types = qw(http)
+ }
+ $self->{types} = { map { lc($_) => 1 } @types };
+
+ return $self;
+}
+
+sub host {
+ my ($self, $arg) = @_;
+
+ my $file;
+ while ($arg =~ s/\s*-(\w+)//) {
+ $file = 1 if $1 eq 'file';
+ }
+ if ($file) {
+ $file = $self->dequote($arg);
+ if (open(my $fh, '<', $file)) {
+ my @hosts;
+ while (<$fh>) {
+ chomp;
+ s/^\s+//;
+ s/\s+$//;
+ next if (/^$/ || /^#/);
+ push @hosts, $_
+ }
+ close($fh);
+ return @hosts;
+ } else {
+ error("$self->{cfgname}:$.: can't open $file: $!");
+ return ()
+ }
+ }
+ return ($self->dequote($arg));
+}
+
+sub lstn_ok {
+ my ($self, $s, $name) = @_;
+ if (defined($self->{listener})) {
+ return 0 unless defined($name);
+ return exists($self->{listener}{$name})
+ }
+ return !defined($s) || $self->{types}{'http' . lc($s)};
+}
+
+sub scan {
+ my ($self) = @_;
+ debug(1, "initializing file list from $self->{cfgname}");
+ if ($self->{host}) {
+ $self->define_domain($self->{host});
+ }
+ open(my $fh, '<', $self->{cfgname})
+ or do {
+ error("can't open $self->{cfgname}: $!");
+ return 0;
+ };
+ use constant {
+ ST_INIT => 0,
+ ST_LISTENER => 1,
+ ST_SERVICE => 2,
+ ST_EXPEND => 3,
+ ST_MATCH => 4,
+ ST_IGNORE => 5
+ };
+ my $state = ST_INIT;
+ my $acme;
+ my @collect_state;
+ my @lsthosts;
+ my @srvhosts;
+ my $endcnt;
+ while (<$fh>) {
+ chomp;
+
+ s/^\s+//;
+ if ($self->{comment} && m{#\s*(no-)?$self->{comment}}) {
+ if (@collect_state) {
+ my $hint = 1;
+ if ($1) {
+ $hint = 0;
+ }
+ debug(4, "$self->{cfgname}:$.: hint=$hint");
+ $collect_state[$#collect_state] = $hint;
+ }
+ }
+
+ s/#.*//;
+ next if (/^$/);
+
+ if ($state == ST_INIT) {
+ if (/^ListenHTTP(?:(?<s>S)?\s+"(?<name>.*)"\s*)?$/i) {
+ if ($self->lstn_ok($+{s}, $+{name})) {
+ debug(4, "$self->{cfgname}:$.: listener");
+ $state = ST_LISTENER;
+ $acme = 0;
+ if (defined($+{s}) && uc($+{s}) eq 'S') {
+ $acme = 1;
+ }
+ push @collect_state, !defined($self->{comment});
+ @lsthosts = ();
+ } else {
+ $state = ST_IGNORE;
+ }
+ }
+ } elsif ($state == ST_LISTENER) {
+ if (/^Service(?:\s+".*"\s*)?$/i) {
+ debug(4, "$self->{cfgname}:$.: service");
+ push @collect_state, $collect_state[$#collect_state];
+ $state = ST_SERVICE;
+ @srvhosts = ();
+ } elsif (/^ACME\s/i) {
+ $acme = 1;
+ } elsif (/^End$/i) {
+ debug(4, "$self->{cfgname}:$.: listener ends");
+ pop @collect_state;
+ if ($acme && @lsthosts) {
+ if ($self->{host}) {
+ $self->define_alias($self->{host}, map { @$_ } @lsthosts);
+ } else {
+ foreach my $hosts (@lsthosts) {
+ my $cn = shift @{$hosts};
+ $self->define_domain($cn);
+ $self->define_alias($cn, @{$hosts}) if @{$hosts};
+ }
+ }
+ }
+ $state = ST_INIT;
+ }
+ } elsif ($state == ST_IGNORE) {
+ if (/^Service(?:\s+".*"\s*)?$/i || /^Match/i || /^Rewrite/i) {
+ $endcnt++;
+ } elsif (/^End$/i) {
+ if ($endcnt == 0) {
+ $state = ST_INIT;
+ } else {
+ $endcnt--;
+ }
+ }
+ } elsif ($state == ST_SERVICE) {
+ if (s/^Host\s+//i) {
+ if ($collect_state[$#collect_state]) {
+ if (my @hosts = $self->host($_)) {
+ debug(3, "$self->{cfgname}:$.: hosts ".join(',', @hosts));
+ push @srvhosts, @hosts;
+ }
+ }
+ } elsif (/^Backend/i) {
+ $state = ST_EXPEND;
+ } elsif (/^Match/i) {
+ $state = ST_MATCH;
+ } elsif (/^End$/i) {
+ $state = ST_LISTENER;
+ if (@srvhosts) {
+ push @lsthosts, [ @srvhosts ];
+ }
+ pop @collect_state;
+ debug(4, "$self->{cfgname}:$.: service ends");
+ }
+ } elsif ($state == ST_MATCH) {
+ if (s/^Host\s+//i) {
+ if ($collect_state[$#collect_state]) {
+ if (my @hosts = $self->host($_)) {
+ debug(3, "$self->{cfgname}:$.: hosts ".join(',', @hosts));
+ push @srvhosts, @hosts;
+ }
+ }
+ } elsif (/^End$/i) {
+ $state = ST_SERVICE;
+ }
+ } elsif ($state == ST_EXPEND) {
+ if (/^End$/i) {
+ $state = ST_SERVICE;
+ }
+ }
+ }
+ close $fh;
+
+ if (@collect_state) {
+ error("$self->{cfgname}: parsing failed, " . (0+@collect_state) .
+ " states remained on stack");
+ return 0
+ }
+
+ return 1;
+}
+
+sub dequote {
+ my ($self, $arg) = @_;
+ if (defined($arg) && $arg =~ s{^\s*"(.*?)"\s*$}{$1}) {
+ $arg =~ s{\\([\\"])}{$1}g;
+ }
+ return $arg;
+}
+
+1;

Return to:

Send suggestions and report system problems to the System administrator.