From 4ffaf6f54d2c55a67906ddd53a80e6d5cf053b15 Mon Sep 17 00:00:00 2001 From: bernd Date: Thu, 4 Mar 2021 19:25:48 +0100 Subject: [PATCH] =?UTF-8?q?funktionen=20in=20common.php=20ausgelagert,=20t?= =?UTF-8?q?reiberweichen=20f=C3=BCr=20datenbankabfragen,=20spamschutz=20hi?= =?UTF-8?q?nzugef=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/common.php | 67 ++++++++++++++++++++++ lib/db.php | 105 +++++++++++++++++++--------------- lib/request.php | 147 ++++++++++++++++++++++++++++++++---------------- 3 files changed, 226 insertions(+), 93 deletions(-) create mode 100644 lib/common.php diff --git a/lib/common.php b/lib/common.php new file mode 100644 index 0000000..878b45e --- /dev/null +++ b/lib/common.php @@ -0,0 +1,67 @@ +pdo = $pdo; @@ -134,14 +140,18 @@ class Database { /** * Kontrolliert, ob eine Tabelle zum Eintragen der Requests * vorhanden ist. - * TODO: Die Query ist Postgres spezifisch. Treiber durchreichen und - * eine Weiche anlegen? */ $this->log->d("Check if table requests exists"); - $stmt = "SELECT * FROM information_schema.tables WHERE + if ($this->pdo->driver === "PDO_PGSQL") { + $stmt = "SELECT * FROM information_schema.tables WHERE table_type = 'BASE TABLE' and table_name = 'requests'"; + } else if ($this->pdo->driver === "PDO_PSQLITE") { + // ungetestet + $stmt = "SELECT 1 FROM sqlite_master WHERE type='table' and + name='requests'"; + } try { $response = $this->pdo->query($stmt); } catch (PDOException $e) { @@ -164,13 +174,24 @@ class Database { */ $this->log->n("Try to create table requests"); - $stmt = "CREATE TABLE IF NOT EXISTS requests ( + if ($this->pdo->driver === "PDO_PGSQL") { + $stmt = "CREATE TABLE IF NOT EXISTS requests ( id serial PRIMARY KEY, nick varchar(80) NOT NULL UNIQUE, email varchar(80) NOT NULL, token char(32) NOT NULL UNIQUE, ip bytea, time integer NOT NULL);"; + } else if ($this->pdo->driver === "PDO_PSQLITE") { + // ungetestet + $stmt = "CREATE TABLE IF NOT EXISTS request ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + nick TEXT NUT NULL UNIQUE; + email TEXT NOT NULL, + token TEXT NOT NULL UNIQUE, + ip BLOB, + time INTEGER NOT NULL);"; + } try { $this->pdo->exec($stmt); } catch (PDOException $e) { @@ -191,7 +212,8 @@ class Database { * Sollte diese eine Exception werfen, wird true (es gibt einen * Benutzer) zurück gegeben. Ansonsten wird der Rückgabewert * von der Funktion getNick ausgewertet, die aus der übergebenen - * Matrix-ID den localpart extrahiert. + * Matrix-ID den localpart extrahiert. Nutzt für die Suche die + * Klassenfunktion searchUser(). Braucht getNick() common.php! */ $this->log->d("Search for localpart {$nick} in users"); @@ -212,7 +234,7 @@ class Database { else { foreach ($response as $array) { - $uid = $this->getNick($array['name']); + $uid = getNick($array['name']); $this->log->d("Compare {$nick} with {$uid}"); if ($uid === $nick) { $this->log->i("MXID localpart already exists: {$nick}"); @@ -232,7 +254,8 @@ class Database { * Nutzernamen. Die Abfrage der Datenbank wird an die Funktion * searchUser delegiert. Sollte diese eine Exception werfen, wird * true (es gibt einen benutzer) zurück gegeben. Ansonsten wird die - * Anzahl der Treffer ausgewertet. + * Anzahl der Treffer ausgewertet. Nutzt für die Suche die eigene + * Funktion searchUser(). */ $this->log->d("Search for localpart {$nick} in requests"); @@ -245,7 +268,7 @@ class Database { } $count = count($response); if ($count > 0) { - $this->log->d("Search for {$nick}: {$count} hit(s)"); + $this->log->n("Search for {$nick}: {$count} hit(s)"); return true; } $this->log->d("Nothing found"); @@ -278,55 +301,47 @@ class Database { } } - private function getNick(string $mid): string - { + public function getTimestamps(): array { + /** - * Extrahiert aus einer Matrix-ID den localpart. - * TODO: In eine bibliothek auslagern? (/lib/common) + * Schaut in der Datenbank, ob es bereits Einträge mit der aktuellen + * Remote IP gibt und gibt alle Einträge des dazu gehörendem + * Zeitstempels zurück. Braucht getRemoteHexIP() aus common.php! + * TODO: flexibler gestalten? IP als Parameter übergeben? */ - - $this->log->d("Extract nick from {$mid}"); - $uid = ""; - $append = false; - $strarray = str_split($mid); - foreach ($strarray as $char) - { - if ($char == '@') - { - $append = true; - } - else if ($char == ':') - { - $this->log->d("Extracted: {$uid}"); - return $uid; - } - else - { - if ($append === true) - { - $uid = $uid.$char; - } - } + + $ip = getRemoteHexIP(); + $this->log->i("Search for IP: {$_SERVER['REMOTE_ADDR']}"); + $stmt = $this->pdo->prepare("SELECT time FROM requests WHERE + ip = :ip"); + try { + $stmt->BindValue(':ip', $ip, PDO::PARAM_LOB); + $stmt->execute(); + $response = $stmt->fetchAll(); + } catch (PDOException $e) { + $this->log->e("getIPs: ERROR: {$e->getMessage()}"); } + $count = count($response); + $this->log->d("{$count} Timestamps found");; + return $response; } public function saveRequest($token): bool { /** * Speichert den gewünschten Nick, die Emailadresse, das Token, die - * IP und einen Zeitstempel in der Tabelle Requests. + * IP und einen Zeitstempel in der Tabelle Requests. Braucht die + * Funktionen getRemoteHexIP() und getNow() aus common.php. * TODO: IP nicht Hexadezimal, sondern Binär speichern. Spart Platz * und ist schneller. Bin ich leider zu blöd für. * TODO: Sollten/Müssen Nick und Email noch durch htmlspecialchars() * oder reichen die prepared Statments? */ - $bin = inet_pton($_SERVER['REMOTE_ADDR']); - $ip = bin2hex($bin); + $ip = getRemoteHexIP(); $nick = $_POST['login']; $email = $_POST['email']; - date_default_timezone_set("Europe/Berlin"); - $time = time(); + $time = getNow(); $this->log->d("Save request for: {$nick} with {$token} at {$time}"); try { $stmt = $this->pdo->prepare("INSERT INTO requests @@ -343,11 +358,9 @@ class Database { $this->log->e("Error: {$e->getMessage()}"); return false; } - if ($response === 1) { - $this->log->i("Request saved successfull"); + if ($response) { + $this->log->n("Request saved successfull"); return true; - } else { - $this->log->e("Database returns: {$response}"); } return false; } diff --git a/lib/request.php b/lib/request.php index bb8941b..1d46e4b 100644 --- a/lib/request.php +++ b/lib/request.php @@ -13,6 +13,7 @@ if (!defined('INCLUDES_ALLOWED')) require("base.php"); require("static/mail.php"); +require_once("common.php"); class Request extends BaseClass { @@ -52,6 +53,9 @@ class Request extends BaseClass { } else if (false === $this->checkUser()) { $message = "User Id is already taken"; return false; + } else if (false === $this->checkRequests()) { + $message = "Too many requests"; + return false; } else { if ($this->generateToken(16) === true) { if ($this->saveRequest() === true) { @@ -67,53 +71,6 @@ class Request extends BaseClass { return false; } - - private function saveRequest(): bool { - - /** - * Veranlaßt die Speicherung der Anfrage in der Tabelle requests. - * Bekommt aus der Datenbank (auch im Falle einer PDO Exception) - * einen Boolean zurück. - */ - - try { - $response = $this->db->saveRequest($this->token); - } catch (Exception $e) { - $this->log->e("Error: Database returns: {$e->getMessage()}"); - } - if ($response === true) { - return true; - } - return false; - } - - private function sendVerificationMail(): bool { - - /** - * Verschickt die Mail mit dem Verifizierungslink. - * TODO: Reicht filter_input()? Was kann hier passieren? - */ - - $mailTo = filter_input(INPUT_POST, 'email', FILTER_SANITIZE_EMAIL); - $mxdomain = $this->config->getMxDomain(); - $baseurl = $this->config->getBaseURL(); - $validator = $this->config->getValidator(); - $mailFrom = $this->config->getMailFrom(); - $mailSubject = $this->config->getMailSubject(); - $mailClosure = $this->config->getMailClosure(); - $this->log->d("Try to send verification mail"); - - $link = $baseurl . $validator . $this->token . "\r\n\r\n"; - $mailbody = MAILTEXT1 . " {$mxdomain} " . MAILTEXT2 . "\r\n\r\n" . $link . $mailClosure; - if (mail($mailTo, $mailSubject, $mailbody, $mailFrom)) - { - $this->log->i("Validationmail successfull send"); - return true; - } - $this->log->e("Sending validation mail failed"); - return false; - } - private function checkCaptcha(): bool { /** @@ -205,6 +162,102 @@ class Request extends BaseClass { return true; } + private function checkRequests(): bool { + + /** + * Prüft, ob für es von der aktuellen Remote IP bereits Anfragen + * gibt. Diese sollten gewisse Limits nicht überschreiten. + */ + + $timestamps = $this->db->getTimestamps(); + $now = getNow(); + + // Wenn es der erste Request ist -> return true + $count = count($timestamps); + if ($count === 0) { + $this->log->i("First request from {$_SERVER['REMOTE_ADDR']}"); + return true; + } + $first = $timestamps[array_key_first($timestamps)]['time']; + $last = $timestamps[array_key_last($timestamps)]['time']; + $duration = $last - $first; + $average = $duration / $count; + + // Maximal 10 Anfragen in 24 Stunden + if ($count > 10 && $duration < 86400) { + $this->log->n("To many requests in 24 hours"); + return false; + } + + // Wenn der letzte Request weniger als 10 Sekunden her ist -> return + // false + if (($now - $last) < 5) { + $this->log->n("Time between two requests under 5 seconds"); + return false; + } + + // Wenn die durchschnittliche Dauer zwischen allen Anfragen unter 30 + // Sekunden liegt -> return false + if (($average) < 30 && $count > 3) { + $this->log->n("Duration between all requests under 10 seconds"); + return false; + } + + // nicht mehr als 20 Anfragen von einer IP in der Datenbank + // speichern -> return false + if ($count > 20) { + $this->log->n("To many requests in database"); + return false; + } + return true; + } + + private function saveRequest(): bool { + + /** + * Veranlaßt die Speicherung der Anfrage in der Tabelle requests. + * Bekommt aus der Datenbank (auch im Falle einer PDO Exception) + * einen Boolean zurück. + */ + + try { + $response = $this->db->saveRequest($this->token); + } catch (Exception $e) { + $this->log->e("Error: Database returns: {$e->getMessage()}"); + } + if ($response === true) { + return true; + } + return false; + } + + private function sendVerificationMail(): bool { + + /** + * Verschickt die Mail mit dem Verifizierungslink. + * TODO: Reicht filter_input()? Was kann hier passieren? + */ + + $mailTo = filter_input(INPUT_POST, 'email', FILTER_SANITIZE_EMAIL); + $mxdomain = $this->config->getMxDomain(); + $baseurl = $this->config->getBaseURL(); + $validator = $this->config->getValidator(); + $mailFrom = $this->config->getMailFrom(); + $mailSubject = $this->config->getMailSubject(); + $mailClosure = $this->config->getMailClosure(); + $this->log->d("Try to send verification mail"); + + $link = $baseurl . $validator . $this->token . "\r\n\r\n"; + $mailbody = MAILTEXT1 . " {$mxdomain} " . MAILTEXT2 . "\r\n\r\n" . $link . $mailClosure; + if (mail($mailTo, $mailSubject, $mailbody, $mailFrom)) + { + $this->log->i("Validationmail successfull send"); + return true; + } + $this->log->e("Sending validation mail failed"); + return false; + } + } ?>