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