funktionen in common.php ausgelagert, treiberweichen für datenbankabfragen, spamschutz hinzugefügt

This commit is contained in:
bernd 2021-03-04 19:25:48 +01:00
parent 2a804e985d
commit 4ffaf6f54d
3 changed files with 226 additions and 93 deletions

67
lib/common.php Normal file
View file

@ -0,0 +1,67 @@
<?php
/**
* file: common.php
* date: 03.03.2021
* author: bernd@nr18.space
* desc: Allgemeine Funktionen, die mal in lib/db.php waren aber nichts mit
* der Datenbank zu tun haben.
*/
if (!defined('INCLUDES_ALLOWED'))
die('Access denied.');
function getNow(): int {
/**
* Zeitzone aud Berlin setzen und die aktuelle Zeit in Sekunden seit
* 01.01.1970 zurückgeben.
*/
date_default_timezone_set("Europe/Berlin");
return time();
}
function getNick(string $mid): string
{
/**
* Extrahiert aus einer Matrix-ID den localpart.
* TODO: In eine bibliothek auslagern? (/lib/common)
*/
$uid = "";
$append = false;
$strarray = str_split($mid);
foreach ($strarray as $char)
{
if ($char == '@')
{
$append = true;
}
else if ($char == ':')
{
return $uid;
}
else
{
if ($append === true)
{
$uid = $uid.$char;
}
}
}
}
function getRemoteHexIP() {
/**
* Wandelt die IP des anfragenden Clients in einen Hexadezimalen
* String um.
* TODO: Funktion allgemeiner halten? IP als Parameter übergeben?
*/
return bin2hex(inet_pton($_SERVER['REMOTE_ADDR']));
}

View file

@ -15,6 +15,9 @@ ini_set("display_startip_errors", "on");
if (!defined('INCLUDES_ALLOWED'))
die('Access denied.');
require_once("common.php");
function testDriver(): bool
{
print_r(PDO::getAvailableDrivers());
@ -112,8 +115,11 @@ class Database {
* und die Instanz des Loggers. Läßt kontrollieren ob die Tabelle zum
* Eintragen der Requests vorhanden ist und stößt die Erstellung der
* Tabelle an, wenn sie fehlt.
* TODO: Geht das eleganter? So wird das bei jeder Anfrage
* ausgeführt.
* TODO: Geht das eleganter? So wird bei jedem Request kontrolliert,
* ob die Tabelle vorhanden ist. Anderseits gibt eine Konstruktor
* nichts zurück. Ergo wird bei einem Fehler beim Erstellen der
* Tabelle auch nicht abgebrochen, sondern erst beim Versuch etwas
* hineinzuschreiben.
*/
$this->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;
}

View file

@ -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;
}
}
?>