]> git.proxmox.com Git - pmg-api.git/blame - PMG/DBTools.pm
move cleanup_stats() to PMG::DBTools::purge_statistic_database()
[pmg-api.git] / PMG / DBTools.pm
CommitLineData
a355f100
DM
1package PMG::DBTools;
2
3use strict;
4use warnings;
5
6use POSIX ":sys_wait_h";
7use POSIX ':signal_h';
8use DBI;
8fb6f404 9use Time::Local;
a355f100 10
f3464b71 11use PVE::SafeSyslog;
a355f100
DM
12use PVE::Tools;
13
f3464b71 14use PMG::Utils;
0a580593
DM
15use PMG::RuleDB;
16
987ee804
DM
17our $default_db_name = "Proxmox_ruledb";
18
2e049252
DM
19our $cgreylist_merge_sql =
20 'INSERT INTO CGREYLIST (IPNet,Host,Sender,Receiver,Instance,RCTime,' .
21 'ExTime,Delay,Blocked,Passed,MTime,CID) ' .
22 'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ' .
23 'ON CONFLICT (IPNet,Sender,Receiver) DO UPDATE SET ' .
0c6e9b56
DM
24 'Host = CASE WHEN CGREYLIST.MTime >= excluded.MTime THEN CGREYLIST.Host ELSE excluded.Host END,' .
25 'CID = GREATEST(CGREYLIST.CID, excluded.CID), RCTime = LEAST(CGREYLIST.RCTime, excluded.RCTime),' .
26 'ExTime = GREATEST(CGREYLIST.ExTime, excluded.ExTime),' .
27 'Delay = GREATEST(CGREYLIST.Delay, excluded.Delay),' .
28 'Blocked = GREATEST(CGREYLIST.Blocked, excluded.Blocked),' .
29 'Passed = GREATEST(CGREYLIST.Passed, excluded.Passed)';
2e049252 30
a355f100
DM
31sub open_ruledb {
32 my ($database, $host, $port) = @_;
33
5475e159 34 $port //= 5432;
a355f100 35
5475e159 36 $database //= $default_db_name;
a355f100
DM
37
38 if ($host) {
39
155b0da9
DM
40 # Note: pmgtunnel uses UDP sockets inside directory '/var/run/pmgtunnel',
41 # and the cluster 'cid' as port number. You can connect to the
42 # socket with: host => /var/run/pmgtunnel, port => $cid
43
a355f100
DM
44 my $dsn = "dbi:Pg:dbname=$database;host=$host;port=$port;";
45
46 my $timeout = 5;
47 # only low level alarm interface works for DBI->connect
48 my $mask = POSIX::SigSet->new(SIGALRM);
49 my $action = POSIX::SigAction->new(sub { die "connect timeout\n" }, $mask);
50 my $oldaction = POSIX::SigAction->new();
51 sigaction(SIGALRM, $action, $oldaction);
52
53 my $rdb;
54
55 eval {
56 alarm($timeout);
5475e159 57 $rdb = DBI->connect($dsn, 'root', undef,
a355f100
DM
58 { PrintError => 0, RaiseError => 1 });
59 alarm(0);
60 };
61 alarm(0);
62 sigaction(SIGALRM, $oldaction); # restore original handler
f1bf78ff 63
a355f100
DM
64 die $@ if $@;
65
66 return $rdb;
67 } else {
155b0da9 68 my $dsn = "DBI:Pg:dbname=$database;host=/var/run/postgresql;port=$port";
a355f100 69
5475e159 70 my $dbh = DBI->connect($dsn, $> == 0 ? 'root' : 'www-data', undef,
a355f100
DM
71 { PrintError => 0, RaiseError => 1 });
72
73 return $dbh;
74 }
75}
76
97cab76a
DM
77sub postgres_admin_cmd {
78 my ($cmd, $options, @params) = @_;
79
80 $cmd = ref($cmd) ? $cmd : [ $cmd ];
81 my $uid = getpwnam('postgres') || die "getpwnam postgres failed\n";
82
83 local $> = $uid;
84 $! && die "setuid postgres ($uid) failed - $!\n";
85
86 PVE::Tools::run_command([@$cmd, '-U', 'postgres', @params], %$options);
87}
88
a355f100
DM
89sub delete_ruledb {
90 my ($dbname) = @_;
91
97cab76a 92 postgres_admin_cmd('dropdb', undef, $dbname);
a355f100
DM
93}
94
f1bf78ff
DM
95sub database_list {
96
97 my $database_list = {};
98
99 my $parser = sub {
100 my $line = shift;
101
102 my ($name, $owner) = map { PVE::Tools::trim($_) } split(/\|/, $line);
103 return if !$name || !$owner;
104
105 $database_list->{$name} = { owner => $owner };
106 };
107
97cab76a 108 postgres_admin_cmd('psql', { outfunc => $parser }, '--list', '--quiet', '--tuples-only');
f1bf78ff
DM
109
110 return $database_list;
111}
112
f1bf78ff
DM
113my $cgreylist_ctablecmd = <<__EOD;
114 CREATE TABLE CGreylist
115 (IPNet VARCHAR(16) NOT NULL,
116 Host INTEGER NOT NULL,
117 Sender VARCHAR(255) NOT NULL,
118 Receiver VARCHAR(255) NOT NULL,
119 Instance VARCHAR(255),
120 RCTime INTEGER NOT NULL,
121 ExTime INTEGER NOT NULL,
122 Delay INTEGER NOT NULL DEFAULT 0,
123 Blocked INTEGER NOT NULL,
124 Passed INTEGER NOT NULL,
125 CID INTEGER NOT NULL,
126 MTime INTEGER NOT NULL,
127 PRIMARY KEY (IPNet, Sender, Receiver));
128
129 CREATE INDEX CGreylist_Instance_Sender_Index ON CGreylist (Instance, Sender);
130
131 CREATE INDEX CGreylist_ExTime_Index ON CGreylist (ExTime);
132
133 CREATE INDEX CGreylist_MTime_Index ON CGreylist (MTime);
134__EOD
135
136my $clusterinfo_ctablecmd = <<__EOD;
137 CREATE TABLE ClusterInfo
138 (CID INTEGER NOT NULL,
139 Name VARCHAR NOT NULL,
140 IValue INTEGER,
141 SValue VARCHAR,
142 PRIMARY KEY (CID, Name))
143__EOD
144
145my $daily_stat_ctablecmd = <<__EOD;
146 CREATE TABLE DailyStat
147 (Time INTEGER NOT NULL UNIQUE,
148 CountIn INTEGER NOT NULL,
149 CountOut INTEGER NOT NULL,
150 BytesIn REAL NOT NULL,
151 BytesOut REAL NOT NULL,
152 VirusIn INTEGER NOT NULL,
153 VirusOut INTEGER NOT NULL,
154 SpamIn INTEGER NOT NULL,
155 SpamOut INTEGER NOT NULL,
156 BouncesIn INTEGER NOT NULL,
157 BouncesOut INTEGER NOT NULL,
158 GreylistCount INTEGER NOT NULL,
159 SPFCount INTEGER NOT NULL,
160 PTimeSum REAL NOT NULL,
161 MTime INTEGER NOT NULL,
162 RBLCount INTEGER DEFAULT 0 NOT NULL,
163 PRIMARY KEY (Time));
164
165 CREATE INDEX DailyStat_MTime_Index ON DailyStat (MTime);
166
167__EOD
168
169my $domain_stat_ctablecmd = <<__EOD;
170 CREATE TABLE DomainStat
171 (Time INTEGER NOT NULL,
172 Domain VARCHAR(255) NOT NULL,
173 CountIn INTEGER NOT NULL,
174 CountOut INTEGER NOT NULL,
175 BytesIn REAL NOT NULL,
176 BytesOut REAL NOT NULL,
177 VirusIn INTEGER NOT NULL,
178 VirusOut INTEGER NOT NULL,
179 SpamIn INTEGER NOT NULL,
180 SpamOut INTEGER NOT NULL,
181 BouncesIn INTEGER NOT NULL,
182 BouncesOut INTEGER NOT NULL,
183 PTimeSum REAL NOT NULL,
184 MTime INTEGER NOT NULL,
185 PRIMARY KEY (Time, Domain));
186
187 CREATE INDEX DomainStat_MTime_Index ON DomainStat (MTime);
188__EOD
189
190my $statinfo_ctablecmd = <<__EOD;
191 CREATE TABLE StatInfo
192 (Name VARCHAR(255) NOT NULL UNIQUE,
193 IValue INTEGER,
194 SValue VARCHAR(255),
195 PRIMARY KEY (Name))
196__EOD
197
198my $virusinfo_stat_ctablecmd = <<__EOD;
199 CREATE TABLE VirusInfo
200 (Time INTEGER NOT NULL,
201 Name VARCHAR NOT NULL,
202 Count INTEGER NOT NULL,
203 MTime INTEGER NOT NULL,
204 PRIMARY KEY (Time, Name));
205
206 CREATE INDEX VirusInfo_MTime_Index ON VirusInfo (MTime);
207
208__EOD
209
210# mail storage stable
211# QTypes
212# V - Virus quarantine
213# S - Spam quarantine
214# D - Delayed Mails - not implemented
215# A - Held for Audit - not implemented
216# Status
217# N - new
218# D - deleted
219
220my $cmailstore_ctablecmd = <<__EOD;
221 CREATE TABLE CMailStore
222 (CID INTEGER DEFAULT 0 NOT NULL,
223 RID INTEGER NOT NULL,
224 ID SERIAL UNIQUE,
225 Time INTEGER NOT NULL,
226 QType "char" NOT NULL,
227 Bytes INTEGER NOT NULL,
228 Spamlevel INTEGER NOT NULL,
229 Info VARCHAR NULL,
230 Sender VARCHAR(255) NOT NULL,
231 Header VARCHAR NOT NULL,
232 File VARCHAR(255) NOT NULL,
233 PRIMARY KEY (CID, RID));
234 CREATE INDEX CMailStore_Time_Index ON CMailStore (Time);
235
236 CREATE TABLE CMSReceivers
237 (CMailStore_CID INTEGER NOT NULL,
238 CMailStore_RID INTEGER NOT NULL,
239 PMail VARCHAR(255) NOT NULL,
240 Receiver VARCHAR(255),
241 TicketID INTEGER NOT NULL,
242 Status "char" NOT NULL,
243 MTime INTEGER NOT NULL);
244
245 CREATE INDEX CMailStore_ID_Index ON CMSReceivers (CMailStore_CID, CMailStore_RID);
246
247 CREATE INDEX CMSReceivers_MTime_Index ON CMSReceivers (MTime);
248
249__EOD
250
251my $cstatistic_ctablecmd = <<__EOD;
252 CREATE TABLE CStatistic
253 (CID INTEGER DEFAULT 0 NOT NULL,
254 RID INTEGER NOT NULL,
255 ID SERIAL UNIQUE,
256 Time INTEGER NOT NULL,
257 Bytes INTEGER NOT NULL,
258 Direction Boolean NOT NULL,
259 Spamlevel INTEGER NOT NULL,
260 VirusInfo VARCHAR(255) NULL,
261 PTime INTEGER NOT NULL,
262 Sender VARCHAR(255) NOT NULL,
263 PRIMARY KEY (CID, RID));
264
265 CREATE INDEX CStatistic_Time_Index ON CStatistic (Time);
266
267 CREATE TABLE CReceivers
268 (CStatistic_CID INTEGER NOT NULL,
269 CStatistic_RID INTEGER NOT NULL,
270 Receiver VARCHAR(255) NOT NULL,
271 Blocked Boolean NOT NULL);
272
273 CREATE INDEX CStatistic_ID_Index ON CReceivers (CStatistic_CID, CStatistic_RID);
274__EOD
275
276# user preferences (black an whitelists, ...)
277# Name: perference name ('BL' -> blacklist, 'WL' -> whitelist)
278# Data: arbitrary data
279my $userprefs_ctablecmd = <<__EOD;
280 CREATE TABLE UserPrefs
281 (PMail VARCHAR,
282 Name VARCHAR(255),
283 Data VARCHAR,
284 MTime INTEGER NOT NULL,
285 PRIMARY KEY (PMail, Name));
286
287 CREATE INDEX UserPrefs_MTime_Index ON UserPrefs (MTime);
288
289__EOD
758c7b6b 290
0a580593
DM
291sub cond_create_dbtable {
292 my ($dbh, $name, $ctablecmd) = @_;
293
294 eval {
295 $dbh->begin_work;
296
297 my $cmd = "SELECT tablename FROM pg_tables " .
298 "WHERE tablename = lower ('$name')";
299
300 my $sth = $dbh->prepare ($cmd);
758c7b6b 301
0a580593
DM
302 $sth->execute();
303
304 if (!(my $ref = $sth->fetchrow_hashref())) {
305 $dbh->do ($ctablecmd);
306 }
758c7b6b 307
0a580593
DM
308 $sth->finish();
309
310 $dbh->commit;
311 };
312 if (my $err = $@) {
313 $dbh->rollback;
9ef3f143 314 die $err;
0a580593
DM
315 }
316}
f1bf78ff
DM
317
318sub create_ruledb {
319 my ($dbname) = @_;
320
987ee804 321 $dbname = $default_db_name if !$dbname;
f1bf78ff 322
97cab76a
DM
323 my $silent_opts = { outfunc => sub {}, errfunc => sub {} };
324 # make sure we have user 'root'
325 eval { postgres_admin_cmd('createuser', $silent_opts, '-D', 'root'); };
5475e159
DM
326 # also create 'www-data' (and give it read-only access below)
327 eval { postgres_admin_cmd('createuser', $silent_opts, '-I', '-D', 'www-data'); };
97cab76a 328
f1bf78ff
DM
329 # use sql_ascii to avoid any character set conversions, and be compatible with
330 # older postgres versions (update from 8.1 must be possible)
f1bf78ff 331
97cab76a
DM
332 postgres_admin_cmd('createdb', undef, '-E', 'sql_ascii', '-T', 'template0',
333 '--lc-collate=C', '--lc-ctype=C', $dbname);
f1bf78ff
DM
334
335 my $dbh = open_ruledb($dbname);
336
5475e159
DM
337 # make sure 'www-data' can read all tables
338 $dbh->do("ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO \"www-data\"");
339
f1bf78ff
DM
340 $dbh->do (
341<<EOD
342 CREATE TABLE Attribut
343 (Object_ID INTEGER NOT NULL,
344 Name VARCHAR(20) NOT NULL,
345 Value BYTEA NULL,
346 PRIMARY KEY (Object_ID, Name));
347
348 CREATE INDEX Attribut_Object_ID_Index ON Attribut(Object_ID);
349
350 CREATE TABLE Object
351 (ID SERIAL UNIQUE,
352 ObjectType INTEGER NOT NULL,
353 Objectgroup_ID INTEGER NOT NULL,
354 Value BYTEA NULL,
355 PRIMARY KEY (ID));
356
357 CREATE TABLE Objectgroup
358 (ID SERIAL UNIQUE,
359 Name VARCHAR(255) NOT NULL,
360 Info VARCHAR(255) NULL,
361 Class VARCHAR(10) NOT NULL,
362 PRIMARY KEY (ID));
363
364 CREATE TABLE Rule
365 (ID SERIAL UNIQUE,
366 Name VARCHAR(255) NULL,
367 Priority INTEGER NOT NULL,
368 Active INTEGER NOT NULL DEFAULT 0,
369 Direction INTEGER NOT NULL DEFAULT 2,
370 Count INTEGER NOT NULL DEFAULT 0,
371 PRIMARY KEY (ID));
372
373 CREATE TABLE RuleGroup
374 (Objectgroup_ID INTEGER NOT NULL,
375 Rule_ID INTEGER NOT NULL,
376 Grouptype INTEGER NOT NULL,
377 PRIMARY KEY (Objectgroup_ID, Rule_ID, Grouptype));
378
379 $cgreylist_ctablecmd;
380
381 $clusterinfo_ctablecmd;
382
383 $daily_stat_ctablecmd;
384
385 $domain_stat_ctablecmd;
386
387 $statinfo_ctablecmd;
388
389 $cmailstore_ctablecmd;
390
391 $cstatistic_ctablecmd;
392
393 $userprefs_ctablecmd;
394
395 $virusinfo_stat_ctablecmd;
396EOD
397 );
398
399 return $dbh;
400}
401
0a580593
DM
402sub cond_create_action_quarantine {
403 my ($ruledb) = @_;
404
405 my $dbh = $ruledb->{dbh};
406
407 eval {
408 my $sth = $dbh->prepare(
409 "SELECT * FROM Objectgroup, Object " .
410 "WHERE Object.ObjectType = ? AND Objectgroup.Class = ? " .
411 "AND Object.objectgroup_id = Objectgroup.id");
412
413 my $otype = PMG::RuleDB::Quarantine::otype();
414 if ($sth->execute($otype, 'action') <= 0) {
415 my $obj = PMG::RuleDB::Quarantine->new ();
416 my $txt = decode_entities(PMG::RuleDB::Quarantine->otype_text);
417 my $quarantine = $ruledb->create_group_with_obj
9578dcd7 418 ($obj, $txt, 'Move to quarantine.');
0a580593
DM
419 }
420 };
421}
422
423sub cond_create_std_actions {
424 my ($ruledb) = @_;
425
426 cond_create_action_quarantine($ruledb);
427
428 #cond_create_action_report_spam($ruledb);
429}
430
431
432sub upgradedb {
433 my ($ruledb) = @_;
434
435 my $dbh = $ruledb->{dbh};
436
758c7b6b 437 # make sure we do not use slow sequential scans when upgraing
0a580593
DM
438 # database (before analyze can gather statistics)
439 $dbh->do("set enable_seqscan = false");
440
97cab76a
DM
441 my $tables = {
442 'DailyStat'=> $daily_stat_ctablecmd,
443 'DomainStat', $domain_stat_ctablecmd,
444 'StatInfo', $statinfo_ctablecmd,
445 'CMailStore', $cmailstore_ctablecmd,
446 'UserPrefs', $userprefs_ctablecmd,
447 'CGreylist', $cgreylist_ctablecmd,
448 'CStatistic', $cstatistic_ctablecmd,
449 'ClusterInfo', $clusterinfo_ctablecmd,
450 'VirusInfo', $virusinfo_stat_ctablecmd,
451 };
452
453 foreach my $table (keys %$tables) {
454 cond_create_dbtable($dbh, $table, $tables->{$tables});
455 }
0a580593 456
e7c865af 457 cond_create_std_actions($ruledb);
0a580593 458
dc295201 459 # upgrade tables here if necessary
0a580593
DM
460
461 # update obsolete content type names
462 eval {
e7c865af
DM
463 $dbh->do("UPDATE Object " .
464 "SET value = 'content-type:application/java-vm' ".
465 "WHERE objecttype = 3003 " .
466 "AND value = 'content-type:application/x-java-vm';");
0a580593
DM
467 };
468
97cab76a
DM
469 foreach my $table (keys %$tables) {
470 eval { $dbh->do("ANALYZE $table"); };
471 warn $@ if $@;
472 }
0a580593
DM
473}
474
475sub init_ruledb {
476 my ($ruledb, $reset, $testmode) = @_;
477
478 my $dbh = $ruledb->{dbh};
479
480 if (!$reset) {
481 # Greylist Objectgroup
482 my $greylistgroup = PMG::RuleDB::Group->new
483 ("GreyExclusion", "-", "greylist");
484 $ruledb->save_group ($greylistgroup);
485
486 } else {
487 # we do not touch greylist objects
488 my $glids = "SELECT object.ID FROM Object, Objectgroup WHERE " .
489 "objectgroup_id = objectgroup.id and class = 'greylist'";
490
491 $dbh->do ("DELETE FROM Rule; " .
492 "DELETE FROM RuleGroup; " .
493 "DELETE FROM Attribut WHERE Object_ID NOT IN ($glids); " .
494 "DELETE FROM Object WHERE ID NOT IN ($glids); " .
495 "DELETE FROM Objectgroup WHERE class != 'greylist';");
496 }
497
498 # WHO Objects
499
500 # Blacklist
501 my $obj = PMG::RuleDB::EMail->new ('nomail@fromthisdomain.com');
502 my $blacklist = $ruledb->create_group_with_obj(
503 $obj, 'Blacklist', 'Global blacklist');
758c7b6b 504
0a580593
DM
505 # Whitelist
506 $obj = PMG::RuleDB::EMail->new('mail@fromthisdomain.com');
507 my $whitelist = $ruledb->create_group_with_obj(
508 $obj, 'Whitelist', 'Global whitelist');
509
510 # WHEN Objects
511
512 # Working hours
a29b9649 513 $obj = PMG::RuleDB::TimeFrame->new(8*60, 16*60);
0a580593
DM
514 my $working_hours =$ruledb->create_group_with_obj($obj, 'Office Hours' ,
515 'Usual office hours');
516
517 # WHAT Objects
518
0a580593 519 # Images
a29b9649
DM
520 $obj = PMG::RuleDB::ContentTypeFilter->new('image/.*');
521 my $img_content = $ruledb->create_group_with_obj(
522 $obj, 'Images', 'All kinds of graphic files');
523
0a580593 524 # Multimedia
a29b9649
DM
525 $obj = PMG::RuleDB::ContentTypeFilter->new('audio/.*');
526 my $mm_content = $ruledb->create_group_with_obj(
527 $obj, 'Multimedia', 'Audio and Video');
0a580593 528
a29b9649
DM
529 $obj = PMG::RuleDB::ContentTypeFilter->new('video/.*');
530 $ruledb->group_add_object($mm_content, $obj);
0a580593
DM
531
532 # Office Files
a29b9649
DM
533 $obj = PMG::RuleDB::ContentTypeFilter->new('application/vnd\.ms-excel');
534 my $office_content = $ruledb->create_group_with_obj(
535 $obj, 'Office Files', 'Common Office Files');
758c7b6b 536
a29b9649
DM
537 $obj = PMG::RuleDB::ContentTypeFilter->new(
538 'application/vnd\.ms-powerpoint');
758c7b6b 539
a29b9649 540 $ruledb->group_add_object($office_content, $obj);
758c7b6b 541
a29b9649 542 $obj = PMG::RuleDB::ContentTypeFilter->new('application/msword');
0a580593 543 $ruledb->group_add_object ($office_content, $obj);
758c7b6b 544
a29b9649
DM
545 $obj = PMG::RuleDB::ContentTypeFilter->new(
546 'application/vnd\.openxmlformats-officedocument\..*');
547 $ruledb->group_add_object($office_content, $obj);
758c7b6b 548
a29b9649
DM
549 $obj = PMG::RuleDB::ContentTypeFilter->new(
550 'application/vnd\.oasis\.opendocument\..*');
551 $ruledb->group_add_object($office_content, $obj);
0a580593 552
a29b9649
DM
553 $obj = PMG::RuleDB::ContentTypeFilter->new(
554 'application/vnd\.stardivision\..*');
555 $ruledb->group_add_object($office_content, $obj);
758c7b6b 556
a29b9649
DM
557 $obj = PMG::RuleDB::ContentTypeFilter->new(
558 'application/vnd\.sun\.xml\..*');
559 $ruledb->group_add_object($office_content, $obj);
758c7b6b 560
0a580593 561 # Dangerous Content
a29b9649
DM
562 $obj = PMG::RuleDB::ContentTypeFilter->new(
563 'application/x-ms-dos-executable');
564 my $exe_content = $ruledb->create_group_with_obj(
565 $obj, 'Dangerous Content', 'executable files and partial messages');
758c7b6b 566
a29b9649
DM
567 $obj = PMG::RuleDB::ContentTypeFilter->new('application/x-java');
568 $ruledb->group_add_object($exe_content, $obj);
569 $obj = PMG::RuleDB::ContentTypeFilter->new('application/javascript');
570 $ruledb->group_add_object($exe_content, $obj);
571 $obj = PMG::RuleDB::ContentTypeFilter->new('application/x-executable');
572 $ruledb->group_add_object($exe_content, $obj);
79ef2452
DM
573 $obj = PMG::RuleDB::ContentTypeFilter->new('application/x-ms-dos-executable');
574 $ruledb->group_add_object($exe_content, $obj);
a29b9649
DM
575 $obj = PMG::RuleDB::ContentTypeFilter->new('message/partial');
576 $ruledb->group_add_object($exe_content, $obj);
577 $obj = PMG::RuleDB::MatchFilename->new('.*\.(vbs|pif|lnk|shs|shb)');
578 $ruledb->group_add_object($exe_content, $obj);
be6c5e4a 579 $obj = PMG::RuleDB::MatchFilename->new('.*\.\{.+\}');
a29b9649 580 $ruledb->group_add_object($exe_content, $obj);
0a580593
DM
581
582 # Virus
758c7b6b
DM
583 $obj = PMG::RuleDB::Virus->new();
584 my $virus = $ruledb->create_group_with_obj(
585 $obj, 'Virus', 'Matches virus infected mail');
586
0a580593
DM
587 # WHAT Objects
588
589 # Spam
758c7b6b
DM
590 $obj = PMG::RuleDB::Spam->new(3);
591 my $spam3 = $ruledb->create_group_with_obj(
592 $obj, 'Spam (Level 3)', 'Matches possible spam mail');
e7c865af 593
758c7b6b
DM
594 $obj = PMG::RuleDB::Spam->new(5);
595 my $spam5 = $ruledb->create_group_with_obj(
596 $obj, 'Spam (Level 5)', 'Matches possible spam mail');
e7c865af 597
758c7b6b
DM
598 $obj = PMG::RuleDB::Spam->new(10);
599 my $spam10 = $ruledb->create_group_with_obj(
600 $obj, 'Spam (Level 10)', 'Matches possible spam mail');
0a580593
DM
601
602 # ACTIONS
603
758c7b6b
DM
604 # Mark Spam
605 $obj = PMG::RuleDB::ModField->new('X-SPAM-LEVEL', '__SPAM_INFO__');
606 my $mod_spam_level = $ruledb->create_group_with_obj(
e7c865af 607 $obj, 'Modify Spam Level',
758c7b6b 608 'Mark mail as spam by adding a header tag.');
0a580593
DM
609
610 # Mark Spam
758c7b6b
DM
611 $obj = PMG::RuleDB::ModField->new('subject', 'SPAM: __SUBJECT__');
612 my $mod_spam_subject = $ruledb->create_group_with_obj(
e7c865af 613 $obj, 'Modify Spam Subject',
758c7b6b 614 'Mark mail as spam by modifying the subject.');
e7c865af 615
0a580593 616 # Remove matching attachments
758c7b6b
DM
617 $obj = PMG::RuleDB::Remove->new(0);
618 my $remove = $ruledb->create_group_with_obj(
619 $obj, 'Remove attachments', 'Remove matching attachments');
e7c865af 620
0a580593 621 # Remove all attachments
758c7b6b
DM
622 $obj = PMG::RuleDB::Remove->new(1);
623 my $remove_all = $ruledb->create_group_with_obj(
624 $obj, 'Remove all attachments', 'Remove all attachments');
0a580593
DM
625
626 # Accept
758c7b6b
DM
627 $obj = PMG::RuleDB::Accept->new();
628 my $accept = $ruledb->create_group_with_obj(
629 $obj, 'Accept', 'Accept mail for Delivery');
0a580593
DM
630
631 # Block
758c7b6b
DM
632 $obj = PMG::RuleDB::Block->new ();
633 my $block = $ruledb->create_group_with_obj($obj, 'Block', 'Block mail');
0a580593
DM
634
635 # Quarantine
758c7b6b
DM
636 $obj = PMG::RuleDB::Quarantine->new();
637 my $quarantine = $ruledb->create_group_with_obj(
638 $obj, 'Quarantine', 'Move mail to quarantine');
0a580593
DM
639
640 # Notify Admin
758c7b6b
DM
641 $obj = PMG::RuleDB::Notify->new('__ADMIN__');
642 my $notify_admin = $ruledb->create_group_with_obj(
643 $obj, 'Notify Admin', 'Send notification');
0a580593
DM
644
645 # Notify Sender
758c7b6b
DM
646 $obj = PMG::RuleDB::Notify->new('__SENDER__');
647 my $notify_sender = $ruledb->create_group_with_obj(
648 $obj, 'Notify Sender', 'Send notification');
0a580593
DM
649
650 # Add Disclaimer
758c7b6b
DM
651 $obj = PMG::RuleDB::Disclaimer->new ();
652 my $add_discl = $ruledb->create_group_with_obj(
653 $obj, 'Disclaimer', 'Add Disclaimer');
0a580593
DM
654
655 # Attach original mail
656 #$obj = Proxmox::RuleDB::Attach->new ();
758c7b6b 657 #my $attach_orig = $ruledb->create_group_with_obj ($obj, 'Attach Original Mail',
0a580593
DM
658 # 'Attach Original Mail');
659
660 ####################### RULES ##################################
661
662 ## Block Dangerous Files
a29b9649 663 my $rule = PMG::RuleDB::Rule->new ('Block Dangerous Files', 93, 1, 0);
0a580593
DM
664 $ruledb->save_rule ($rule);
665
666 $ruledb->rule_add_what_group ($rule, $exe_content);
667 $ruledb->rule_add_action ($rule, $remove);
668
669 ## Block Viruses
a29b9649 670 $rule = PMG::RuleDB::Rule->new ('Block Viruses', 96, 1, 0);
0a580593
DM
671 $ruledb->save_rule ($rule);
672
673 $ruledb->rule_add_what_group ($rule, $virus);
674 $ruledb->rule_add_action ($rule, $notify_admin);
758c7b6b 675
0a580593
DM
676 if ($testmode) {
677 $ruledb->rule_add_action ($rule, $block);
678 } else {
679 $ruledb->rule_add_action ($rule, $quarantine);
680 }
681
682 ## Virus Alert
a29b9649 683 $rule = PMG::RuleDB::Rule->new ('Virus Alert', 96, 1, 1);
0a580593
DM
684 $ruledb->save_rule ($rule);
685
686 $ruledb->rule_add_what_group ($rule, $virus);
687 $ruledb->rule_add_action ($rule, $notify_sender);
688 $ruledb->rule_add_action ($rule, $notify_admin);
689 $ruledb->rule_add_action ($rule, $block);
758c7b6b 690
0a580593 691 ## Blacklist
a29b9649 692 $rule = PMG::RuleDB::Rule->new ('Blacklist', 98, 1, 0);
0a580593
DM
693 $ruledb->save_rule ($rule);
694
695 $ruledb->rule_add_from_group ($rule, $blacklist);
696 $ruledb->rule_add_action ($rule, $block);
697
698 ## Modify header
699 if (!$testmode) {
a29b9649 700 $rule = PMG::RuleDB::Rule->new ('Modify Header', 90, 1, 0);
0a580593
DM
701 $ruledb->save_rule ($rule);
702 $ruledb->rule_add_action ($rule, $mod_spam_level);
703 }
704
705 ## Whitelist
a29b9649 706 $rule = PMG::RuleDB::Rule->new ('Whitelist', 85, 1, 0);
0a580593
DM
707 $ruledb->save_rule ($rule);
708
709 $ruledb->rule_add_from_group ($rule, $whitelist);
710 $ruledb->rule_add_action ($rule, $accept);
711
712 if ($testmode) {
a29b9649 713 $rule = PMG::RuleDB::Rule->new ('Mark Spam', 80, 1, 0);
0a580593
DM
714 $ruledb->save_rule ($rule);
715
716 $ruledb->rule_add_what_group ($rule, $spam10);
717 $ruledb->rule_add_action ($rule, $mod_spam_level);
718 $ruledb->rule_add_action ($rule, $mod_spam_subject);
719 } else {
720 # Quarantine/Mark Spam (Level 3)
a29b9649 721 $rule = PMG::RuleDB::Rule->new ('Quarantine/Mark Spam (Level 3)', 80, 1, 0);
0a580593
DM
722 $ruledb->save_rule ($rule);
723
724 $ruledb->rule_add_what_group ($rule, $spam3);
725 $ruledb->rule_add_action ($rule, $mod_spam_subject);
726 $ruledb->rule_add_action ($rule, $quarantine);
727 #$ruledb->rule_add_action ($rule, $count_spam);
728 }
729
730 # Quarantine/Mark Spam (Level 5)
a29b9649 731 $rule = PMG::RuleDB::Rule->new ('Quarantine/Mark Spam (Level 5)', 79, 0, 0);
0a580593
DM
732 $ruledb->save_rule ($rule);
733
734 $ruledb->rule_add_what_group ($rule, $spam5);
735 $ruledb->rule_add_action ($rule, $mod_spam_subject);
736 $ruledb->rule_add_action ($rule, $quarantine);
737
738 ## Block Spam Level 10
a29b9649 739 $rule = PMG::RuleDB::Rule->new ('Block Spam (Level 10)', 78, 0, 0);
0a580593
DM
740 $ruledb->save_rule ($rule);
741
742 $ruledb->rule_add_what_group ($rule, $spam10);
743 $ruledb->rule_add_action ($rule, $block);
744
745 ## Block Outgoing Spam
a29b9649 746 $rule = PMG::RuleDB::Rule->new ('Block outgoing Spam', 70, 0, 1);
0a580593
DM
747 $ruledb->save_rule ($rule);
748
749 $ruledb->rule_add_what_group ($rule, $spam3);
750 $ruledb->rule_add_action ($rule, $notify_admin);
751 $ruledb->rule_add_action ($rule, $notify_sender);
752 $ruledb->rule_add_action ($rule, $block);
753
754 ## Add disclaimer
a29b9649 755 $rule = PMG::RuleDB::Rule->new ('Add Disclaimer', 60, 0, 1);
0a580593
DM
756 $ruledb->save_rule ($rule);
757 $ruledb->rule_add_action ($rule, $add_discl);
758
759 # Block Multimedia Files
a29b9649 760 $rule = PMG::RuleDB::Rule->new ('Block Multimedia Files', 87, 0, 2);
0a580593
DM
761 $ruledb->save_rule ($rule);
762
763 $ruledb->rule_add_what_group ($rule, $mm_content);
764 $ruledb->rule_add_action ($rule, $remove);
765
766 #$ruledb->rule_add_from_group ($rule, $anybody);
767 #$ruledb->rule_add_from_group ($rule, $trusted);
768 #$ruledb->rule_add_to_group ($rule, $anybody);
769 #$ruledb->rule_add_what_group ($rule, $ct_filter);
770 #$ruledb->rule_add_action ($rule, $add_discl);
771 #$ruledb->rule_add_action ($rule, $remove);
772 #$ruledb->rule_add_action ($rule, $bcc);
773 #$ruledb->rule_add_action ($rule, $storeq);
774 #$ruledb->rule_add_action ($rule, $accept);
775
776 cond_create_std_actions ($ruledb);
777}
778
8f4e102b
DM
779sub get_remote_time {
780 my ($rdb) = @_;
781
782 my $sth = $rdb->prepare("SELECT EXTRACT (EPOCH FROM TIMESTAMP (0) WITH TIME ZONE 'now') as ctime;");
783 $sth->execute();
784 my $ctinfo = $sth->fetchrow_hashref();
785 $sth->finish ();
786
787 return $ctinfo ? $ctinfo->{ctime} : 0;
788}
789
cfdf6608
DM
790sub init_masterdb {
791 my ($lcid, $database) = @_;
792
793 die "got unexpected cid for new master" if !$lcid;
794
795 my $dbh;
796
797 eval {
798 $dbh = open_ruledb($database);
799
800 $dbh->begin_work;
801
802 print STDERR "update quarantine database\n";
803 $dbh->do ("UPDATE CMailStore SET CID = $lcid WHERE CID = 0;" .
804 "UPDATE CMSReceivers SET CMailStore_CID = $lcid WHERE CMailStore_CID = 0;");
805
806 print STDERR "update statistic database\n";
807 $dbh->do ("UPDATE CStatistic SET CID = $lcid WHERE CID = 0;" .
808 "UPDATE CReceivers SET CStatistic_CID = $lcid WHERE CStatistic_CID = 0;");
809
810 print STDERR "update greylist database\n";
811 $dbh->do ("UPDATE CGreylist SET CID = $lcid WHERE CID = 0;");
812
813 $dbh->commit;
814 };
815 my $err = $@;
816
817 if ($dbh) {
818 $dbh->rollback if $err;
819 $dbh->disconnect();
820 }
821
822 die $err if $err;
823}
824
8fb6f404
DM
825sub purge_statistic_database {
826 my ($dbh, $statlifetime) = @_;
827
828 return if $statlifetime <= 0;
829
830 my (undef, undef, undef, $mday, $mon, $year) = localtime(time());
831 my $end = timelocal(0, 0, 0, $mday, $mon, $year);
832 my $start = $end - $statlifetime*86400;
833
834 # delete statistics older than $start
835
836 my $rows = 0;
837
838 eval {
839 $dbh->begin_work;
840
841 my $sth = $dbh->prepare("DELETE FROM CStatistic WHERE time < $start");
842 $sth->execute;
843 $rows = $sth->rows;
844 $sth->finish;
845
846 if ($rows > 0) {
847 $sth = $dbh->prepare(
848 "DELETE FROM CReceivers WHERE NOT EXISTS " .
849 "(SELECT * FROM CStatistic WHERE CID = CStatistic_CID AND RID = CStatistic_RID)");
850
851 $sth->execute;
852 }
853 $dbh->commit;
854 };
855 if (my $err = $@) {
856 $dbh->rollback;
857 die $err;
858 }
859
860 return $rows;
861}
862
da686811
DM
863sub copy_table {
864 my ($ldb, $rdb, $table) = @_;
865
866 $table = lc($table);
867
868 my $sth = $ldb->column_info(undef, undef, $table, undef);
869 my $attrs = $sth->fetchall_arrayref({});
870
871 my @col_arr;
872 foreach my $ref (@$attrs) {
873 push @col_arr, $ref->{COLUMN_NAME};
874 }
875
876 $sth->finish();
877
878 my $cols = join(', ', @col_arr);
879 $cols || die "unable to fetch column definitions of table '$table' : ERROR";
880
881 $rdb->do("COPY $table ($cols) TO STDOUT");
882
883 my $data = '';
884
885 eval {
886 $ldb->do("COPY $table ($cols) FROM stdin");
887
888 while ($rdb->pg_getcopydata($data) >= 0) {
889 $ldb->pg_putcopydata($data);
890 }
891
892 $ldb->pg_putcopyend();
893 };
894 if (my $err = $@) {
895 $ldb->pg_putcopyend();
896 die $err;
897 }
898}
899
6cf5b4aa
DM
900sub copy_selected_data {
901 my ($dbh, $select_sth, $table, $attrs, $callback) = @_;
902
903 my $count = 0;
904
905 my $insert_sth = $dbh->prepare(
c2e5905e 906 "INSERT INTO ${table}(" . join(',', @$attrs) . ') ' .
6cf5b4aa
DM
907 'VALUES (' . join(',', ('?') x scalar(@$attrs)) . ')');
908
909 while (my $ref = $select_sth->fetchrow_hashref()) {
910 $callback->($ref) if $callback;
911 $count++;
912 $insert_sth->execute(map { $ref->{$_} } @$attrs);
913 }
914
915 return $count;
916}
917
987ee804
DM
918sub update_master_clusterinfo {
919 my ($clientcid) = @_;
920
921 my $dbh = open_ruledb();
922
923 $dbh->do("DELETE FROM ClusterInfo WHERE CID = $clientcid");
924
925 my @mt = ('CMSReceivers', 'CGreylist', 'UserPrefs', 'DomainStat', 'DailyStat', 'VirusInfo');
926
927 foreach my $table (@mt) {
928 $dbh->do ("INSERT INTO ClusterInfo (cid, name, ivalue) select $clientcid, 'lastmt_$table', " .
929 "EXTRACT(EPOCH FROM now())");
930 }
931}
932
933sub update_client_clusterinfo {
934 my ($mastercid) = @_;
935
936 my $dbh = open_ruledb();
937
938 $dbh->do ("DELETE FROM StatInfo"); # not needed at node
939
940 $dbh->do ("DELETE FROM ClusterInfo WHERE CID = $mastercid");
941
942 $dbh->do ("INSERT INTO ClusterInfo (cid, name, ivalue) select $mastercid, 'lastid_CMailStore', " .
943 "COALESCE (max (rid), -1) FROM CMailStore WHERE cid = $mastercid");
944
945 $dbh->do ("INSERT INTO ClusterInfo (cid, name, ivalue) select $mastercid, 'lastid_CStatistic', " .
946 "COALESCE (max (rid), -1) FROM CStatistic WHERE cid = $mastercid");
947
948 my @mt = ('CMSReceivers', 'CGreylist', 'UserPrefs', 'DomainStat', 'DailyStat', 'VirusInfo');
949
950 foreach my $table (@mt) {
951 $dbh->do ("INSERT INTO ClusterInfo (cid, name, ivalue) select $mastercid, 'lastmt_$table', " .
952 "COALESCE (max (mtime), 0) FROM $table");
953 }
954}
955
7b313034
DM
956sub create_clusterinfo_default {
957 my ($dbh, $rcid, $name, $ivalue, $svalue) = @_;
958
959 my $sth = $dbh->prepare("SELECT * FROM ClusterInfo WHERE CID = ? AND Name = ?");
960 $sth->execute($rcid, $name);
961 if (!$sth->fetchrow_hashref()) {
962 $dbh->do("INSERT INTO ClusterInfo (CID, Name, IValue, SValue) " .
963 "VALUES (?, ?, ?, ?)", undef,
964 $rcid, $name, $ivalue, $svalue);
965 }
966 $sth->finish();
967}
968
969sub read_int_clusterinfo {
970 my ($dbh, $rcid, $name) = @_;
971
972 my $sth = $dbh->prepare(
973 "SELECT ivalue as value FROM ClusterInfo " .
974 "WHERE cid = ? AND NAME = ?");
975 $sth->execute($rcid, $name);
976 my $cinfo = $sth->fetchrow_hashref();
977 $sth->finish();
978
979 return $cinfo->{value};
980}
981
982sub write_maxint_clusterinfo {
983 my ($dbh, $rcid, $name, $value) = @_;
984
2e049252 985 $dbh->do("UPDATE ClusterInfo SET ivalue = GREATEST(ivalue, ?) " .
7b313034
DM
986 "WHERE cid = ? AND name = ?", undef,
987 $value, $rcid, $name);
988}
989
987ee804
DM
990sub init_nodedb {
991 my ($cinfo) = @_;
992
993 my $ni = $cinfo->{master};
994
995 die "no master defined - unable to sync data from master\n" if !$ni;
996
997 my $master_ip = $ni->{ip};
998 my $master_cid = $ni->{cid};
22c16ff6 999 my $master_name = $ni->{name};
987ee804
DM
1000
1001 my $fn = "/tmp/masterdb$$.tar";
1002 unlink $fn;
1003
1004 my $dbname = $default_db_name;
1005
1006 eval {
1007 print STDERR "copying master database from '${master_ip}'\n";
1008
97cab76a
DM
1009 open (my $fh, ">", $fn) || die "open '$fn' failed - $!\n";
1010
1011 postgres_admin_cmd(
1012 ['/usr/bin/ssh', '-o', 'BatchMode=yes',
1013 '-o', "HostKeyAlias=${master_name}",
1014 $master_ip, 'pg_dump'],
1015 { output => '>&' . fileno($fh) },
1016 $dbname, '-F', 'c');
22c16ff6 1017
97cab76a 1018 close($fh);
987ee804
DM
1019
1020 my $size = -s $fn;
1021
1022 print STDERR "copying master database finished (got $size bytes)\n";
1023
1024 print STDERR "delete local database\n";
1025
97cab76a 1026 postgres_admin_cmd('dropdb', undef, $dbname , '--if-exists');
987ee804
DM
1027
1028 print STDERR "create new local database\n";
1029
97cab76a 1030 postgres_admin_cmd('createdb', undef, $dbname);
987ee804
DM
1031
1032 print STDERR "insert received data into local database\n";
1033
1034 my $mess;
1035 my $parser = sub {
1036 my $line = shift;
1037
1038 if ($line =~ m/restoring data for table \"(.+)\"/) {
1039 print STDERR "restoring table $1\n";
1040 } elsif (!$mess && ($line =~ m/creating (INDEX|CONSTRAINT)/)) {
1041 $mess = "creating indexes";
1042 print STDERR "$mess\n";
1043 }
1044 };
1045
97cab76a
DM
1046 my $opts = {
1047 outfunc => $parser,
1048 errfunc => $parser,
1049 errmsg => "pg_restore failed"
1050 };
1051
1052 postgres_admin_cmd('pg_restore', $opts, '-d', $dbname, '-v', $fn);
987ee804
DM
1053
1054 print STDERR "run analyze to speed up database queries\n";
1055
97cab76a 1056 postgres_admin_cmd('psql', { input => 'analyze;' }, $dbname);
987ee804
DM
1057
1058 update_client_clusterinfo($master_cid);
1059 };
1060
1061 my $err = $@;
1062
1063 unlink $fn;
1064
1065 die $err if $err;
1066}
1067
f3464b71
DM
1068sub cluster_sync_status {
1069 my ($cinfo) = @_;
1070
1071 my $dbh;
1072
1073 my $minmtime;
1074
1075 foreach my $ni (values %{$cinfo->{ids}}) {
1076 next if $cinfo->{local}->{cid} == $ni->{cid}; # skip local CID
1077 $minmtime->{$ni->{cid}} = 0;
1078 }
1079
1080 eval {
1081 $dbh = open_ruledb();
1082
1083 my $sth = $dbh->prepare(
1084 "SELECT cid, MIN (ivalue) as minmtime FROM ClusterInfo " .
1085 "WHERE name = 'lastsync' AND ivalue > 0 " .
1086 "GROUP BY cid");
1087
1088 $sth->execute();
1089
1090 while (my $info = $sth->fetchrow_hashref()) {
1091 foreach my $ni (values %{$cinfo->{ids}}) {
1092 next if $cinfo->{local}->{cid} == $ni->{cid}; # skip local CID
1093 if ($ni->{cid} == $info->{cid}) { # node exists
1094 $minmtime->{$ni->{cid}} = $info->{minmtime};
1095 }
1096 }
1097 }
1098
1099 $sth->finish ();
1100 };
1101 my $err = $@;
1102
1103 $dbh->disconnect() if $dbh;
1104
b902c0b8 1105 syslog('err', $err) if $err;
f3464b71
DM
1106
1107 return $minmtime;
1108}
1109
cfdf6608 1110
a355f100 11111;