summaryrefslogtreecommitdiffstats
path: root/inc
diff options
context:
space:
mode:
authorSimon Rettberg2021-06-25 16:21:17 +0200
committerSimon Rettberg2021-06-25 16:21:17 +0200
commit32f0677dbca9e3347b931c1d0105eb37aa57e90d (patch)
treeddad4562e7ee8439a24e2462d44614692bb71d14 /inc
parentUpdate .idea (diff)
downloadslx-admin-32f0677dbca9e3347b931c1d0105eb37aa57e90d.tar.gz
slx-admin-32f0677dbca9e3347b931c1d0105eb37aa57e90d.tar.xz
slx-admin-32f0677dbca9e3347b931c1d0105eb37aa57e90d.zip
[eventlog] Add event filtering and notification system
Diffstat (limited to 'inc')
-rw-r--r--inc/database.inc.php35
-rw-r--r--inc/eventlog.inc.php25
-rw-r--r--inc/mailer.inc.php178
3 files changed, 232 insertions, 6 deletions
diff --git a/inc/database.inc.php b/inc/database.inc.php
index a55555b4..09006f3e 100644
--- a/inc/database.inc.php
+++ b/inc/database.inc.php
@@ -100,6 +100,41 @@ class Database
}
/**
+ * Fetch and group by first column. First column is key, value is a list of rows with remaining columns.
+ * [
+ * col1 => [
+ * [col2, col3],
+ * [col2, col3],
+ * ],
+ * ...,
+ * ]
+ *
+ * @return array|bool Associative array, first column is key, remaining columns are array values
+ */
+ public static function queryGroupList($query, $args = array(), $ignoreError = null)
+ {
+ $res = self::simpleQuery($query, $args, $ignoreError);
+ if ($res === false)
+ return false;
+ return $res->fetchAll(PDO::FETCH_GROUP);
+ }
+
+ /**
+ * Fetch and use first column as key of returned array.
+ * This is like queryGroup list, but it is assumed that the first column is unique, so
+ * the remaining columns won't be wrapped in another array.
+ *
+ * @return array|bool Associative array, first column is key, remaining columns are array values
+ */
+ public static function queryIndexedList($query, $args = array(), $ignoreError = null)
+ {
+ $res = self::simpleQuery($query, $args, $ignoreError);
+ if ($res === false)
+ return false;
+ return $res->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_UNIQUE);
+ }
+
+ /**
* Execute the given query and return the number of rows affected.
* Mostly useful for UPDATEs or INSERTs
*
diff --git a/inc/eventlog.inc.php b/inc/eventlog.inc.php
index 3ebb82a4..a29261b8 100644
--- a/inc/eventlog.inc.php
+++ b/inc/eventlog.inc.php
@@ -17,12 +17,14 @@ class EventLog
error_log($message);
return;
}
+ $data = [
+ 'type' => $type,
+ 'message' => $message,
+ 'details' => $details
+ ];
Database::exec("INSERT INTO eventlog (dateline, logtypeid, description, extra)"
- . " VALUES (UNIX_TIMESTAMP(), :type, :message, :details)", array(
- 'type' => $type,
- 'message' => $message,
- 'details' => $details
- ), true);
+ . " VALUES (UNIX_TIMESTAMP(), :type, :message, :details)", $data, true);
+ self::applyFilterRules('#serverlog', $data);
}
public static function failure($message, $details = '')
@@ -51,5 +53,16 @@ class EventLog
return;
Database::exec("TRUNCATE eventlog");
}
-
+
+ /**
+ * @param string $type the event. Will either be client state like ~poweron, ~runstate etc. or a client log type
+ * @param array $data A structured array containing event specific data that can be matched.
+ */
+ public static function applyFilterRules(string $type, array $data)
+ {
+ if (!Module::isAvailable('eventlog'))
+ return;
+ FilterRuleProcessor::applyFilterRules($type, $data);
+ }
+
}
diff --git a/inc/mailer.inc.php b/inc/mailer.inc.php
new file mode 100644
index 00000000..942bfc75
--- /dev/null
+++ b/inc/mailer.inc.php
@@ -0,0 +1,178 @@
+<?php
+
+class Mailer
+{
+
+ /** @var array|null */
+ private static $configs = null;
+
+ /** @var array */
+ private $curlOptions;
+
+ /** @var string|null */
+ private $replyTo;
+
+ /** @var string */
+ private $errlog = '';
+
+ // $keys = array('host', 'port', 'ssl', 'senderAddress', 'replyTo', 'username', 'password', 'serverName');
+ public function __construct(string $hosturi, bool $startTls, string $from, string $user, string $pass, string $replyTo = null)
+ {
+ $this->curlOptions = [
+ CURLOPT_URL => $hosturi,
+ CURLOPT_USE_SSL => $startTls ? CURLUSESSL_ALL : CURLUSESSL_TRY,
+ CURLOPT_MAIL_FROM => $from,
+ CURLOPT_UPLOAD => 1,
+ CURLOPT_VERBOSE => 1, // XXX
+ ];
+ if (!empty($user)) {
+ $this->curlOptions += [
+ CURLOPT_LOGIN_OPTIONS => 'AUTH=NTLM;AUTH=DIGEST-MD5;AUTH=CRAM-MD5;AUTH=PLAIN;AUTH=LOGIN',
+ CURLOPT_USERNAME => $user,
+ CURLOPT_PASSWORD => $pass,
+ ];
+ }
+ $this->replyTo = $replyTo;
+ }
+
+ /**
+ * Send a mail to given list of recipients.
+ * @param string[] $rcpts Recipients
+ * @param string $subject Mail subject
+ * @param string $text Mail body
+ * @return int curl error code, CURLE_OK on success
+ */
+ public function send(array $rcpts, string $subject, string $text): int
+ {
+ // Convert all line breaks to CRLF while trying to avoid introducing additional ones
+ $text = str_replace(["\r\n", "\r"], "\n", $text);
+ $text = str_replace("\n", "\r\n", $text);
+ $mail = ["Date: " . date('r')];
+ foreach ($rcpts as $r) {
+ $mail[] = self::mimeEncode('To', $r);
+ }
+ $mail[] = self::mimeEncode('Content-Type', 'text/plain; charset="utf-8"');
+ $mail[] = self::mimeEncode('Subject', $subject);
+ $mail[] = self::mimeEncode('From', $this->curlOptions[CURLOPT_MAIL_FROM]);
+ $mail[] = self::mimeEncode('Message-ID', '<' . Util::randomUuid() . '@rfcpedant.example.org>');
+ if (!empty($this->replyTo)) {
+ $mail[] = self::mimeEncode('Reply-To', $this->replyTo);
+ }
+
+ $mail = implode("\r\n", $mail) . "\r\n\r\n" . $text;
+ $c = curl_init();
+ $pos = 0;
+ $this->curlOptions[CURLOPT_MAIL_RCPT] = array_map(function ($mail) {
+ return preg_replace('/\s.*$/', '', $mail);
+ }, $rcpts);
+ $this->curlOptions[CURLOPT_READFUNCTION] = function($ch, $fp, $len) use (&$pos, $mail) {
+ $txt = substr($mail, $pos, $len);
+ $pos += strlen($txt);
+ return $txt;
+ };
+ $err = fopen('php://temp', 'w+');
+ $this->curlOptions[CURLOPT_STDERR] = $err;
+ curl_setopt_array($c, $this->curlOptions);
+ curl_exec($c);
+ rewind($err);
+ $this->errlog = stream_get_contents($err);
+ fclose($err);
+ return curl_errno($c);
+ }
+
+ public function getLog(): string
+ {
+ return $this->errlog;
+ }
+
+ public static function queue(int $configid, array $rcpts, string $subject, string $text)
+ {
+ foreach ($rcpts as $rcpt) {
+ Database::exec("INSERT INTO mail_queue (rcpt, subject, body, dateline, configid)
+ VALUES (:rcpt, :subject, :body, UNIX_TIMESTAMP(), :config)",
+ ['rcpt' => $rcpt, 'subject' => $subject, 'body' => $text, 'config' => $configid]);
+ }
+ }
+
+ public static function flushQueue()
+ {
+ $list = Database::queryGroupList("SELECT Concat(configid, rcpt, subject) AS keyval,
+ mailid, configid, rcpt, subject, body, dateline FROM mail_queue
+ WHERE nexttry <= UNIX_TIMESTAMP() LIMIT 20");
+ $cutoff = time() - 43200; // 12h
+ $mailers = [];
+ foreach ($list as $mails) {
+ $delete = [];
+ $body = [];
+ foreach ($mails as $mail) {
+ $delete[] = $mail['mailid'];
+ if ($mail['dateline'] < $cutoff) {
+ EventLog::info("Dropping queued mail '{$mail['subject']}' for {$mail['rcpt']} as it's too old.");
+ continue; // Ignore
+ }
+ $body[] = ' *** ' . date('d.m.Y H:i:s', $mail['dateline']) . "\r\n"
+ . $mail['body'];
+ }
+ if (!empty($body) && isset($mail)) {
+ $body = implode("\r\n\r\n - - - - -\r\n\r\n", $body);
+ if (!isset($mailers[$mail['configid']])) {
+ $mailers[$mail['configid']] = self::instanceFromConfig($mail['configid']);
+ if ($mailers[$mail['configid']] === null) {
+ EventLog::failure("Invalid mailer config id: " . $mail['configid']);
+ }
+ }
+ if (($mailer = $mailers[$mail['configid']]) !== null) {
+ $ret = $mailer->send([$mail['rcpt']], $mail['subject'], $body);
+ if ($ret !== CURLE_OK) {
+ Database::exec("UPDATE mail_queue SET nexttry = UNIX_TIMESTAMP() + 7200
+ WHERE mailid IN (:ids)", ['ids' => $delete]);
+ $delete = [];
+ EventLog::info("Error sending mail '{$mail['subject']}' for {$mail['rcpt']}.",
+ $mailer->getLog());
+ }
+ }
+ }
+ // Now delete these, either sending succeeded or it's too old
+ if (!empty($delete)) {
+ Database::exec("DELETE FROM mail_queue WHERE mailid IN (:ids)", ['ids' => $delete]);
+ }
+ }
+ }
+
+ private static function mimeEncode(string $field, string $string): string
+ {
+ if (preg_match('/[\r\n\x7f-\xff]/', $string)) {
+ return iconv_mime_encode($field, $string);
+ }
+ return "$field: $string";
+ }
+
+ private static function getConfig(int $configId): array
+ {
+ if (!is_array(self::$configs)) {
+ self::$configs = Database::queryIndexedList("SELECT configid, host, port, `ssl`, senderaddress, replyto,
+ username, password
+ FROM mail_config");
+ }
+ return self::$configs[$configId] ?? [];
+ }
+
+ /**
+ * @param int $configId
+ * @return Mailer|null
+ */
+ public static function instanceFromConfig(int $configId)
+ {
+ $config = self::getConfig($configId);
+ if (empty($config))
+ return null;
+ if ($config['ssl'] === 'IMPLICIT') {
+ $uri = "smtps://{$config['host']}:${config['port']}";
+ } else {
+ $uri = "smtp://{$config['host']}:${config['port']}";
+ }
+ return new Mailer($uri, $config['ssl'] === 'EXPLICIT', $config['senderaddress'], $config['username'],
+ $config['password'], $config['replyto']);
+ }
+
+} \ No newline at end of file