From c1f5f4d4515f4939d27522ebf31763b64f7de9ce Mon Sep 17 00:00:00 2001 From: Krombel Date: Sat, 10 Feb 2018 18:01:42 +0100 Subject: [PATCH 01/46] first WIP implementation --- .gitignore | 1 + MatrixConnection.php | 77 +++++++++++++++++ config.sample.php | 6 ++ functions.php | 52 +++++++++++ lang.de.php | 13 +++ language.php | 7 ++ register.php | 200 +++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 356 insertions(+) create mode 100644 .gitignore create mode 100644 MatrixConnection.php create mode 100644 config.sample.php create mode 100644 functions.php create mode 100644 lang.de.php create mode 100644 language.php create mode 100644 register.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4f4773f --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +config.php diff --git a/MatrixConnection.php b/MatrixConnection.php new file mode 100644 index 0000000..55c2ee3 --- /dev/null +++ b/MatrixConnection.php @@ -0,0 +1,77 @@ +hs = $homeserver; + $this->at = $access_token; + } + + function send($room_id, $message) { + $send_message = NULL; + if (!$message) { + error_log("no message to send"); + } elseif(is_array($message)) { + $send_message = $message; + } elseif ($message instanceof MatrixMessage) { + $sendmessage = $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($message)); + curl_setopt($handle, CURLOPT_HTTPHEADER, array("Content-Type: application/json")); + + return exec_curl_request($handle); + } + + function send_msg($room_id, $message) { + return $this->send($room_id, array( + "msgtype" => "m.notice", + "body" => $message + ) + ); + } +} + +class MatrixMessage +{ + private $message; + + function __construct() { + $this->message = array( + "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/config.sample.php b/config.sample.php new file mode 100644 index 0000000..e998fdb --- /dev/null +++ b/config.sample.php @@ -0,0 +1,6 @@ + diff --git a/functions.php b/functions.php new file mode 100644 index 0000000..949df06 --- /dev/null +++ b/functions.php @@ -0,0 +1,52 @@ + false, + "message" => $msg + ); + echo json_encode($response); + print("\n"); + exit(); +} + +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'); + } + return false; + } else { + $response = json_decode($response, true); + if (isset($response["event_id"])) { + $response = true; + } else { + $response = false; + } + } + + return $response; + +} +?> diff --git a/lang.de.php b/lang.de.php new file mode 100644 index 0000000..c1f4489 --- /dev/null +++ b/lang.de.php @@ -0,0 +1,13 @@ + "Sitzungstoken nicht vorhanden oder ungültig.", +"UNKNOWN_USER_OR_PASSWORD" => "Nutzername und/oder Passwort(-Wiederholung) fehlen", +"USERNAME_LENGTH_INVALID" => "Entweder mehr als 20 oder weniger als 3 Zeichen für den Nutzernamen verwendet", +"USERNAME_NOT_ALNUM" => "Nutzername ist nicht alphanumerisch", +"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", +); +?> diff --git a/language.php b/language.php new file mode 100644 index 0000000..17c3004 --- /dev/null +++ b/language.php @@ -0,0 +1,7 @@ + diff --git a/register.php b/register.php new file mode 100644 index 0000000..92683dd --- /dev/null +++ b/register.php @@ -0,0 +1,200 @@ + + + 20 || strlen($_POST["username"]) < 3)) { + $message = $language["USERNAME_LENGTH_INVALID"]; + } + elseif (ctype_alnum($_POST['username']) != true) { + $message = $language["USERNAME_NOT_ALNUM"]; + } + elseif ($_POST["password"] != $_POST["password_confirm"]) { + $message = $language["PASSWORD_NOT_MATCH"]; + } + elseif (isset($_POST["note"]) && strlen($_POST["note"]) > 50) { + $message = $language["NOTE_LENGTH_EXEEDED"]; + } + elseif (!isset($_POST["email"]) || !filter_var($_POST["email"], FILTER_VALIDATE_EMAIL)) { + $message = $language["EMAIL_INVALID_FORMAT"]; + } + elseif (isset($_POST["first_name"]) && ! preg_match("/[A-Z][a-z]+/", $_POST["first_name"])) { + $message = $language["FIRSTNAME_INVALID_FORMAT"]; + } + elseif (isset($_POST["last_name"]) && ! preg_match("/[A-Z][a-z]+/", $_POST["last_name"])) { + $message = $language["SIRNAME_INVALID_FORMAT"]; + } + else { + // check valid password + + $first = filter_var($_POST["first_name"], FILTER_SANITIZE_STRING); + $last = filter_var($_POST["last_name"], FILTER_SANITIZE_STRING); + $user = filter_var($_POST["username"], FILTER_SANITIZE_STRING); + $pass = filter_var($_POST["password"], FILTER_SANITIZE_STRING); + $email = filter_var($_POST["email"], FILTER_VALIDATE_EMAIL); + $note = filter_var($_POST["note"], FILTER_SANITIZE_STRING); + + + $success = true; + } + if ($success) { + print("Erfolgreich"); + print(""); + print("

Erfolgreich

"); + print("

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

"); + print("Zur Registrierungsseite"); + } else { + print("".$message.""); + print(""); + print("

" . $message . "

"); + print("Zur Registrierungsseite"); + } +} else { + $_SESSION["token"] = bin2hex(random_bytes(16)); +?> + Registriere dich für cg-s.tk + + + + + + +
+
+
+
+
+

Bitte für registrieren2-Schritt-Registrierung

+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ +
+
+
+
+ +
+
+
+ */ ?> + "> + + + +

Hinweis:
+ cg-s.tk is 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 cg-s.tk +

+
+
+
+
+ + + + -- 2.39.5 From b56798dc35a68f409bd5ea857c089fadf9e526eb Mon Sep 17 00:00:00 2001 From: Krombel Date: Sun, 11 Feb 2018 19:47:35 +0100 Subject: [PATCH 02/46] move language to lang-directory --- lang.de.php => lang/lang.de.php | 0 language.php | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename lang.de.php => lang/lang.de.php (100%) diff --git a/lang.de.php b/lang/lang.de.php similarity index 100% rename from lang.de.php rename to lang/lang.de.php diff --git a/language.php b/language.php index 17c3004..d44568c 100644 --- a/language.php +++ b/language.php @@ -3,5 +3,5 @@ $lang = "en"; if(isset($_GET['lang'])){ $lang = $_GET['lang']; } -require_once("lang.".$lang.".php"); +require_once("lang/lang.".$lang.".php"); ?> -- 2.39.5 From f306dda4f91d32e4c24adc6d022604854e5a7028 Mon Sep 17 00:00:00 2001 From: Krombel Date: Sun, 11 Feb 2018 19:48:38 +0100 Subject: [PATCH 03/46] do not request password on register request the aim is that the initial password is generated and send on register approval --- lang/lang.de.php | 2 +- register.php | 24 ++---------------------- 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/lang/lang.de.php b/lang/lang.de.php index c1f4489..4ba80f9 100644 --- a/lang/lang.de.php +++ b/lang/lang.de.php @@ -1,7 +1,7 @@ "Sitzungstoken nicht vorhanden oder ungültig.", -"UNKNOWN_USER_OR_PASSWORD" => "Nutzername und/oder Passwort(-Wiederholung) fehlen", +"UNKNOWN_USERNAME" => "Nutzername fehlt", "USERNAME_LENGTH_INVALID" => "Entweder mehr als 20 oder weniger als 3 Zeichen für den Nutzernamen verwendet", "USERNAME_NOT_ALNUM" => "Nutzername ist nicht alphanumerisch", "PASSWORD_NOT_MATCH" => "Passwörter stimmen nicht überein", diff --git a/register.php b/register.php index 92683dd..eb177b4 100644 --- a/register.php +++ b/register.php @@ -18,8 +18,8 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") { // token not present or invalid $message = $language["UNKNOWN_SESSION"]; } - elseif (!isset($_POST["username"], $_POST["password"], $_POST["password_confirm"])) { - $message = $language["UNKNOWN_USER_OR_PASSWORD"]; + elseif (!isset($_POST["username"])) { + $message = $language["UNKNOWN_USERNAME"]; } elseif (strlen($_POST["username"] > 20 || strlen($_POST["username"]) < 3)) { $message = $language["USERNAME_LENGTH_INVALID"]; @@ -27,9 +27,6 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") { elseif (ctype_alnum($_POST['username']) != true) { $message = $language["USERNAME_NOT_ALNUM"]; } - elseif ($_POST["password"] != $_POST["password_confirm"]) { - $message = $language["PASSWORD_NOT_MATCH"]; - } elseif (isset($_POST["note"]) && strlen($_POST["note"]) > 50) { $message = $language["NOTE_LENGTH_EXEEDED"]; } @@ -178,23 +175,6 @@ user_name.oninvalid = function(event) { user_name.onkeyup = function (event) { event.target.setCustomValidity(""); } - -- 2.39.5 From bd06342ccf295e370febdf44203f4ad2098987d3 Mon Sep 17 00:00:00 2001 From: Krombel Date: Sun, 11 Feb 2018 20:22:40 +0100 Subject: [PATCH 04/46] add saving registrations to sqlite --- config.sample.php | 2 +- database.php | 29 +++++++++++++++++++++++++++++ register.php | 15 +++++++++++---- 3 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 database.php diff --git a/config.sample.php b/config.sample.php index e998fdb..72f7162 100644 --- a/config.sample.php +++ b/config.sample.php @@ -1,6 +1,6 @@ diff --git a/database.php b/database.php new file mode 100644 index 0000000..3bd1b81 --- /dev/null +++ b/database.php @@ -0,0 +1,29 @@ +exec("CREATE TABLE registrations( + id INTEGER PRIMARY KEY AUTOINCREMENT, + first_name TEXT, + last_name TEXT, + username TEXT, + note TEXT, + email TEXT, + verify_token TEXT, + request_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP)"); +} +else { + // establish connection + $db = new PDO('sqlite:' . $db_file); + $ins_stmt = $db->prepare("INSERT INTO registrations + (first_name, last_name, note, email, username, verify_token) + VALUES (:first_name, :last_name, :note, :email, :username, :verify_token); +} + +// set writeable when not set already +if (!is_writable($db_file)) { + chmod($db_file, 0777); +} +?> \ No newline at end of file diff --git a/register.php b/register.php index eb177b4..02ccca9 100644 --- a/register.php +++ b/register.php @@ -41,15 +41,22 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") { } else { // check valid password + require_once("../database.php"); + $ins_stmt->bindParam(':first_name', $first); + $ins_stmt->bindParam(':last_name', $last); + $ins_stmt->bindParam(':username', $user); + $ins_stmt->bindParam(':note', $note); + $ins_stmt->bindParam(':email', $email); + $ins_stmt->bindParam(':verify_token ', $vToken); $first = filter_var($_POST["first_name"], FILTER_SANITIZE_STRING); $last = filter_var($_POST["last_name"], FILTER_SANITIZE_STRING); $user = filter_var($_POST["username"], FILTER_SANITIZE_STRING); - $pass = filter_var($_POST["password"], FILTER_SANITIZE_STRING); - $email = filter_var($_POST["email"], FILTER_VALIDATE_EMAIL); $note = filter_var($_POST["note"], FILTER_SANITIZE_STRING); + $email = filter_var($_POST["email"], FILTER_VALIDATE_EMAIL); + $vToken= bin2hex(random_bytes(16)); - + $ins_stmt->execute(); $success = true; } if ($success) { @@ -67,7 +74,7 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") { } else { $_SESSION["token"] = bin2hex(random_bytes(16)); ?> - Registriere dich für cg-s.tk + Registriere dich für <?php echo $homeserver; ?> + + + + +
+
+
+
+
+

+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+ +
+ +
+ +
+ +
+ + + + +
+
+
+
+
+
+ - - - -
+ + + + +
-
-
-
-

-
-
-
-
-
-
- -
-
-
-
- -
-
-
- -
- -
- -
- -
- - - - -
-
-
-
+
+
+
+

+
+
+
+
+
+
+ +
+
+
+
+ +
+
-
- + + var password = document.getElementById("password") + , confirm_password = document.getElementById("password_confirm"); + function validatePassword(){ + if(password.value != confirm_password.value) { + confirm_password.setCustomValidity("Passwörter stimmen nicht überein"); + } else { + confirm_password.setCustomValidity(''); + } + } + password.onchange = validatePassword; + confirm_password.onkeyup = validatePassword; + + -- 2.39.5 From 8e50ae1bbd98498d5e03df45bf2e3b3bea386904 Mon Sep 17 00:00:00 2001 From: Krombel Date: Tue, 6 Mar 2018 18:08:44 +0100 Subject: [PATCH 35/46] language: Fallback to de-de instead of failing --- language.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/language.php b/language.php index 3b47ae2..5a1c960 100644 --- a/language.php +++ b/language.php @@ -5,7 +5,8 @@ if(isset($_GET['lang'])){ } $lang_file = dirname(__FILE__) . "/lang/lang.".$lang.".php"; if (!file_exists($lang_file)) { - throw new Exception("Translation for " . $lang . " not found"); + error_log("Translation for " . $lang . " not found. Fallback to 'de-de'"); + $lang = "de-de"; } require_once($lang_file); unset($lang_file); -- 2.39.5 From cd239847ede955290338c1385094faff7b55eccf Mon Sep 17 00:00:00 2001 From: Krombel Date: Tue, 6 Mar 2018 18:25:20 +0100 Subject: [PATCH 36/46] fix: Do not publish the secret password of register_bot --- database.php | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/database.php b/database.php index caf241a..d191e82 100644 --- a/database.php +++ b/database.php @@ -1,4 +1,4 @@ -userRegistered("register_bot")) { $password = $this->addUser("Register", "Bot", "register_bot", $config["register_email"]); $config["register_password"] = $password; - $myfile = fopen("config.json", "w"); + $myfile = fopen(dirname(__FILE__) . "/config.json", "w"); fwrite($myfile, json_encode($config, JSON_PRETTY_PRINT)); fclose($myfile); } @@ -101,28 +101,28 @@ class mxDatabase 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); } @@ -152,7 +152,7 @@ class mxDatabase * @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) { @@ -164,7 +164,7 @@ class mxDatabase require_once("language.php"); throw new Exception($language["USERNAME_REGISTERED"] . " (registered)"); } - + $verify_token = bin2hex(random_bytes(16)); $admin_token = bin2hex(random_bytes(16)); @@ -180,7 +180,7 @@ class mxDatabase /** * Gets the user for the verify_admin page. - * + * * @return ArrayOfUser|NULL Array with "first_name, last_name, username, note and email" * as members */ @@ -205,7 +205,7 @@ class mxDatabase /** * 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 */ @@ -253,11 +253,11 @@ class mxDatabase * @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 @@ -272,7 +272,7 @@ class mxDatabase $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; } -- 2.39.5 From d5f2b05d4d78c3cb8dbace70bfde7c3da5288e57 Mon Sep 17 00:00:00 2001 From: Krombel Date: Wed, 7 Mar 2018 18:55:10 +0100 Subject: [PATCH 37/46] make compatible to postgres --- database.php | 28 ++++++++++++++-------------- public/index.php | 4 +++- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/database.php b/database.php index d191e82..565bb84 100644 --- a/database.php +++ b/database.php @@ -3,8 +3,6 @@ require_once("config.php"); if (!isset($config["databaseURI"])) { throw new Exception ("malformed configuration: databaseURI not defined"); } -$db_input = "sqlite:" . dirname(__FILE__) . "/db_file.sqlite"; -$db_input = $config["databaseURI"]; abstract class RegisterState { @@ -45,7 +43,7 @@ class mxDatabase $this->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 INTEGER PRIMARY KEY AUTOINCREMENT, + id SERIAL PRIMARY KEY, state INT DEFAULT 0, first_name TEXT, last_name TEXT, @@ -57,7 +55,7 @@ class mxDatabase admin_token TEXT, request_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP)"); $this->db->exec("CREATE TABLE IF NOT EXISTS logins ( - id INTEGER PRIMARY KEY AUTOINCREMENT, + id SERIAL PRIMARY KEY, active INT DEFAULT 1, first_name TEXT, last_name TEXT, @@ -100,28 +98,28 @@ class mxDatabase function setRegistrationStateVerify($state, $token) { $sql = "UPDATE registrations SET state = " . $state - . ' WHERE verify_token = "' . $token . '";'; + . " WHERE verify_token = '" . $token . "';"; return $this->db->exec($sql); } function setRegistrationStateById($state, $id) { $sql = "UPDATE registrations SET state = " . $state - . ' WHERE id = "' . $id . '";'; + . " WHERE id = '" . $id . "';"; return $this->db->exec($sql); } function setRegistrationStateAdmin($state, $token) { $sql = "UPDATE registrations SET state = " . $state - . ' WHERE admin_token = "' . $token . '";'; + . " 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 . '";'; + . " WHERE verify_token = '" . $token . "' OR admin_token = '" . $token . "';"; return $this->db->exec($sql); } @@ -168,10 +166,10 @@ class mxDatabase $verify_token = bin2hex(random_bytes(16)); $admin_token = bin2hex(random_bytes(16)); - $this->db->exec('INSERT INTO registrations + $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.'")'); + VALUES ('" . $first_name."','" . $last_name . "','" . $username . "','" . $note . "','" + . $email."','" .$verify_token."','" .$admin_token."')"); return [ "verify_token"=> $verify_token, @@ -270,8 +268,8 @@ class mxDatabase $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 . '")'; + . "('" . $first_name."','" . $last_name . "','" . $username . "','" + . $password_hash . "','" . $email . "');"; if ($this->db->exec($sql)) { return $password; @@ -280,5 +278,7 @@ class mxDatabase } } -$mx_db = new mxDatabase($db_input); +if (!isset($mx_db)) { + $mx_db = new mxDatabase($config["databaseURI"], $config["databaseUser"], $config["databasePass"]); +} ?> diff --git a/public/index.php b/public/index.php index c36eb2e..136cf12 100644 --- a/public/index.php +++ b/public/index.php @@ -51,7 +51,9 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") { $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); - $password = filter_var($_POST["password"], 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); -- 2.39.5 From 6b98ac4ae77856bfa6017619452baeede0590a0b Mon Sep 17 00:00:00 2001 From: Krombel Date: Thu, 8 Mar 2018 11:46:33 +0100 Subject: [PATCH 38/46] reformat and cleanup; Add auth error to response on internal login(debug) --- MatrixConnection.php | 4 +--- config.sample.php | 34 +++++++++++++++++----------------- internal/login.php | 12 ++---------- 3 files changed, 20 insertions(+), 30 deletions(-) diff --git a/MatrixConnection.php b/MatrixConnection.php index e97a39e..e6c52bc 100644 --- a/MatrixConnection.php +++ b/MatrixConnection.php @@ -127,9 +127,7 @@ class MatrixMessage private $message; function __construct() { - $this->message = array( - "msgtype" => "m.notice", - ); + $this->message = ["msgtype" => "m.notice"]; } function set_type($msgtype) { diff --git a/config.sample.php b/config.sample.php index 9717915..6028e67 100644 --- a/config.sample.php +++ b/config.sample.php @@ -1,26 +1,26 @@ "example.com", - "access_token" => "To be used for sending the registration notification", + "homeserver" => "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', + // 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/", + // 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", + // 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, + // 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", + // 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/internal/login.php b/internal/login.php index 3bf36d1..3219255 100644 --- a/internal/login.php +++ b/internal/login.php @@ -15,14 +15,6 @@ abstract class LoginRequester { $loginRequester = LoginRequester::UNDEFINED; try { - $input = json_decode(' - { - "user": { - "id": "@matrix.id.of.the.user:example.com", - "password": "passwordOfTheUser" - } - } - ',true); $inputJSON = file_get_contents('php://input'); $input = json_decode($inputJSON, TRUE); $mxid = NULL; @@ -101,7 +93,7 @@ try { } } catch (Exception $e) { error_log("Auth failed with error: " . $e->getMessage()); - //$response["auth"]["error"] = $e->getMessage(); + $response["auth"]["error"] = $e->getMessage(); } print (json_encode($response, JSON_PRETTY_PRINT) . "\n"); -?> \ No newline at end of file +?> -- 2.39.5 From bce1d01b6d43c4a81344eccb73bfd85f71abfcc3 Mon Sep 17 00:00:00 2001 From: Krombel Date: Thu, 8 Mar 2018 12:43:17 +0100 Subject: [PATCH 39/46] WIP: implement missing endpoints for mxisd --- database.php | 40 ++++++++++++++++++++++++++++ internal/directory_lookup.php | 33 +++++++++++++++++++++++ internal/identity_bulk.php | 49 +++++++++++++++++++++++++++++++++++ internal/identity_single.php | 43 ++++++++++++++++++++++++++++++ internal/login.php | 1 - 5 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 internal/directory_lookup.php create mode 100644 internal/identity_bulk.php create mode 100644 internal/identity_single.php diff --git a/database.php b/database.php index 565bb84..166f791 100644 --- a/database.php +++ b/database.php @@ -276,6 +276,46 @@ class mxDatabase } 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 . "%';"; + $res = $this->db->query($sql); + + if ($res->fetchColumn() > 0) { + $sql = "SELECT first_name, last_name, localpart FROM logins WHERE" + . " localpart LIKE '%" . $term . "%';"; + foreach ($this->db->query($sql) as $row) { + array_push($result, [ + "display_name" => $first_name . " " . $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 . "';"; + $res = $this->db->query($sql); + + if ($res->fetchColumn() > 0) { + $sql = "SELECT first_name, last_name, localpart FROM logins WHERE" + . " email = '" . $term . "';"; + foreach ($this->db->query($sql) as $row) { + array_push($result, [ + "display_name" => $first_name . " " . $last_name, + "user_id" => $row["localpart"], + ]); + } + } + return $result; + } } if (!isset($mx_db)) { diff --git a/internal/directory_lookup.php b/internal/directory_lookup.php new file mode 100644 index 0000000..7a44128 --- /dev/null +++ b/internal/directory_lookup.php @@ -0,0 +1,33 @@ + false, + "result" => [], +]; + +try { + $inputJSON = file_get_contents('php://input'); + $input = json_decode($inputJSON, TRUE); + if (!isset($input["by"])) { + throw new Exception('"id" 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("ídentity_bulk failed with error: " . $e->getMessage()); + $response["error"] = $e->getMessage(); +} +print (json_encode($response, JSON_PRETTY_PRINT) . "\n"); +?> \ No newline at end of file diff --git a/internal/identity_bulk.php b/internal/identity_bulk.php new file mode 100644 index 0000000..f4b5840 --- /dev/null +++ b/internal/identity_bulk.php @@ -0,0 +1,49 @@ + [] +]; +try { + $inputJSON = file_get_contents('php://input'); + $input = json_decode($inputJSON, TRUE); + 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($input["lookup"]["address"]); + if (!empty($res2)) { + array_push($response["lookup"], [ + "medium" => $lookup["medium"], + "address" => $lookup["address"], + "id" => [ + "type" => "localpart", + "value" => $res2[0]["user_id"], + ] + ] + ); + } + case "msisdn": + error_log("sb requested a bulk lookup for 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"); +?> \ No newline at end of file diff --git a/internal/identity_single.php b/internal/identity_single.php new file mode 100644 index 0000000..0268660 --- /dev/null +++ b/internal/identity_single.php @@ -0,0 +1,43 @@ +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"); +?> \ No newline at end of file diff --git a/internal/login.php b/internal/login.php index 3219255..e63bd08 100644 --- a/internal/login.php +++ b/internal/login.php @@ -5,7 +5,6 @@ $response = [ ] ]; -require_once("../config.php"); require_once("../database.php"); abstract class LoginRequester { const UNDEFINED = 0; -- 2.39.5 From d2b5cfbb5e965cc1fa34e88d570908455409db2e Mon Sep 17 00:00:00 2001 From: Krombel Date: Thu, 8 Mar 2018 13:20:26 +0100 Subject: [PATCH 40/46] fixes for internal mxisd endpoints --- database.php | 10 +++++++--- internal/directory_lookup.php | 7 +++++-- internal/identity_bulk.php | 5 +++-- internal/identity_single.php | 7 +++++-- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/database.php b/database.php index 166f791..32db834 100644 --- a/database.php +++ b/database.php @@ -289,7 +289,7 @@ class mxDatabase . " localpart LIKE '%" . $term . "%';"; foreach ($this->db->query($sql) as $row) { array_push($result, [ - "display_name" => $first_name . " " . $last_name, + "display_name" => $row["first_name"] . " " . $row["last_name"], "user_id" => $row["localpart"], ]); } @@ -309,7 +309,7 @@ class mxDatabase . " email = '" . $term . "';"; foreach ($this->db->query($sql) as $row) { array_push($result, [ - "display_name" => $first_name . " " . $last_name, + "display_name" => $row["first_name"] . " " . $row["last_name"], "user_id" => $row["localpart"], ]); } @@ -319,6 +319,10 @@ class mxDatabase } if (!isset($mx_db)) { - $mx_db = new mxDatabase($config["databaseURI"], $config["databaseUser"], $config["databasePass"]); + if (isset($config["databaseUser"]) && isset($config["databasePass"])) { + $mx_db = new mxDatabase($config["databaseURI"], $config["databaseUser"], $config["databasePass"]); + } else { + $mx_db = new mxDatabase($config["databaseURI"]); + } } ?> diff --git a/internal/directory_lookup.php b/internal/directory_lookup.php index 7a44128..c044c83 100644 --- a/internal/directory_lookup.php +++ b/internal/directory_lookup.php @@ -8,8 +8,11 @@ $response=[ 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('"id" is not defined'); + throw new Exception('"by" is not defined'); } if (!isset($input["search_term"])) { throw new Exception('"search_term" is not defined'); @@ -30,4 +33,4 @@ try { $response["error"] = $e->getMessage(); } print (json_encode($response, JSON_PRETTY_PRINT) . "\n"); -?> \ No newline at end of file +?> diff --git a/internal/identity_bulk.php b/internal/identity_bulk.php index f4b5840..0a2feff 100644 --- a/internal/identity_bulk.php +++ b/internal/identity_bulk.php @@ -22,7 +22,7 @@ try { $res2 = array(); switch ($lookup["medium"]) { case "email": - $res2 = $mx_db->searchUserByEmail($input["lookup"]["address"]); + $res2 = $mx_db->searchUserByEmail($lookup["address"]); if (!empty($res2)) { array_push($response["lookup"], [ "medium" => $lookup["medium"], @@ -34,6 +34,7 @@ try { ] ); } + break; case "msisdn": error_log("sb requested a bulk lookup for msisdn"); break; @@ -46,4 +47,4 @@ try { $response["error"] = $e->getMessage(); } print (json_encode($response, JSON_PRETTY_PRINT) . "\n"); -?> \ No newline at end of file +?> diff --git a/internal/identity_single.php b/internal/identity_single.php index 0268660..404f019 100644 --- a/internal/identity_single.php +++ b/internal/identity_single.php @@ -1,9 +1,12 @@ getMessage(); } print (json_encode($response, JSON_PRETTY_PRINT) . "\n"); -?> \ No newline at end of file +?> -- 2.39.5 From 8c854f716d0c5f5605608c2dc19fe333ec46b167 Mon Sep 17 00:00:00 2001 From: Krombel Date: Thu, 8 Mar 2018 14:33:53 +0100 Subject: [PATCH 41/46] renamed directory_lookup => directory_search --- internal/{directory_lookup.php => directory_search.php} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename internal/{directory_lookup.php => directory_search.php} (93%) diff --git a/internal/directory_lookup.php b/internal/directory_search.php similarity index 93% rename from internal/directory_lookup.php rename to internal/directory_search.php index c044c83..fe2b5e1 100644 --- a/internal/directory_lookup.php +++ b/internal/directory_search.php @@ -29,7 +29,7 @@ try { } } catch (Exception $e) { - error_log("ídentity_bulk failed with error: " . $e->getMessage()); + error_log("failed with error: " . $e->getMessage()); $response["error"] = $e->getMessage(); } print (json_encode($response, JSON_PRETTY_PRINT) . "\n"); -- 2.39.5 From b131e6b09e7e8cecaa4c9411875f4507dd38aecd Mon Sep 17 00:00:00 2001 From: Krombel Date: Thu, 8 Mar 2018 23:04:58 +0100 Subject: [PATCH 42/46] change regex for name-searching to match synapse behaviour --- database.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/database.php b/database.php index 32db834..674a599 100644 --- a/database.php +++ b/database.php @@ -281,12 +281,12 @@ class mxDatabase $term = filter_var($search_term, FILTER_SANITIZE_STRING); $result = array(); $sql = "SELECT COUNT(*) FROM logins WHERE" - . " localpart LIKE '%" . $term . "%';"; + . " localpart LIKE '" . $term . "%';"; $res = $this->db->query($sql); if ($res->fetchColumn() > 0) { $sql = "SELECT first_name, last_name, localpart FROM logins WHERE" - . " localpart LIKE '%" . $term . "%';"; + . " localpart LIKE '" . $term . "%';"; foreach ($this->db->query($sql) as $row) { array_push($result, [ "display_name" => $row["first_name"] . " " . $row["last_name"], -- 2.39.5 From 8d84c99492db5601c5e00afeebc7bd0ecd2b7575 Mon Sep 17 00:00:00 2001 From: Krombel Date: Thu, 8 Mar 2018 23:32:22 +0100 Subject: [PATCH 43/46] give complete config object to mxDatabase to resolve issue with undefined ref to it --- database.php | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/database.php b/database.php index 674a599..27e0fb4 100644 --- a/database.php +++ b/database.php @@ -35,10 +35,27 @@ class mxDatabase /** * Creates mxDatabase object - * @param db_input path to the sqlite file where the credentials should be stored + * @param config object which has following members: + * databaseURI: path to the sqlite file where the credentials should be stored * or a param which can be used to connect to a database with PDO + * databaseUser and databasePass when authentication is required + * register_email which email does the register bot have (here used for providing lookup) */ - function __construct($db_input, $user='', $password='') { + function __construct($config) { + if (empty($config)) { + throw new Exception("config is empty"); + } + if (!isset($config["databaseURI"])) { + throw new Exception("'databaseURI' not defined"); + } + $db_input = $config["databaseURI"]; + $user = ''; + $password = ''; + if (isset($config["databaseUser"]) && isset($config["databasePass"])) { + // only use it when both are defined + $user = $config["databaseUser"]; + $password = $config["databasePass"]; + } // create database file when not existent yet $this->db = new PDO($db_input, $user, $password); $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); @@ -53,7 +70,8 @@ class mxDatabase email TEXT, verify_token TEXT, admin_token TEXT, - request_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP)"); + request_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )"); $this->db->exec("CREATE TABLE IF NOT EXISTS logins ( id SERIAL PRIMARY KEY, active INT DEFAULT 1, @@ -66,7 +84,6 @@ class mxDatabase last_modified TIMESTAMP DEFAULT CURRENT_TIMESTAMP )"); // make sure the bot is allowed to login - require_once("config.php"); if (!$this->userRegistered("register_bot")) { $password = $this->addUser("Register", "Bot", "register_bot", $config["register_email"]); $config["register_password"] = $password; @@ -319,10 +336,6 @@ class mxDatabase } if (!isset($mx_db)) { - if (isset($config["databaseUser"]) && isset($config["databasePass"])) { - $mx_db = new mxDatabase($config["databaseURI"], $config["databaseUser"], $config["databasePass"]); - } else { - $mx_db = new mxDatabase($config["databaseURI"]); - } + $mx_db = new mxDatabase($config); } ?> -- 2.39.5 From d58eeafdb58cf9b254848ef35b89b0b22d05d377 Mon Sep 17 00:00:00 2001 From: Krombel Date: Thu, 15 Mar 2018 15:41:08 +0100 Subject: [PATCH 44/46] fix security issue and filter on active users --- database.php | 12 +++++------- internal/identity_bulk.php | 5 ++++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/database.php b/database.php index 674a599..91d4f63 100644 --- a/database.php +++ b/database.php @@ -232,10 +232,8 @@ class mxDatabase if ($res->fetchColumn() > 0) { $sql = "SELECT first_name, last_name, email, password_hash FROM logins " - . " WHERE localpart = '" . $localpart . "' LIMIT 1;"; + . " WHERE localpart = '" . $localpart . "' AND active = 1 LIMIT 1;"; foreach ($this->db->query($sql) as $row) { - error_log($password . "-" . $row["password_hash"]); - // will only be executed once if (password_verify($password, $row["password_hash"])) { return $row; } @@ -281,12 +279,12 @@ class mxDatabase $term = filter_var($search_term, FILTER_SANITIZE_STRING); $result = array(); $sql = "SELECT COUNT(*) FROM logins WHERE" - . " localpart LIKE '" . $term . "%';"; + . " 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 . "%';"; + . " localpart LIKE '" . $term . "%' AND active = 1;"; foreach ($this->db->query($sql) as $row) { array_push($result, [ "display_name" => $row["first_name"] . " " . $row["last_name"], @@ -301,12 +299,12 @@ class mxDatabase $term = filter_var($search_term, FILTER_SANITIZE_STRING); $result = array(); $sql = "SELECT COUNT(*) FROM logins WHERE" - . " email = '" . $term . "';"; + . " 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 . "';"; + . " email = '" . $term . "' AND active = 1;"; foreach ($this->db->query($sql) as $row) { array_push($result, [ "display_name" => $row["first_name"] . " " . $row["last_name"], diff --git a/internal/identity_bulk.php b/internal/identity_bulk.php index 0a2feff..3768a70 100644 --- a/internal/identity_bulk.php +++ b/internal/identity_bulk.php @@ -6,6 +6,10 @@ $response = [ 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'); } @@ -36,7 +40,6 @@ try { } break; case "msisdn": - error_log("sb requested a bulk lookup for msisdn"); break; default: throw new Exception("unknown type for \"by\" param"); -- 2.39.5 From eb414e67dfabe58d2ea16219c7d197f722533818 Mon Sep 17 00:00:00 2001 From: Krombel Date: Thu, 15 Mar 2018 16:26:59 +0100 Subject: [PATCH 45/46] use config for using static references to my installation --- mail_templates.php | 3 ++- public/index.php | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/mail_templates.php b/mail_templates.php index ee1dc85..c6a3b22 100644 --- a/mail_templates.php +++ b/mail_templates.php @@ -1,7 +1,8 @@

Hinweis:
- cg-s.tk is ein geschlossenes Chat-Netzwerk in dem jeder Nutzer bestätigt werden muss.
+ 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 cg-s.tk + Liebe Grüße vom Team von

-- 2.39.5 From ff4947a49dd09c95e51b2f7b07fa2c4cd89165d2 Mon Sep 17 00:00:00 2001 From: Krombel Date: Mon, 19 Mar 2018 13:48:34 +0100 Subject: [PATCH 46/46] update README to contain missing api endpoints; abort execution when no message should be send --- MatrixConnection.php | 1 + README.md | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/MatrixConnection.php b/MatrixConnection.php index e6c52bc..aa63a3c 100644 --- a/MatrixConnection.php +++ b/MatrixConnection.php @@ -18,6 +18,7 @@ class MatrixConnection $send_message = NULL; if (!$message) { error_log("no message to send"); + return false; } elseif(is_array($message)) { $send_message = $message; } elseif ($message instanceof MatrixMessage) { diff --git a/README.md b/README.md index b613d0f..7af7bb2 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,9 @@ This is done in several steps: - 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 | to follow | Search for users by arbitrary input | -| rest.endpoints.identity.single | to follow | Endpoint to query a single 3PID | -| rest.endpoints.identity.bulk | to follow | Endpoint to query a list of 3PID | +| 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 | -- 2.39.5