]>
git.proxmox.com Git - pmg-api.git/blob - src/PMG/NodeConfig.pm
1 package PMG
::NodeConfig
;
9 use PVE
::JSONSchema
qw(get_standard_option);
12 use PMG
::API2
::ACMEPlugin
;
15 # register up to 5 domain names per node for now
18 my $inotify_file_id = 'pmg-node-config.conf';
19 my $config_filename = '/etc/pmg/node.conf';
20 my $lockfile = "/var/lock/pmg-node-config.lck";
22 my $acme_domain_desc = {
25 format
=> 'pmg-acme-domain',
26 format_description
=> 'domain',
27 description
=> 'domain for this node\'s ACME certificate',
32 format
=> 'pve-configid',
33 description
=> 'The ACME plugin ID',
34 format_description
=> 'name of the plugin configuration',
36 default => 'standalone',
40 format
=> 'pmg-acme-alias',
41 format_description
=> 'domain',
42 description
=> 'Alias for the Domain to verify ACME Challenge over DNS',
47 format
=> 'pmg-certificate-type-list',
48 format_description
=> 'usage list',
49 description
=> 'Whether this domain is used for the API, SMTP or both',
54 account
=> get_standard_option
('pmg-acme-account-name'),
60 description
=> 'Node specific ACME settings.',
67 description
=> 'ACME domain and validation plugin',
68 format
=> $acme_domain_desc,
74 sub acme_config_schema
: prototype(;$) {
81 additionalProperties
=> 0,
89 my $config_schema = acme_config_schema
();
91 # Parse the config's acme property string if it exists.
93 # Returns nothing if the entry is not set.
94 sub parse_acme
: prototype($) {
96 my $data = $cfg->{acme
};
98 return PVE
::JSONSchema
::parse_property_string
($acmedesc, $data);
100 return; # empty list otherwise
103 # Turn the acme object into a property string.
104 sub print_acme
: prototype($) {
106 return PVE
::JSONSchema
::print_property_string
($acmedesc, $acme);
109 # Parse a domain entry from the config.
110 sub parse_domain
: prototype($) {
112 return PVE
::JSONSchema
::parse_property_string
($acme_domain_desc, $data);
115 # Turn a domain object into a property string.
116 sub print_domain
: prototype($) {
118 return PVE
::JSONSchema
::print_property_string
($acme_domain_desc, $domain);
121 sub read_pmg_node_config
{
122 my ($filename, $fh) = @_;
123 my $raw = defined($fh) ?
do { local $/ = undef; <$fh> } : '';
124 my $digest = Digest
::SHA
::sha1_hex
($raw);
125 my $conf = PVE
::JSONSchema
::parse_config
($config_schema, $filename, $raw);
126 $conf->{digest
} = $digest;
130 sub write_pmg_node_config
{
131 my ($filename, $fh, $cfg) = @_;
132 my $raw = PVE
::JSONSchema
::dump_config
($config_schema, $filename, $cfg);
134 # higher level ACME sanity checking
136 PVE
::Tools
::safe_print
($filename, $fh, $raw);
139 PVE
::INotify
::register_file
($inotify_file_id, $config_filename,
140 \
&read_pmg_node_config
,
141 \
&write_pmg_node_config
,
143 always_call_parser
=> 1);
147 my $p = PVE
::Tools
::lock_file
($lockfile, undef, $code);
153 # auto-adds the standalone plugin if no config is there for backwards
154 # compatibility, so ALWAYS call the cfs registered parser
155 return PVE
::INotify
::read_file
($inotify_file_id);
160 return PVE
::INotify
::write_file
($inotify_file_id, $self);
163 # we always convert domain values to lower case, since DNS entries are not case
164 # sensitive and ACME implementations might convert the ordered identifiers
166 # FIXME: Could also be shared between PVE and PMG
168 my ($conf, $noerr) = @_;
173 if (defined($conf->{acme
})) {
175 PVE
::JSONSchema
::parse_property_string
($acmedesc, $conf->{acme
})
178 return undef if $noerr;
181 my $standalone_domains = delete($res->{domains
}) // '';
182 $res->{domains
} = {};
183 for my $domain (split(";", $standalone_domains)) {
184 $domain = lc($domain);
185 die "duplicate domain '$domain' in ACME config properties\n"
186 if defined($res->{domains
}->{$domain});
188 $res->{domains
}->{$domain}->{plugin
} = 'standalone';
189 $res->{domains
}->{$domain}->{_configkey
} = 'acme';
193 $res->{account
} //= 'default';
195 for my $index (0..$MAXDOMAINS) {
196 my $domain_rec = $conf->{"acmedomain$index"};
197 next if !defined($domain_rec);
200 PVE
::JSONSchema
::parse_property_string
($acme_domain_desc, $domain_rec)
203 return undef if $noerr;
206 my $domain = lc(delete $parsed->{domain
});
207 if (my $exists = $res->{domains
}->{$domain}) {
208 return undef if $noerr;
209 die "duplicate domain '$domain' in ACME config properties"
210 ." 'acmedomain$index' and '$exists->{_configkey}'\n";
212 $parsed->{plugin
} //= 'standalone';
214 my $plugins = PMG
::API2
::ACMEPlugin
::load_config
();
215 my $plugin_id = $parsed->{plugin
};
216 if ($plugin_id ne 'standalone') {
217 die "plugin '$plugin_id' for domain '$domain' not found!\n"
218 if !$plugins->{ids
}->{$plugin_id};
221 # validation for wildcard domain names happens on the domain w/o
222 # wildcard - see https://tools.ietf.org/html/rfc8555#section-7.1.3
223 if ($domain =~ /^\*\.(.*)$/ ) {
224 $res->{validationtarget
}->{$1} = $domain;
225 die "wildcard domain validation for '$domain' needs a dns-01 plugin.\n"
226 if $plugins->{ids
}->{$plugin_id}->{type
} ne 'dns';
230 $parsed->{_configkey
} = "acmedomain$index";
231 $res->{domains
}->{$domain} = $parsed;
237 # Helper to filter the domains hash. Returns `undef` if the list is empty.
238 sub filter_domains_by_type
: prototype($$) {
239 my ($domains, $type) = @_;
241 return undef if !$domains || !%$domains;
245 foreach my $domain (keys %$domains) {
246 my $entry = $domains->{$domain};
247 if (grep { $_ eq $type } PVE
::Tools
::split_list
($entry->{usage
})) {
248 $out->{$domain} = $entry;
252 return undef if !%$out;