matrix-register/lib/request.php

278 lines
9.7 KiB
PHP

<?php
/**
* file: lib/request.php
* date: 28.02.2021
* user: bernd@nr18.space
* desc:
*/
if (!defined('INCLUDES_ALLOWED'))
die('Access denied.');
require("base.php");
require("static/mail.php");
require_once("common.php");
class Request extends BaseClass {
/**
* Klasse zur Bearbeitung einer Anfrage nach einem Matrix Account. Erbt
* aus der Klasse BaseClass ein Konfigurations- und ein Datenbankobjekt
* ($this->config, $this->db), die Funktion generateToken() und sowie
* die Variable $this-token.
* @param string $message
* @return bool
*/
public function checkRequest(string &$message): bool {
/**
* Hauptfunktion der Klasse Request - steuert alle Prüfungen, das
* Speichern des Requests und das Versenden der Email. Gibt True
* oder False zurück und setzt die Variable "message", welche auf
* der Webseite ausgegeben wird.
*/
if (!isset($this->db)) {
$this->log->e("There is no database");
$message = "Something goes wrong";
return false;
}
$ip = getRemoteHexIP();
$this->log->i("Request started for nick: {$_POST['login']}");
try {
if (false === $this->checkCaptcha()) {
$message = "Captcha invalid";
return false;
} else if (false === $this->checkEmail()) {
$message = "Email invalid";
return false;
} else if (false === $this->checkMXID($this->config->getMxDomain())) {
$message = "User ID invalid";
return false;
} else if (false === $this->checkUser($_POST['login'] ?? '')) {
$message = "User Id is already taken";
return false;
} else if (false === $this->checkRequests($ip)) {
$message = "Too many requests";
return false;
} else {
if ($this->generateToken(16) === true) {
if ($this->saveRequest($ip) === true) {
if ($this->sendVerificationMail() === true) {
$login = htmlspecialchars($_POST['login']);
$message = "Your request for '{$login}' is saved and a
verification mail is send";
return true;
}
}
}
}
} catch (Throwable $e) {
$this->log->e($e->getMessage());
$message = "unexpected error";
}
return false;
}
private function checkCaptcha(): bool {
/**
* Prüfen, ob das Captcha die korrekte Hausnummer abbildet.
* Filter_input gibt im Erfolgsfall einen Integer zurück.
*/
$this->log->d("Checking captcha");
$captcha = filter_input(INPUT_POST, 'captcha', FILTER_VALIDATE_INT);
if ($captcha == 26) {
$this->log->d("Captcha valid");
return true;
}
$this->log->e("Invalid captcha");
return false;
}
private function checkEmail(): bool {
/**
* Prüfen, ob die Emailadresse schematisch gültig ist. filter_input
* gibt im Erfolgsfall den Wert, im Fehlerfall false oder null, wenn
* die Variable nicht gestzt ist, zurück. Letzteres wird beim
* vorangehenden Test checkForms geprüft und kann daher nicht
* auftreten.
*/
$this->log->d("Checking email schema");
if (filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL)) {
$this->log->d("Email schema is valid");
return true;
}
$this->log->e("Email schema invalid");
return false;
}
private function checkMXID(string $mxid): bool {
/**
* Prüft, ob der gewünschte localpart nur Zeichen enthält, die von
* der Matrix-Spezifikation erlaubt sind. Die Spezifikation erlaubt
* nur Kleinbuchstaben, Ziffern, Minus, Punkt, Gleichheitszeichen,
* Unterstrich und Schrägstrich. Geprüft wird auch, ob die Länge des
* gewünschten localpart zusammen mit dem domainpart sowie dem @ und
* : die Länge von 255 Zeichen nicht überschreiten.
*/
$this->log->d("Check MXID localpart");
$str_array = str_split($_POST['login']);
$max_length = 255 - (mb_strlen($mxid) + 2);
$specials = array('-', '.', '=', '_', '/');
$numbers = array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9');
$possibles = array_merge(range('a', 'z'), $numbers, $specials);
foreach ($str_array as $item) {
if (in_array($item, $possibles, true) === false) {
$this->log->e("MXID localpart: invalid character: {$item}");
return false;
}
}
if (mb_strlen($_POST['login']) > $max_length) {
$this->log->e("MXID localpart: too long");
return false;
}
$this->log->d("MXID localpart is valid");
return true;
}
/**
* @param string $nick
* @return bool
*/
private function userExistsInRequestsOrUsers(string $nick): bool
{
return $this->db->UserExistsInRequests($nick) || $this->db->UserExistsInUsers($nick);
}
/**
* @param string $login
* @return bool
*/
private function checkUser(string $login): bool {
/**
* Prüft, ob der gewünschte Nutzernamen nicht bereits vergeben ist.
* Dazu wird in den Datenbanktabellen users (bereits registrierte
* benutzer) und requests (bereits beantragte benutzer) nach einer
* eventuellen Übereinstimmung geschaut. Eine spezielle Behandlung
* des Suchstrings ist nach meiner Meinung hier nicht nötig, da der
* String vorher auf Matrix-konforme Zeichen getestet wurde und die
* Datenbankfuntionen prepared Statements verwendet.
*/
$this->log->d("Checking if username is available");
return ! $this->userExistsInRequestsOrUsers($login);
}
private function checkRequests(string $ip): 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($ip);
$now = getNow();
// Wenn es der erste Request ist -> return true
$count = count($timestamps);
if ($count === 0) {
$this->log->d("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 5 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 bei mehr
// als 3 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(string $ip): 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 {
return $this->db->saveRequest($this->token, $ip);
} catch (Exception $e) {
$this->log->e("Error: Database returns: {$e->getMessage()}");
}
return false;
}
private function sendVerificationMail(): bool {
/**
* Verschickt die Mail mit dem Verifizierungslink.
* TODO: Reicht filter_input()? Was kann hier passieren? Beim Check
* der Mailadresse verwenden wir FILTER_VALIDATE_EMAIL. SANITZE
* könnte eine Mail an eine andere Adresse schicken, als dann in der
* Datenbankn gespeichert wäre. Besser hier auch VALIDATE benutzen?
* TODO: Wie sieht das mit dem Risiko aus, daß jemand über die
* Konfigurationsdatei Code einschleust? Was kann man dagegen tun?
*/
$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->n("Validationmail successfull send to {$mailTo}");
return true;
}
$this->log->e("Sending validation mail failed");
return false;
}
}