From d5c18c2f2ecbd472c806c5b65b461110d3cfe733 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Mar 2018 13:57:15 +0100 Subject: [PATCH] First running version (#2) This currently depends of interacting with matrix_synapse_rest_auth[1] and mxisd[2]. How to integrate that is explained in the README [1] https://github.com/kamax-io/matrix-synapse-rest-auth [2] https://github.com/kamax-io/mxisd --- .gitignore | 2 + MatrixConnection.php | 155 ++++++++++++++++ README.md | 25 ++- config.sample.php | 26 +++ cron.php | 92 +++++++++ database.php | 339 ++++++++++++++++++++++++++++++++++ internal/directory_search.php | 36 ++++ internal/identity_bulk.php | 53 ++++++ internal/identity_single.php | 46 +++++ internal/login.php | 98 ++++++++++ lang/lang.de-de.php | 27 +++ language.php | 13 ++ mail_templates.php | 108 +++++++++++ public/index.php | 218 ++++++++++++++++++++++ public/verify.php | 77 ++++++++ public/verify_admin.php | 168 +++++++++++++++++ 16 files changed, 1481 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 MatrixConnection.php create mode 100644 config.sample.php create mode 100644 cron.php create mode 100644 database.php create mode 100644 internal/directory_search.php create mode 100644 internal/identity_bulk.php create mode 100644 internal/identity_single.php create mode 100644 internal/login.php create mode 100644 lang/lang.de-de.php create mode 100644 language.php create mode 100644 mail_templates.php create mode 100644 public/index.php create mode 100644 public/verify.php create mode 100644 public/verify_admin.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ec6a661 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +config.php +db_file.sqlite \ No newline at end of file diff --git a/MatrixConnection.php b/MatrixConnection.php new file mode 100644 index 0000000..aa63a3c --- /dev/null +++ b/MatrixConnection.php @@ -0,0 +1,155 @@ +hs = $homeserver; + $this->at = $access_token; + } + + function send($room_id, $message) { + if (!$this->at) { + error_log("No access token defined"); + return false; + } + + $send_message = NULL; + if (!$message) { + error_log("no message to send"); + return false; + } elseif(is_array($message)) { + $send_message = $message; + } elseif ($message instanceof MatrixMessage) { + $send_message = $message->get_object(); + } else { + error_log("message is of not valid type\n"); + return false; + } + + $url="https://".$this->hs."/_matrix/client/r0/rooms/" + . urlencode($room_id) ."/send/m.room.message?access_token=".$this->at; + $handle = curl_init($url); + curl_setopt($handle, CURLOPT_RETURNTRANSFER, true); + curl_setopt($handle, CURLOPT_CONNECTTIMEOUT, 5); + curl_setopt($handle, CURLOPT_TIMEOUT, 60); + curl_setopt($handle, CURLOPT_POSTFIELDS, json_encode($send_message)); + curl_setopt($handle, CURLOPT_HTTPHEADER, array("Content-Type: application/json")); + + $response = $this->exec_curl_request($handle); + return isset($response["event_id"]); + } + + function send_msg($room_id, $message) { + return $this->send($room_id, array( + "msgtype" => "m.notice", + "body" => $message + ) + ); + } + + function hasUser($username) { + if (!$username) { + throw new Exception ("no user given to lookup"); + } + + $url = "https://".$this->hs."/_matrix/client/r0/profile/@" . $username . ":" . $this->hs; + $handle = curl_init($url); + curl_setopt($handle, CURLOPT_RETURNTRANSFER, true); + curl_setopt($handle, CURLOPT_CONNECTTIMEOUT, 5); + curl_setopt($handle, CURLOPT_TIMEOUT, 60); + curl_setopt($handle, CURLOPT_HTTPHEADER, array("Content-Type: application/json")); + + $res = $this->exec_curl_request($handle); + return !(isset($res["errcode"]) && $res["errcode"] == "M_UNKNOWN"); + } + + function register($username, $password, $shared_secret) { + if (!$username) { + error_log("no username provided"); + } + if (!$password) { + error_log("no message to send"); + } + + $mac = hash_hmac('sha1', $username, $shared_secret); + + $data = array( + "username" => $username, + "password" => $password, + "mac" => $mac, + ); + $url = "https://".$this->hs."/_matrix/client/v2_alpha/register"; + $handle = curl_init($url); + curl_setopt($handle, CURLOPT_RETURNTRANSFER, true); + curl_setopt($handle, CURLOPT_CONNECTTIMEOUT, 5); + curl_setopt($handle, CURLOPT_TIMEOUT, 60); + curl_setopt($handle, CURLOPT_HTTPHEADER, array("Content-Type: application/json")); + curl_setopt($handle, CURLOPT_POSTFIELDS, json_encode($data)); + + return $this->exec_curl_request($handle); + } + + function exec_curl_request($handle) { + $response = curl_exec($handle); + + if ($response === false) { + $errno = curl_errno($handle); + $error = curl_error($handle); + error_log("Curl returned error $errno: $error\n"); + curl_close($handle); + return false; + } + + $http_code = intval(curl_getinfo($handle, CURLINFO_HTTP_CODE)); + curl_close($handle); + + if ($http_code >= 500) { + // do not want to DDOS server if something goes wrong + sleep(10); + return false; + } else if ($http_code != 200) { + $response = json_decode($response, true); + error_log("Request has failed with error {$response['error']}\n"); + if ($http_code == 401) { + throw new Exception('Invalid access token provided'); + } + } else { + $response = json_decode($response, true); + } + + return $response; + } +} + +class MatrixMessage +{ + private $message; + + function __construct() { + $this->message = ["msgtype" => "m.notice"]; + } + + function set_type($msgtype) { + $this->message["msgtype"] = $msgtype; + } + + function set_format($format) { + $this->message["format"] = $format; + } + + function set_body($body) { + $this->message["body"] = $body; + } + + function set_formatted_body($fbody, $format="org.matrix.custom.html") { + $this->message["formatted_body"] = $fbody; + $this->message["format"] = $format; + } + + function get_object() { + return $this->message; + } +} +?> diff --git a/README.md b/README.md index 3ffa19c..7af7bb2 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,27 @@ This bot provides a two-step-registration for matrix. This is done in several steps: - potential new user registers on a bot-provided side -- bot sends a message to prefined room with a registration notification. +- bot sends a message to predefined room with a registration notification. - users in that room now can approve or decline the registration. -- The bot then uses the registration token to register the user or just drops the registration request. \ No newline at end of file +- When approved + - the bot creates credentials + - sends them to the user + - stores them encrypted in own database + - provides that credentials to [matrix-synapse-rest-auth](https://github.com/kamax-io/matrix-synapse-rest-auth#integrate) which has to be configured to query login.php + +2nd step: Implement the other apis to integrade [mxisd](https://github.com/kamax-io/mxisd/blob/master/docs/backends/rest.md) + +## How to install + +- Copy `config.sample.php` to `config.php` and configure the bot as you can find there +- Configure your webserver to publish the folder `public` and configure. + The folder `internal` contains files that can be accessed by mxisd or matrix-synapse-rest-auth +- To integrate with matrix-synapse-rest-auth: + - `/_matrix-internal/identity/v1/check_credentials` should map to `internal/login.php` +- To integrate with mxisd: Have a look at [the docs](https://github.com/kamax-io/mxisd/blob/master/docs/backends/rest.md) and apply as follows: +| Key | file which handles that | Description | +|--------------------------------|-------------------------------|------------------------------------------------------| +| rest.endpoints.auth | internal/login.php | Validate credentials and get user profile | +| rest.endpoints.directory | internal/directory_search.php | Search for users by arbitrary input | +| rest.endpoints.identity.single | internal/identity_single.php | Endpoint to query a single 3PID | +| rest.endpoints.identity.bulk | internal/identity_bulk.php | Endpoint to query a list of 3PID | diff --git a/config.sample.php b/config.sample.php new file mode 100644 index 0000000..6028e67 --- /dev/null +++ b/config.sample.php @@ -0,0 +1,26 @@ + "example.com", + "access_token" => "To be used for sending the registration notification", + + // Which e-mail-adresse shall the bot use to send e-mails? + "register_email" => 'register_bot@example.com', + // Where should the bot post registration requests to? + "register_room" => '$registerRoomID:example.com', + + // Where is the public part of the bot located? make sure you have a / at the end + "webroot" => "https://myregisterdomain.net/", + + // optional: Do you have a place where howTo's are located? If not leave this value out + "howToURL" => "https://my-url-for-storing-howTos.net", + + // When you want to collect the password on registration set this to true + "getPasswordOnRegistration" => false, + + // to define where the data should be stored: + "databaseURI" => "sqlite:" . dirname(__FILE__) . "/db_file.sqlite", + // credentials for sqlite not used + "databaseUser" => "dbUser123", + "databasePass" => "secretPassword", +] +?> diff --git a/cron.php b/cron.php new file mode 100644 index 0000000..473a3cd --- /dev/null +++ b/cron.php @@ -0,0 +1,92 @@ +query($sql) as $row) { + $first_name = $row["first_name"]; + $last_name = $row["last_name"]; + $username = $row["username"]; + $email = $row["email"]; + $state = $row["state"]; + + try { + switch ($state) { + case RegisterState::PendingEmailSend: + $verify_url = $config["webroot"] . "/verify.php?t=" . $row["verify_token"]; + $success = send_mail_pending_verification( + $config["homeserver"], + $row["first_name"] . " " . $row["last_name"], + $row["email"], + $verify_url); + + if ($success) { + $mx_db->setRegistrationStateById(RegisterState::PendingEmailVerify, $row["id"]); + } else { + throw new Exception("Could not send mail to ".$row["first_name"]." ".$row["last_name"]."(".$row["id"].")"); + } + break; + case RegisterState::PendingAdminSend: + require_once("MatrixConnection.php"); + $adminUrl = $config["webroot"] . "/verify_admin.php?t=" . $row["admin_token"]; + $mxConn = new MatrixConnection($config["homeserver"], $config["access_token"]); + $mxMsg = new MatrixMessage(); + $mxMsg->set_body($first_name . ' ' . $last_name . " möchte sich registrieren und hat folgende Notiz hinterlassen:\r\n" + . $row["note"] . "\r\n" + . "Zum Bearbeiten hier klicken:\r\n" . $adminUrl); + $mxMsg->set_formatted_body($first_name . ' ' . $last_name . " möchte sich registrieren und hat folgende Notiz hinterlassen:
" + . $row["note"] . "
" + . "Zum Bearbeiten hier klicken"); + $mxMsg->set_type("m.text"); + $response = $mxConn->send($config["register_room"], $mxMsg); + + if ($response) { + $mx_db->setRegistrationStateById(RegisterState::PendingAdminVerify, $row["id"]); + + send_mail_pending_approval($config["homeserver"], $first_name . " " . $last_name, $email); + } else { + throw new Exception("Could not send notification for ".$row["first_name"]." ".$row["last_name"]."(".$row["id"].") to admins."); + } + break; + case RegisterState::PendingRegistration: + // Registration got accepted but registration failed + + $password = $mx_db->addUser($row["first_name"], $row["last_name"], $row["username"], $row["email"]); + if ($password != NULL) { + // send registration_success + $res = send_mail_registration_success($config["homeserver"], $first_name . " " . $last_name, $email, $username, $password, $config["howToURL"]); + if ($res) { + $mx_db->setRegistrationStateById(RegisterState::AllDone, $row["id"]); + } else { + $mx_db->setRegistrationStateById(RegisterState::PendingSendRegistrationMail, $row["id"]); + } + } else { + send_mail_registration_allowed_but_failed($config["homeserver"], $first_name . " " . $last_name, $email); + $mxMsg = new MatrixMessage(); + $mxMsg->set_type("m.text"); + $mxMsg->set_body("Fehler beim Registrieren von " . $first_name . " " . $last_name . "."); + $mxConn->send($config["register_room"], $mxMsg); + throw new Exception($language["REGISTRATION_FAILED"]); + } + break; + case RegisterState::PendingSendRegistrationMail: + print ("Error: Unhandled state: PendingSendRegistrationMail for " . $first_name . " " . $last_name . " (" . $username . ")\n"); + break; + case RegisterState::RegistrationDeclined: + case RegisterState::AllDone: + // do reqular cleanup + break; + } + } catch (Exception $e) { + print("Error while handling cron for " . $first_name . " " . $last_name . " (" . $username . ")\n"); + print($e->getMessage()); + } +} +?> diff --git a/database.php b/database.php new file mode 100644 index 0000000..d33d6cf --- /dev/null +++ b/database.php @@ -0,0 +1,339 @@ +db = new PDO($db_input, $user, $password); + $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $this->db->exec("CREATE TABLE IF NOT EXISTS registrations( + id SERIAL PRIMARY KEY, + state INT DEFAULT 0, + first_name TEXT, + last_name TEXT, + username TEXT, + password_hash TEXT DEFAULT '', + note TEXT, + email TEXT, + verify_token TEXT, + admin_token TEXT, + request_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )"); + $this->db->exec("CREATE TABLE IF NOT EXISTS logins ( + id SERIAL PRIMARY KEY, + active INT DEFAULT 1, + first_name TEXT, + last_name TEXT, + localpart TEXT, + password_hash TEXT, + email TEXT, + create_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_modified TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )"); + // make sure the bot is allowed to login + if (!$this->userRegistered("register_bot")) { + $password = $this->addUser("Register", "Bot", "register_bot", $config["register_email"]); + $config["register_password"] = $password; + $myfile = fopen(dirname(__FILE__) . "/config.json", "w"); + fwrite($myfile, json_encode($config, JSON_PRETTY_PRINT)); + fclose($myfile); + } + + // set writeable when not set already + if (strpos($db_input, "sqlite") === 0) { + $sqlite_file = substr($db_input, strlen("sqlite:")); + if (!is_writable($sqlite_file)) { + chmod($sqlite_file, 0660); + } + unset($sqlite_file); + } + } + + /** + * WARNING: This allows accessing the database directly. + * This was only be added for convenience. You are advised to not use this function extensively + * + * @param sql String wich will be passed directly to the database + * @return Response of PDO::query() + */ + function query($sql) { + return $this->db->query($sql); + } + + function setRegistrationStateVerify($state, $token) { + $sql = "UPDATE registrations SET state = " . $state + . " WHERE verify_token = '" . $token . "';"; + + return $this->db->exec($sql); + } + + function setRegistrationStateById($state, $id) { + $sql = "UPDATE registrations SET state = " . $state + . " WHERE id = '" . $id . "';"; + + return $this->db->exec($sql); + } + + function setRegistrationStateAdmin($state, $token) { + $sql = "UPDATE registrations SET state = " . $state + . " WHERE admin_token = '" . $token . "';"; + + return $this->db->exec($sql); + } + + function setRegistrationState($state, $token) { + $sql = "UPDATE registrations SET state = " . $state + . " WHERE verify_token = '" . $token . "' OR admin_token = '" . $token . "';"; + + return $this->db->exec($sql); + } + + function userPendingRegistrations($username) { + $sql = "SELECT COUNT(*) FROM registrations WHERE username = '" . $username . "' AND NOT state = " + . RegisterState::RegistrationDeclined . " LIMIT 1;"; + $res = $this->db->query($sql); + if ($res->fetchColumn() > 0) { + return true; + } + return false; + } + function userRegistered($username) { + $sql = "SELECT COUNT(*) FROM logins WHERE localpart = '" . $username . "' LIMIT 1;"; + $res = $this->db->query($sql); + if ($res->fetchColumn() > 0) { + return true; + } + return false; + } + + /** + * Adds user to the database. Next steps should be sending a verify-mail to the user + * @param first_name First name of the user + * @param last_name Sirname of the user + * @param username the future localpart of that user + * @param note Note the user typed in to give a hint + * @param email E-Mail-Adress which will be stored into the database. + * This will be send to the server on first login + * + * @return ["verify_token"] + */ + function addRegistration($first_name, $last_name, $username, $note, $email) { + if ($this->userPendingRegistrations($username)) { + require_once("language.php"); + throw new Exception($language["USERNAME_PENDING_REGISTRATION"]." (requested)"); + } + if ($this->userRegistered($username)) { + require_once("language.php"); + throw new Exception($language["USERNAME_REGISTERED"] . " (registered)"); + } + + $verify_token = bin2hex(random_bytes(16)); + $admin_token = bin2hex(random_bytes(16)); + + $this->db->exec("INSERT INTO registrations + (first_name, last_name, username, note, email, verify_token, admin_token) + VALUES ('" . $first_name."','" . $last_name . "','" . $username . "','" . $note . "','" + . $email."','" .$verify_token."','" .$admin_token."')"); + + return [ + "verify_token"=> $verify_token, + ]; + } + + /** + * Gets the user for the verify_admin page. + * + * @return ArrayOfUser|NULL Array with "first_name, last_name, username, note and email" + * as members + */ + function getUserForApproval($admin_token) { + $sql = "SELECT COUNT(*) FROM registrations WHERE admin_token = '" . $admin_token . "'" + . " AND state = " . RegisterState::PendingAdminVerify . " LIMIT 1;"; + $res = $this->db->query($sql); + $first_name = NULL; $last_name = NULL; $username = NULL; $note = NULL; $email = NULL; + + if ($res->fetchColumn() > 0) { + $sql = "SELECT first_name, last_name, username, note, email FROM registrations" + . " WHERE admin_token = '" . $admin_token . "'" + . " AND state = " . RegisterState::PendingAdminVerify + . " LIMIT 1;"; + foreach ($this->db->query($sql) as $row) { + // will only be executed once + return $row; + } + } + return NULL; + } + + /** + * Gets the user when it opens the page to verify its mail + * + * @return ArrayOfUser|NULL Array with "first_name, last_name, note, email and admin_token" + * as members + */ + function getUserForVerify($verify_token) { + $sql = "SELECT COUNT(*) FROM registrations WHERE verify_token = '" . $verify_token . "'" + . " AND state = " . RegisterState::PendingEmailVerify . " LIMIT 1;"; + $res = $this->db->query($sql); + $first_name = NULL; $last_name = NULL; $username = NULL; $note = NULL; $email = NULL; + + if ($res->fetchColumn() > 0) { + $sql = "SELECT first_name, last_name, note, email, admin_token FROM registrations " + . " WHERE verify_token = '" . $verify_token . "'" + . " AND state = " . RegisterState::PendingEmailVerify . " LIMIT 1;"; + foreach ($this->db->query($sql) as $row) { + // will only be executed once + return $row; + } + } + return NULL; + } + + function getUserForLogin($localpart, $password) { + $sql = "SELECT COUNT(*) FROM logins WHERE localpart = '" . $localpart + . "' AND active = 1 LIMIT 1;"; + $res = $this->db->query($sql); + + if ($res->fetchColumn() > 0) { + $sql = "SELECT first_name, last_name, email, password_hash FROM logins " + . " WHERE localpart = '" . $localpart . "' AND active = 1 LIMIT 1;"; + foreach ($this->db->query($sql) as $row) { + if (password_verify($password, $row["password_hash"])) { + return $row; + } + } + } + return NULL; + } + + /** + * adds User to be able to login afterwards. + * @param first_name First name of the user + * @param last_name Sirname of the user + * @param username the future localpart of that user + * @param email E-Mail-Adress which will be stored into the database. + * This will be send to the server on first login + * + * @return password|NULL with member password as this method generates a + * password and saves that into the database + * NULL when failed + * + */ + function addUser($first_name, $last_name, $username, $email) { + // check if user already exists and abort in that case + if ($this->userRegistered($username)) { + return NULL; + } + + // generate a password with 10 characters + $password = bin2hex(openssl_random_pseudo_bytes(5)); + $password_hash = password_hash($password, PASSWORD_BCRYPT, ["cost"=>12]); + + $sql = "INSERT INTO logins (first_name, last_name, localpart, password_hash, email) VALUES " + . "('" . $first_name."','" . $last_name . "','" . $username . "','" + . $password_hash . "','" . $email . "');"; + + if ($this->db->exec($sql)) { + return $password; + } + return NULL; + } + + function searchUserByName($search_term) { + $term = filter_var($search_term, FILTER_SANITIZE_STRING); + $result = array(); + $sql = "SELECT COUNT(*) FROM logins WHERE" + . " localpart LIKE '" . $term . "%' AND active = 1;"; + $res = $this->db->query($sql); + + if ($res->fetchColumn() > 0) { + $sql = "SELECT first_name, last_name, localpart FROM logins WHERE" + . " localpart LIKE '" . $term . "%' AND active = 1;"; + foreach ($this->db->query($sql) as $row) { + array_push($result, [ + "display_name" => $row["first_name"] . " " . $row["last_name"], + "user_id" => $row["localpart"], + ]); + } + } + return $result; + } + + function searchUserByEmail($search_term) { + $term = filter_var($search_term, FILTER_SANITIZE_STRING); + $result = array(); + $sql = "SELECT COUNT(*) FROM logins WHERE" + . " email = '" . $term . "' AND active = 1;"; + $res = $this->db->query($sql); + + if ($res->fetchColumn() > 0) { + $sql = "SELECT first_name, last_name, localpart FROM logins WHERE" + . " email = '" . $term . "' AND active = 1;"; + foreach ($this->db->query($sql) as $row) { + array_push($result, [ + "display_name" => $row["first_name"] . " " . $row["last_name"], + "user_id" => $row["localpart"], + ]); + } + } + return $result; + } +} + +if (!isset($mx_db)) { + $mx_db = new mxDatabase($config); +} +?> diff --git a/internal/directory_search.php b/internal/directory_search.php new file mode 100644 index 0000000..fe2b5e1 --- /dev/null +++ b/internal/directory_search.php @@ -0,0 +1,36 @@ + false, + "result" => [], +]; + +try { + $inputJSON = file_get_contents('php://input'); + $input = json_decode($inputJSON, TRUE); + if (empty($input)) { + throw new Exception('no valid json as input present'); + } + if (!isset($input["by"])) { + throw new Exception('"by" is not defined'); + } + if (!isset($input["search_term"])) { + throw new Exception('"search_term" is not defined'); + } + switch ($input["by"]) { + case "name": + $response["result"] = $mx_db->searchUserByName($input["search_term"]); + break; + case "threepid": + $response["result"] = $mx_db->searchUserByEmail($input["search_term"]); + break; + default: + throw new Exception("unknown type for \"by\" param"); + } + +} catch (Exception $e) { + error_log("failed with error: " . $e->getMessage()); + $response["error"] = $e->getMessage(); +} +print (json_encode($response, JSON_PRETTY_PRINT) . "\n"); +?> diff --git a/internal/identity_bulk.php b/internal/identity_bulk.php new file mode 100644 index 0000000..3768a70 --- /dev/null +++ b/internal/identity_bulk.php @@ -0,0 +1,53 @@ + [] +]; +try { + $inputJSON = file_get_contents('php://input'); + $input = json_decode($inputJSON, TRUE); + if (!isset($input)) { + throw new Exception('request body is no valid json'); + } + + if (!isset($input["lookup"])) { + throw new Exception('"lookup" is not defined'); + } + if (!is_array($input["lookup"])) { + throw new Exception('"lookup" is not an array'); + } + foreach ($input["lookup"] as $lookup) { + if (!isset($lookup["medium"])) { + throw new Exception('"lookup.medium" is not defined'); + } + if (!isset($lookup["address"])) { + throw new Exception('"lookup.address" is not defined'); + } + $res2 = array(); + switch ($lookup["medium"]) { + case "email": + $res2 = $mx_db->searchUserByEmail($lookup["address"]); + if (!empty($res2)) { + array_push($response["lookup"], [ + "medium" => $lookup["medium"], + "address" => $lookup["address"], + "id" => [ + "type" => "localpart", + "value" => $res2[0]["user_id"], + ] + ] + ); + } + break; + case "msisdn": + break; + default: + throw new Exception("unknown type for \"by\" param"); + } + } +} catch (Exception $e) { + error_log("ídentity_bulk failed with error: " . $e->getMessage()); + $response["error"] = $e->getMessage(); +} +print (json_encode($response, JSON_PRETTY_PRINT) . "\n"); +?> diff --git a/internal/identity_single.php b/internal/identity_single.php new file mode 100644 index 0000000..404f019 --- /dev/null +++ b/internal/identity_single.php @@ -0,0 +1,46 @@ +searchUserByEmail($input["lookup"]["address"]); + if (!empty($res2)) { + $response = [ + "lookup" => [ + "medium" => $input["lookup"]["medium"], + "address" => $input["lookup"]["address"], + "id" => [ + "type" => "localpart", + "value" => $res2[0]["user_id"], + ] + ] + ]; + } + + + break; + default: + throw new Exception("unknown type for \"by\" param"); + } +} catch (Exception $e) { + error_log("ídentity_bulk failed with error: " . $e->getMessage()); + $response["error"] = $e->getMessage(); +} +print (json_encode($response, JSON_PRETTY_PRINT) . "\n"); +?> diff --git a/internal/login.php b/internal/login.php new file mode 100644 index 0000000..e63bd08 --- /dev/null +++ b/internal/login.php @@ -0,0 +1,98 @@ + [ + "success" => false, + ] +]; + +require_once("../database.php"); +abstract class LoginRequester { + const UNDEFINED = 0; + const MXISD = 1; + const RestAuth = 2; +} +$loginRequester = LoginRequester::UNDEFINED; + +try { + $inputJSON = file_get_contents('php://input'); + $input = json_decode($inputJSON, TRUE); + $mxid = NULL; + $localpart = NULL; + if (isset($input["user"])) { + if (isset($input["user"]["localpart"])) { + $localpart = $input["user"]["localpart"]; + $loginRequester = LoginRequester::MXISD; + } elseif (isset($input["user"]["id"])) { + // compatibility for matrix-synapse-rest-auth + $mxid = $input["user"]["id"]; + $loginRequester = LoginRequester::RestAuth; + } elseif (isset($input["user"]["mxid"])) { + // compatibility for mxisd + $mxid = $input["user"]["mxid"]; + $loginRequester = LoginRequester::MXISD; + } + } + + // prefer the localpart attribute of mxisd. But in case of matrix-synapse-rest-auth + // we have to parse it on our own + if (empty($localpart) && !empty($mxid)) { + // A mxid would start with an @ so we start at the 2. position + $sepPos = strpos($mxid,':', 1); + if ($sepPos === false) { + // : not found. Assume mxid is localpart + // TODO: further checks + $localpart = $mxid; + } else { + $localpart = substr($mxid, 1, strpos($mxid,':') - 1 ); + } + } + + if (empty($localpart)) { + throw new Exception ("localpart cannot be identified"); + } + + $password = NULL; + if (isset($input["user"]) && isset($input["user"]["password"])) { + $password = $input["user"]["password"]; + } + if (empty($password)) { + throw new Exception ("password is not present"); + } + + $user = $mx_db->getUserForLogin($localpart, $password); + if (!$user) { + throw new Exception("user not found or password did not match"); + } + $response["auth"]["success"] = true; + $response["auth"]["profile"] = [ + "display_name" => $user["first_name"] . " " . $user["last_name"], + "three_pids" => [ + [ + "medium" => "email", + "address" => $user["email"], + ], + ], + ]; + + switch ($loginRequester) { + case LoginRequester::RestAuth: + $response["auth"]["mxid"] = $mxid; + break; + case LoginRequester::MXISD; + $response["auth"]["id"] = [ + "type" => "localpart", + "value" => $localpart, + ]; + break; + default: + // only return that it was successful. + // we do not know how the data shall be transmitted so we do nothing with it + $response["auth"]["success"] = false; + break; + } +} catch (Exception $e) { + error_log("Auth failed with error: " . $e->getMessage()); + $response["auth"]["error"] = $e->getMessage(); +} +print (json_encode($response, JSON_PRETTY_PRINT) . "\n"); +?> diff --git a/lang/lang.de-de.php b/lang/lang.de-de.php new file mode 100644 index 0000000..84dafb0 --- /dev/null +++ b/lang/lang.de-de.php @@ -0,0 +1,27 @@ + "Es konnte keine Konfiguration gefunden werden.", + "UNKNOWN_SESSION" => "Sitzungstoken nicht vorhanden oder ungültig.", + "UNKNOWN_USERNAME" => "Nutzername fehlt", + "UNKNOWN_TOKEN" => "Token ist unbekannt", + "USERNAME_LENGTH_INVALID" => "Entweder mehr als 20 oder weniger als 3 Zeichen für den Nutzernamen verwendet", + "USERNAME_NOT_ALNUM" => "Nutzername ist nicht alphanumerisch", + "USERNAME_PENDING_REGISTRATION" => "Dieser Nutzername wurde bereits zur Registrierung vorgemerkt. Versuche es später noch einmal oder wähle einen anderen Nutzernamen", + "USERNAME_REGISTERED" => "Dieser Nutzername wurde bereits registriert. Bitte wähle einen anderen Nutzernamen", + "PASSWORD_NOT_MATCH" => "Passwörter stimmen nicht überein", + "NOTE_LENGTH_EXEEDED" => "Notiz ist länger als die erlaubten 50 Zeichen", + "EMAIL_INVALID_FORMAT" => "Keine valide E-Mail-Adresse angegeben", + "FIRSTNAME_INVALID_FORMAT" => "Vorname hat ungültiges Format", + "SIRNAME_INVALID_FORMAT" => "Nachname hat ungültiges Format", + "SEND_MAIL_FAIL" => "Senden der E-Mail fehlgeschlagen", + "SEND_MATRIX_FAIL" => "Senden einer Nachricht an die Administratoren fehlgeschlagen", + "REGISTRATION_REQUEST_FAILED" => "Registrierungsanfrage ist fehlgeschlagen", + "REGISTRATION_FAILED" => "Registrierung ist fehlgeschlagen", + "VERIFICATION_SUCEEDED" => "Verifizierung erfolgreich", + "VERIFICATION_FAILED" => "Verifizierung fehlgeschlagen", + "VERIFICATION_SUCCESS_BODY" => "Vielen Dank. Die Administratoren wurden informiert", + "ADMIN_VERIFY_SITE_TITLE" => "Registrierungsanfrage bearbeiten", + "ADMIN_REGISTER_ACCEPTED_BODY" => "Die Registrierungsanfrage wurde akzeptiert. Der Nutzer wurde per Mail informiert.", + "ADMIN_REGISTER_DECLINED_BODY" => "Die Registrierungsanfrage wurde angelehnt. Der Nutzer wurde per Mail informiert.", +); +?> diff --git a/language.php b/language.php new file mode 100644 index 0000000..5a1c960 --- /dev/null +++ b/language.php @@ -0,0 +1,13 @@ + diff --git a/mail_templates.php b/mail_templates.php new file mode 100644 index 0000000..c6a3b22 --- /dev/null +++ b/mail_templates.php @@ -0,0 +1,108 @@ + diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..4c1f56c --- /dev/null +++ b/public/index.php @@ -0,0 +1,218 @@ + + + 20 || strlen($_POST["username"]) < 3)) { + throw new Exception($language["USERNAME_LENGTH_INVALID"]); + } + if (ctype_alnum($_POST['username']) != true) { + throw new Exception($language["USERNAME_NOT_ALNUM"]); + } + if (isset($config["getPasswordOnRegistration"]) && $config["getPasswordOnRegistration"] && + $_POST["password"] != $_POST["password_confirm"]) { + throw new Exception($language["PASSWORD_NOT_MATCH"]); + } + if (isset($_POST["note"]) && strlen($_POST["note"]) > 50) { + throw new Exception($language["NOTE_LENGTH_EXEEDED"]); + } + if (!isset($_POST["email"]) || !filter_var($_POST["email"], FILTER_VALIDATE_EMAIL)) { + throw new Exception($language["EMAIL_INVALID_FORMAT"]); + } + if (isset($_POST["first_name"]) && ! preg_match("/[A-Z][a-z]+/", $_POST["first_name"])) { + throw new Exception($language["FIRSTNAME_INVALID_FORMAT"]); + } + if (isset($_POST["last_name"]) && ! preg_match("/[A-Z][a-z]+/", $_POST["last_name"])) { + throw new Exception($language["SIRNAME_INVALID_FORMAT"]); + } + + $first_name = filter_var($_POST["first_name"], FILTER_SANITIZE_STRING); + $last_name = filter_var($_POST["last_name"], FILTER_SANITIZE_STRING); + $username = filter_var($_POST["username"], FILTER_SANITIZE_STRING); + if (isset($_POST["password"])) { + $password = filter_var($_POST["password"], FILTER_SANITIZE_STRING); + } + $note = filter_var($_POST["note"], FILTER_SANITIZE_STRING); + $email = filter_var($_POST["email"], FILTER_VALIDATE_EMAIL); + + require_once("../database.php"); + $res = $mx_db->addRegistration($first_name, $last_name, $username, $note, $email); + + if (!isset($res["verify_token"])) { + error_log("sth. went wrong. registration did not throw but admin_token not set"); + throw Exception ("Unknown Error"); + } + $verify_token = $res["verify_token"]; + + $verify_url = $config["webroot"] . "/verify.php?t=" . $verify_token; + require_once "../mail_templates.php"; + $success = send_mail_pending_verification( + $config["homeserver"], + $first_name . " " . $last_name, + $email, + $verify_url); + + $mx_db->setRegistrationStateVerify( + ($success ? RegisterState::PendingEmailVerify : RegisterState::PendingEmailSend), + $verify_token); + + print("Erfolgreich"); + print(""); + print("

Erfolgreich

"); + print("

Bitte überprüfe deine E-Mails um deine E-Mail-Adresse zu bestätigen.

"); + print("Zur Registrierungsseite"); + } catch (Exception $e) { + print("" . $language["REGISTRATION_REQUEST_FAILED"] . ""); + print(""); + print("

" . $language["REGISTRATION_REQUEST_FAILED"] . "

"); + print("

" . $e->getMessage() . "

"); + print("Zur Registrierungsseite"); + } +} else { + $_SESSION["token"] = bin2hex(random_bytes(16)); +?> + Registriere dich für <?php echo $config["homeserver"]; ?> + + + + + + +
+
+
+
+
+

Bitte für registrieren2-Schritt-Registrierung

+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ +
+
+
+
+ +
+
+
+ + "> + + +
+

Hinweis:
+ ist ein geschlossenes Chat-Netzwerk in dem jeder Nutzer bestätigt werden muss.
+ Du bekommst eine E-Mail wenn jemand deine Mitgliedschaft bestätigt hat. An diese wird auch dein initiales Passwort gesendet. + Hinterlasse also bitte einen Hinweis zu dir (der nur den entsprechenden Personen gezeigt wird).
+ Liebe Grüße vom Team von +

+
+
+
+
+
+ + + + diff --git a/public/verify.php b/public/verify.php new file mode 100644 index 0000000..38ac223 --- /dev/null +++ b/public/verify.php @@ -0,0 +1,77 @@ + + +getUserForVerify($token); + if ($user == NULL) { + throw new Exception($language["UNKNOWN_TOKEN"]); + } + $first_name = $user["first_name"]; + $last_name = $user["last_name"]; + $note = $user["note"]; + $email = $user["email"]; + $admin_token = $user["admin_token"]; + + require_once("../MatrixConnection.php"); + $adminUrl = $config["webroot"] . "/verify_admin.php?t=" . $admin_token; + $mxConn = new MatrixConnection($config["homeserver"], $config["access_token"]); + $mxMsg = new MatrixMessage(); + $mxMsg->set_body($first_name . ' ' . $last_name . "möchte sich registrieren und hat folgende Notiz hinterlassen:\r\n" + . $note . "\r\n" + . "Zum Bearbeiten hier klicken:\r\n" . $adminUrl); + $mxMsg->set_formatted_body($first_name . ' ' . $last_name . " möchte sich registrieren und hat folgende Notiz hinterlassen:
" + . $note . "
" + . "Zum Bearbeiten hier klicken"); + $mxMsg->set_type("m.text"); + $response = $mxConn->send($config["register_room"], $mxMsg); + + if ($response) { + $message = $language["SEND_MATRIX_FAIL"]; + } + $mx_db->setRegistrationStateVerify( + ($response ? RegisterState::PendingAdminVerify : RegisterState::PendingAdminSend), + $token); + + send_mail_pending_approval($config["homeserver"], $first_name . " " . $last_name, $email); + + print("" . $language["VERIFICATION_SUCEEDED"] . ""); + print(""); + print("

" . $language["VERIFICATION_SUCEEDED"] . "

"); + print("

" . $language["VERIFICATION_SUCCESS_BODY"] . "

"); + print("Zur Registrierungsseite"); +} catch (Exception $e) { + print("" . $language["VERIFICATION_FAILED"] . ""); + print(""); + print("

" . $language["VERIFICATION_FAILED"] . "

"); + print("

" . $e->getMessage() . "

"); + print("Zur Registrierungsseite"); +} +?> + + diff --git a/public/verify_admin.php b/public/verify_admin.php new file mode 100644 index 0000000..15aa335 --- /dev/null +++ b/public/verify_admin.php @@ -0,0 +1,168 @@ + + +getUserForApproval($token); + if ($user == NULL) { + throw new Exception($language["UNKNOWN_TOKEN"]); + } + + $first_name = $user["first_name"]; + $last_name = $user["last_name"]; + $username = $user["username"]; + $note = $user["note"]; + $email = $user["email"]; + + if ($action == RegisterState::RegistrationAccepted) { + $mx_db->setRegistrationStateAdmin(RegisterState::PendingRegistration, $token); + + // register user + require_once("../MatrixConnection.php"); + $mxConn = new MatrixConnection($config["homeserver"], $config["access_token"]); + + // generate a password with 8 characters + $password = $mx_db->addUser($first_name, $last_name, $username, $email); + if ($password != NULL) { + // send registration_success + $res = send_mail_registration_success($config["homeserver"], $first_name . " " . $last_name, $email, $username, $password, $config["howToURL"]); + if ($res) { + $mx_db->setRegistrationStateAdmin(RegisterState::AllDone, $token); + } else { + $mx_db->setRegistrationStateAdmin(RegisterState::PendingSendRegistrationMail, $token); + } + } else { + send_mail_registration_allowed_but_failed($config["homeserver"], $first_name . " " . $last_name, $email); + $mxMsg = new MatrixMessage(); + $mxMsg->set_type("m.text"); + $mxMsg->set_body("Fehler beim Registrieren von " . $first_name . " " . $last_name . "."); + $mxConn->send($config["register_room"], $mxMsg); + throw new Exception($language["REGISTRATION_FAILED"]); + } + + print("" . $language["ADMIN_VERIFY_SITE_TITLE"] . ""); + print(""); + print("

" . $language["ADMIN_VERIFY_SITE_TITLE"] . "

"); + print("

" . $language["ADMIN_REGISTER_ACCEPTED_BODY"] . "

"); + } elseif ($action == RegisterState::RegistrationDeclined) { + $mx_db->setRegistrationStateAdmin(RegisterState::RegistrationDeclined, $token); + send_mail_registration_decline($config["homeserver"], $first_name . " " . $last_name, $email, $decline_reason); + print("" . $language["ADMIN_VERIFY_SITE_TITLE"] . ""); + print(""); + print("

" . $language["ADMIN_VERIFY_SITE_TITLE"] . "

"); + print("

" . $language["ADMIN_REGISTER_DECLINED_BODY"] . "

"); + } else { + + print("" . $language["ADMIN_VERIFY_SITE_TITLE"] . ""); + ?> + + + + + + +
+
+
+
+
+

+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+ +
+ +
+ +
+ +
+ + + + +
+
+
+
+
+
+