]> git.proxmox.com Git - pmg-api.git/blob - PMG/Config.pm
new helpers rewrite_dot_forward() and rewrite_config_postgres()
[pmg-api.git] / PMG / Config.pm
1 package PMG::Config::Base;
2
3 use strict;
4 use warnings;
5 use Data::Dumper;
6
7 use PVE::Tools;
8 use PVE::JSONSchema qw(get_standard_option);
9 use PVE::SectionConfig;
10
11 use base qw(PVE::SectionConfig);
12
13 my $defaultData = {
14 propertyList => {
15 type => { description => "Section type." },
16 section_id => {
17 description => "Secion ID.",
18 type => 'string', format => 'pve-configid',
19 },
20 },
21 };
22
23 sub private {
24 return $defaultData;
25 }
26
27 sub format_section_header {
28 my ($class, $type, $sectionId) = @_;
29
30 if ($type eq 'ldap') {
31 $sectionId =~ s/^ldap_//;
32 return "$type: $sectionId\n";
33 } else {
34 return "section: $type\n";
35 }
36 }
37
38
39 sub parse_section_header {
40 my ($class, $line) = @_;
41
42 if ($line =~ m/^(ldap|section):\s*(\S+)\s*$/) {
43 my ($raw_type, $raw_id) = (lc($1), $2);
44 my $type = $raw_type eq 'section' ? $raw_id : $raw_type;
45 my $section_id = "${raw_type}_${raw_id}";
46 my $errmsg = undef; # set if you want to skip whole section
47 eval { PVE::JSONSchema::pve_verify_configid($raw_id); };
48 $errmsg = $@ if $@;
49 my $config = {}; # to return additional attributes
50 return ($type, $section_id, $errmsg, $config);
51 }
52 return undef;
53 }
54
55 package PMG::Config::Admin;
56
57 use strict;
58 use warnings;
59
60 use base qw(PMG::Config::Base);
61
62 sub type {
63 return 'admin';
64 }
65
66 sub properties {
67 return {
68 dailyreport => {
69 description => "Send daily reports.",
70 type => 'boolean',
71 default => 1,
72 },
73 demo => {
74 description => "Demo mode - do not start SMTP filter.",
75 type => 'boolean',
76 default => 0,
77 },
78 email => {
79 description => "Administrator E-Mail address.",
80 type => 'string', format => 'email',
81 default => 'admin@domain.tld',
82 },
83 proxyport => {
84 description => "HTTP proxy port.",
85 type => 'integer',
86 minimum => 1,
87 default => 8080,
88 },
89 proxyserver => {
90 description => "HTTP proxy server address.",
91 type => 'string',
92 },
93 proxyuser => {
94 description => "HTTP proxy user name.",
95 type => 'string',
96 },
97 proxypassword => {
98 description => "HTTP proxy password.",
99 type => 'string',
100 },
101 };
102 }
103
104 sub options {
105 return {
106 dailyreport => { optional => 1 },
107 demo => { optional => 1 },
108 proxyport => { optional => 1 },
109 proxyserver => { optional => 1 },
110 proxyuser => { optional => 1 },
111 proxypassword => { optional => 1 },
112 };
113 }
114
115 package PMG::Config::Spam;
116
117 use strict;
118 use warnings;
119
120 use base qw(PMG::Config::Base);
121
122 sub type {
123 return 'spam';
124 }
125
126 sub properties {
127 return {
128 languages => {
129 description => "This option is used to specify which languages are considered OK for incoming mail.",
130 type => 'string',
131 pattern => '(all|([a-z][a-z])+( ([a-z][a-z])+)*)',
132 default => 'all',
133 },
134 use_bayes => {
135 description => "Whether to use the naive-Bayesian-style classifier.",
136 type => 'boolean',
137 default => 1,
138 },
139 use_awl => {
140 description => "Use the Auto-Whitelist plugin.",
141 type => 'boolean',
142 default => 1,
143 },
144 use_razor => {
145 description => "Whether to use Razor2, if it is available.",
146 type => 'boolean',
147 default => 1,
148 },
149 use_ocr => {
150 description => "Enable OCR to scan pictures.",
151 type => 'boolean',
152 default => 0,
153 },
154 wl_bounce_relays => {
155 description => "Whitelist legitimate bounce relays.",
156 type => 'string',
157 },
158 bounce_score => {
159 description => "Additional score for bounce mails.",
160 type => 'integer',
161 minimum => 0,
162 maximum => 1000,
163 default => 0,
164 },
165 rbl_checks => {
166 description => "Enable real time blacklists (RBL) checks.",
167 type => 'boolean',
168 default => 1,
169 },
170 maxspamsize => {
171 description => "Maximum size of spam messages in bytes.",
172 type => 'integer',
173 minimim => 64,
174 default => 200*1024,
175 },
176 };
177 }
178
179 sub options {
180 return {
181 use_awl => { optional => 1 },
182 use_razor => { optional => 1 },
183 use_ocr => { optional => 1 },
184 wl_bounce_relays => { optional => 1 },
185 languages => { optional => 1 },
186 use_bayes => { optional => 1 },
187 bounce_score => { optional => 1 },
188 rbl_checks => { optional => 1 },
189 maxspamsize => { optional => 1 },
190 };
191 }
192
193 package PMG::Config::ClamAV;
194
195 use strict;
196 use warnings;
197
198 use base qw(PMG::Config::Base);
199
200 sub type {
201 return 'clamav';
202 }
203
204 sub properties {
205 return {
206 dbmirror => {
207 description => "ClamAV database mirror server.",
208 type => 'string',
209 default => 'database.clamav.net',
210 },
211 archiveblockencrypted => {
212 description => "Wether to block encrypted archives. Mark encrypted archives as viruses.",
213 type => 'boolean',
214 default => 0,
215 },
216 archivemaxrec => {
217 description => "Nested archives are scanned recursively, e.g. if a ZIP archive contains a TAR file, all files within it will also be scanned. This options specifies how deeply the process should be continued. Warning: setting this limit too high may result in severe damage to the system.",
218 minimum => 1,
219 default => 5,
220 },
221 archivemaxfiles => {
222 description => "Number of files to be scanned within an archive, a document, or any other kind of container. Warning: disabling this limit or setting it too high may result in severe damage to the system.",
223 type => 'integer',
224 minimum => 0,
225 default => 1000,
226 },
227 archivemaxsize => {
228 description => "Files larger than this limit won't be scanned.",
229 type => 'integer',
230 minimum => 1000000,
231 default => 25000000,
232 },
233 maxscansize => {
234 description => "Sets the maximum amount of data to be scanned for each input file.",
235 type => 'integer',
236 minimum => 1000000,
237 default => 100000000,
238 },
239 maxcccount => {
240 description => "This option sets the lowest number of Credit Card or Social Security numbers found in a file to generate a detect.",
241 type => 'integer',
242 minimum => 0,
243 default => 0,
244 },
245 };
246 }
247
248 sub options {
249 return {
250 archiveblockencrypted => { optional => 1 },
251 archivemaxrec => { optional => 1 },
252 archivemaxfiles => { optional => 1 },
253 archivemaxsize => { optional => 1 },
254 maxscansize => { optional => 1 },
255 dbmirror => { optional => 1 },
256 maxcccount => { optional => 1 },
257 };
258 }
259
260 package PMG::Config::LDAP;
261
262 use strict;
263 use warnings;
264
265 use base qw(PMG::Config::Base);
266
267 sub type {
268 return 'ldap';
269 }
270
271 sub properties {
272 return {
273 mode => {
274 description => "LDAP protocol mode ('ldap' or 'ldaps').",
275 type => 'string',
276 enum => ['ldap', 'ldaps'],
277 default => 'ldap',
278 },
279 };
280 }
281
282 sub options {
283 return {
284 mode => { optional => 1 },
285 };
286 }
287
288 package PMG::Config::Mail;
289
290 use strict;
291 use warnings;
292
293 use PVE::ProcFSTools;
294
295 use base qw(PMG::Config::Base);
296
297 sub type {
298 return 'mail';
299 }
300
301 my $physicalmem = 0;
302 sub physical_memory {
303
304 return $physicalmem if $physicalmem;
305
306 my $info = PVE::ProcFSTools::read_meminfo();
307 my $total = int($info->{memtotal} / (1024*1024));
308
309 return $total;
310 }
311
312 sub get_max_filters {
313 # estimate optimal number of filter servers
314
315 my $max_servers = 5;
316 my $servermem = 120;
317 my $memory = physical_memory();
318 my $add_servers = int(($memory - 512)/$servermem);
319 $max_servers += $add_servers if $add_servers > 0;
320 $max_servers = 40 if $max_servers > 40;
321
322 return $max_servers - 2;
323 }
324
325 sub properties {
326 return {
327 banner => {
328 description => "ESMTP banner.",
329 type => 'string',
330 maxLength => 1024,
331 default => 'ESMTP Proxmox',
332 },
333 max_filters => {
334 description => "Maximum number of filter processes.",
335 type => 'integer',
336 minimum => 3,
337 maximum => 40,
338 default => get_max_filters(),
339 },
340 hide_received => {
341 description => "Hide received header in outgoing mails.",
342 type => 'boolean',
343 default => 0,
344 },
345 max_size => {
346 description => "Maximum email size. Larger mails are rejected.",
347 type => 'integer',
348 minimum => 1024,
349 default => 1024*1024*10,
350 },
351 };
352 }
353
354 sub options {
355 return {
356 max_size => { optional => 1 },
357 banner => { optional => 1 },
358 max_filters => { optional => 1 },
359 hide_received => { optional => 1 },
360 };
361 }
362 package PMG::Config;
363
364 use strict;
365 use warnings;
366 use IO::File;
367 use Data::Dumper;
368 use Template;
369
370 use PVE::SafeSyslog;
371 use PVE::Tools;
372 use PVE::INotify;
373
374 use PMG::AtomicFile;
375
376 PMG::Config::Admin->register();
377 PMG::Config::Mail->register();
378 PMG::Config::Spam->register();
379 PMG::Config::LDAP->register();
380 PMG::Config::ClamAV->register();
381
382 # initialize all plugins
383 PMG::Config::Base->init();
384
385
386 sub new {
387 my ($type) = @_;
388
389 my $class = ref($type) || $type;
390
391 my $cfg = PVE::INotify::read_file("pmg.conf");
392
393 return bless $cfg, $class;
394 }
395
396 # set section values
397 # this does not work for ldap entries
398 sub set {
399 my ($self, $section, $key, $value) = @_;
400
401 my $pdata = PMG::Config::Base->private();
402
403 die "internal error" if $section eq 'ldap';
404
405 my $plugin = $pdata->{plugins}->{$section};
406 die "no such section '$section'" if !$plugin;
407
408 my $configid = "section_$section";
409 if (defined($value)) {
410 my $tmp = PMG::Config::Base->check_value($section, $key, $value, $section, 0);
411 print Dumper($self->{ids});
412 $self->{ids}->{$configid} = { type => $section } if !defined($self->{ids}->{$configid});
413 $self->{ids}->{$configid}->{$key} = PMG::Config::Base->decode_value($section, $key, $tmp);
414 } else {
415 if (defined($self->{ids}->{$configid})) {
416 delete $self->{ids}->{$configid}->{$key};
417 }
418 }
419
420 return undef;
421 }
422
423 # get section value or default
424 # this does not work for ldap entries
425 sub get {
426 my ($self, $section, $key) = @_;
427
428 my $pdata = PMG::Config::Base->private();
429 return undef if !defined($pdata->{options}->{$section});
430 return undef if !defined($pdata->{options}->{$section}->{$key});
431 my $pdesc = $pdata->{propertyList}->{$key};
432 return undef if !defined($pdesc);
433
434 my $configid = "section_$section";
435 if (defined($self->{ids}->{$configid}) &&
436 defined(my $value = $self->{ids}->{$configid}->{$key})) {
437 return $value;
438 }
439
440 return $pdesc->{default};
441 }
442
443 # get a whole section with default value
444 # this does not work for ldap entries
445 sub get_section {
446 my ($self, $section) = @_;
447
448 my $pdata = PMG::Config::Base->private();
449 return undef if !defined($pdata->{options}->{$section});
450
451 my $res = {};
452
453 foreach my $key (keys %{$pdata->{options}->{$section}}) {
454
455 my $pdesc = $pdata->{propertyList}->{$key};
456
457 my $configid = "section_$section";
458 if (defined($self->{ids}->{$configid}) &&
459 defined(my $value = $self->{ids}->{$configid}->{$key})) {
460 $res->{$key} = $value;
461 next;
462 }
463 $res->{$key} = $pdesc->{default};
464 }
465
466 return $res;
467 }
468
469 # get a whole config with default values
470 # this does not work for ldap entries
471 sub get_config {
472 my ($self) = @_;
473
474 my $pdata = PMG::Config::Base->private();
475
476 my $res = {};
477
478 foreach my $type (keys %{$pdata->{plugins}}) {
479 next if $type eq 'ldap';
480 my $plugin = $pdata->{plugins}->{$type};
481 $res->{$type} = $self->get_section($type);
482 }
483
484 return $res;
485 }
486
487 sub read_pmg_conf {
488 my ($filename, $fh) = @_;
489
490 local $/ = undef; # slurp mode
491
492 my $raw = <$fh>;
493
494 return PMG::Config::Base->parse_config($filename, $raw);
495 }
496
497 sub write_pmg_conf {
498 my ($filename, $fh, $cfg) = @_;
499
500 my $raw = PMG::Config::Base->write_config($filename, $cfg);
501
502 PVE::Tools::safe_print($filename, $fh, $raw);
503 }
504
505 PVE::INotify::register_file('pmg.conf', "/etc/proxmox/pmg.conf",
506 \&read_pmg_conf,
507 \&write_pmg_conf);
508
509
510 # config file generation using templates
511
512 sub rewrite_config_file {
513 my ($self, $tmplname, $dstfn) = @_;
514
515 my $demo = $self->get('admin', 'demo');
516
517 my $srcfn = ($tmplname =~ m|^.?/|) ?
518 $tmplname : "/var/lib/pmg/templates/$tmplname";
519
520 if ($demo) {
521 my $demosrc = "$srcfn.demo";
522 $srcfn = $demosrc if -f $demosrc;
523 }
524
525 my $srcfd = IO::File->new ($srcfn, "r")
526 || die "cant read template '$srcfn' - $!: ERROR";
527 my $dstfd = PMG::AtomicFile->open ($dstfn, "w")
528 || die "cant open config file '$dstfn' - $!: ERROR";
529
530 if ($dstfn eq '/etc/fetchmailrc') {
531 my ($login, $pass, $uid, $gid) = getpwnam('fetchmail');
532 if ($uid && $gid) {
533 chown($uid, $gid, ${*$dstfd}{'io_atomicfile_temp'});
534 }
535 chmod (0600, ${*$dstfd}{'io_atomicfile_temp'});
536 } elsif ($dstfn eq '/etc/clamav/freshclam.conf') {
537 # needed if file contains a HTTPProxyPasswort
538
539 my $uid = getpwnam('clamav');
540 my $gid = getgrnam('adm');
541
542 if ($uid && $gid) {
543 chown ($uid, $gid, ${*$dstfd}{'io_atomicfile_temp'});
544 }
545 chmod (0600, ${*$dstfd}{'io_atomicfile_temp'});
546 }
547
548 my $template = Template->new({});
549
550 my $vars = { pmg => $self->get_config() };
551
552 $template->process($srcfd, $vars, $dstfd) ||
553 die $template->error();
554
555 $srcfd->close();
556 $dstfd->close (1);
557 }
558
559 sub rewrite_config_script {
560 my ($self, $tmplname, $dstfn) = @_;
561
562 $self->rewrite_config_file($tmplname, $dstfn);
563 system("chmod +x $dstfn");
564 }
565
566 # rewrite spam configuration
567 sub rewrite_config_spam {
568 my ($self) = @_;
569
570 my $use_awl = $self->get('spam', 'use_awl');
571 my $use_bayes = $self->get('spam', 'use_bayes');
572 my $use_razor = $self->get('spam', 'use_razor');
573
574 # delete AW and bayes databases if those features are disabled
575 unlink '/root/.spamassassin/auto-whitelist' if !$use_awl;
576 if (!$use_bayes) {
577 unlink '/root/.spamassassin/bayes_journal';
578 unlink '/root/.spamassassin/bayes_seen';
579 unlink '/root/.spamassassin/bayes_toks';
580 }
581
582 # make sure we have a custom.cf file (else cluster sync fails)
583 IO::File->new('/etc/mail/spamassassin/custom.cf', 'a', 0644);
584
585 $self->rewrite_config_file('local.cf.in', '/etc/mail/spamassassin/local.cf');
586 $self->rewrite_config_file('init.pre.in', '/etc/mail/spamassassin/init.pre');
587 $self->rewrite_config_file('v310.pre.in', '/etc/mail/spamassassin/v310.pre');
588 $self->rewrite_config_file('v320.pre.in', '/etc/mail/spamassassin/v320.pre');
589
590 if ($use_razor) {
591 mkdir "/root/.razor";
592 $self->rewrite_config_file('razor-agent.conf.in', '/root/.razor/razor-agent.conf');
593 if (! -e '/root/.razor/identity') {
594 eval {
595 my $timeout = 30;
596 PVE::Tools::run_command (['razor-admin', '-discover'], timeout => $timeout);
597 PVE::Tools::run_command (['razor-admin', '-register'], timeout => $timeout);
598 };
599 my $err = $@;
600 syslog('info', msgquote ("registering razor failed: $err")) if $err;
601 }
602 }
603 }
604
605 # rewrite ClamAV configuration
606 sub rewrite_config_clam {
607 my ($self) = @_;
608
609 $self->rewrite_config_file('clamd.conf.in', '/etc/clamav/clamd.conf');
610 $self->rewrite_config_file('freshclam.conf.in', '/etc/clamav/freshclam.conf');
611 }
612
613 sub rewrite_config_postgres {
614 my ($self) = @_;
615
616 my $pgconfdir = "/etc/postgresql/9.6/main";
617
618 $self->rewrite_config_file('pg_hba.conf.in', "$pgconfdir/pg_hba.conf");
619 $self->rewrite_config_file('postgresql.conf.in', "$pgconfdir/postgresql.conf");
620 }
621
622 # rewrite /root/.forward
623 sub rewrite_dot_forward {
624 my ($self) = @_;
625
626 my $fname = '/root/.forward';
627
628 my $email = $self->get('administration', 'email');
629 open(TMP, ">$fname");
630 if ($email && $email =~ m/\s*(\S+)\s*/) {
631 print (TMP "$1\n");
632 } else {
633 # empty .forward does not forward mails (see man local)
634 }
635 close (TMP);
636 }
637
638 sub rewrite_config {
639 my ($self) = @_;
640
641 $self->rewrite_dot_forward();
642 $self->rewrite_config_postgres();
643 $self->rewrite_config_spam();
644 $self->rewrite_config_clam();
645 }
646
647 1;