10 Commits

12 changed files with 243 additions and 52 deletions

5
.gitignore vendored
View File

@@ -1,2 +1,5 @@
config.php config.php
db_file.sqlite db_file.sqlite
# do not track sources which will be built by composer
/vendor/

View File

@@ -14,29 +14,39 @@ This is done in several steps:
- sends them to the user - sends them to the user
- stores them encrypted in own databas or uses that as initial password for registration - stores them encrypted in own databas or uses that as initial password for registration
To configure synapse so that the users can login that were created via this bot you can either There are two operation modes available:
- set `operationMode=synapse` so the bot uses the register api to push the new users to synapse or - `operationMode=synapse`
- integrate it via [matrix-synapse-rest-auth](https://github.com/kamax-io/matrix-synapse-rest-auth#integrate) by configuring your system to point at `internal/login.php`. - No adjustments on your running environment are required. This bot uses the the [Shared-Secret Registration of synapse](https://github.com/matrix-org/synapse/blob/master/docs/admin_api/register_api.rst) to register the users.
- `operationMode=local`:
When using `operationMode=local` you can have the following benefits (some require [mxisd](https://github.com/kamax-io/mxisd/blob/master/docs/stores/rest.md)) - Bot handles user management. Therefore it stores the user-data and uses [matrix-synapse-rest-auth](https://github.com/kamax-io/matrix-synapse-rest-auth#integrate) to authenticate the users.
- Automatically set the display name based on first and last name on first login - This way it is possible to set the display name of a user on first login (first- and lastname instead of username)
- Use the 3PID lookup for other users (only email) - The email address of the user can be used to implement third party lookup (requires [mxisd](https://github.com/kamax-io/mxisd/blob/master/docs/stores/rest.md))
- Search for users that you have not seen yet - search for users you have not seen yet but are available on the server
## Requirements ## Requirements
- Working PHP environment with - Working PHP environment with
- database connection provider \[one of sqlite, mysql, postgres\] - database connection provider \[one of sqlite, mysql, postgres\]
- curl extension to notify admins and register users (in `operationMode=synapse`) - curl extension
- mail capability to interact with the users (Verification, Approval (+ initial password), Notifications) - mail capability to interact with the users (verification, approval (+ initial password), notifications)
- matrix-synapse-rest-auth when using `operationMode=local` - either via sendmail or with credentials
- some PHP capable webserver which makes the folder `public` accessible to the public and propably `internal` for server-internal access - [composer](https://getcomposer.org) installed
- [matrix-synapse-rest-auth](https://github.com/kamax-io/matrix-synapse-rest-auth) when using `operationMode=local`
## How to install ## How to install
- Copy `config.sample.php` to `config.php` and configure the bot as you can find there ```
git clone https://github.com/krombel/matrix-register-bot
cd matrix-register-bot
composer install
cp config.sample.php config.php
editor config.php
```
- Configure your webserver to have the folder `public` accessible via web. - Configure your webserver to have the folder `public` accessible via web.
The folder `internal` contains files that only provide API access. They can be accessed by mxisd or matrix-synapse-rest-auth
When running `operationMode=local`:
- Configure your webserver to provide the folder `internal` internally. This is only meant to be accessible by mxisd and matrix-synapse-rest-auth
- To integrate with [matrix-synapse-rest-auth](https://github.com/kamax-io/matrix-synapse-rest-auth): - To integrate with [matrix-synapse-rest-auth](https://github.com/kamax-io/matrix-synapse-rest-auth):
- `/_matrix-internal/identity/v1/check_credentials` should map to `internal/login.php` - `/_matrix-internal/identity/v1/check_credentials` should map to `internal/login.php`
- To integrate with [mxisd](https://github.com/kamax-io/mxisd): Have a look at [the docs of mxisd](https://github.com/kamax-io/mxisd/blob/master/docs/stores/rest.md) and apply as follows: - To integrate with [mxisd](https://github.com/kamax-io/mxisd): Have a look at [the docs of mxisd](https://github.com/kamax-io/mxisd/blob/master/docs/stores/rest.md) and apply as follows:
@@ -71,3 +81,4 @@ Here is an example for nginx:
### The bot postpones some actions ### The bot postpones some actions
There is a cron.php which implements retries and database cleanups (e.g. to remove a username claim) There is a cron.php which implements retries and database cleanups (e.g. to remove a username claim)
For this run cron.php regularly with your system of choice. For this run cron.php regularly with your system of choice.
A suggested interval is once per day

15
composer.json Normal file
View File

@@ -0,0 +1,15 @@
{
"name": "krombel/matrix-register-bot",
"description": "Register-Bot which implements a 2-factor registration for synapse servers to take part on matrix-communication",
"type": "project",
"require": {
"phpmailer/phpmailer": "^6.0"
},
"license": "Apache-2.0",
"authors": [
{
"name": "Krombel",
"email": "krombel@krombel.de"
}
]
}

84
composer.lock generated Normal file
View File

@@ -0,0 +1,84 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "6d67203b6e9fc952ae681c538683a497",
"packages": [
{
"name": "phpmailer/phpmailer",
"version": "v6.0.6",
"source": {
"type": "git",
"url": "https://github.com/PHPMailer/PHPMailer.git",
"reference": "8190d73eb5def11a43cfb020b7f36db65330698c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/8190d73eb5def11a43cfb020b7f36db65330698c",
"reference": "8190d73eb5def11a43cfb020b7f36db65330698c",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-filter": "*",
"php": ">=5.5.0"
},
"require-dev": {
"doctrine/annotations": "1.2.*",
"friendsofphp/php-cs-fixer": "^2.2",
"phpdocumentor/phpdocumentor": "2.*",
"phpunit/phpunit": "^4.8 || ^5.7",
"zendframework/zend-eventmanager": "3.0.*",
"zendframework/zend-i18n": "2.7.3",
"zendframework/zend-serializer": "2.7.*"
},
"suggest": {
"ext-mbstring": "Needed to send email in multibyte encoding charset",
"hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
"league/oauth2-google": "Needed for Google XOAUTH2 authentication",
"psr/log": "For optional PSR-3 debug logging",
"stevenmaguire/oauth2-microsoft": "Needed for Microsoft XOAUTH2 authentication",
"symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)"
},
"type": "library",
"autoload": {
"psr-4": {
"PHPMailer\\PHPMailer\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1"
],
"authors": [
{
"name": "Jim Jagielski",
"email": "jimjag@gmail.com"
},
{
"name": "Marcus Bointon",
"email": "phpmailer@synchromedia.co.uk"
},
{
"name": "Andy Prevost",
"email": "codeworxtech@users.sourceforge.net"
},
{
"name": "Brent R. Matzelle"
}
],
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
"time": "2018-11-16T00:41:32+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": []
}

View File

@@ -5,6 +5,18 @@ $config = [
// Which e-mail-adresse shall the bot use to send e-mails? // Which e-mail-adresse shall the bot use to send e-mails?
"register_email" => 'register_bot@example.com', "register_email" => 'register_bot@example.com',
// which settings should be used to send via SMTP Gateway?
"smtp" => [
"host" => "localhost",
"port" => "25",
// use authentication?
"user" => "register@example.com",
"password" => "SecretEMailPassword",
// Use some encryption to SMTP-Server? [ssl, tls] or unset
"encryption" => False
],
// Where should the bot post registration requests to? // Where should the bot post registration requests to?
"register_room" => '$registerRoomID:example.com', "register_room" => '$registerRoomID:example.com',

View File

@@ -17,6 +17,9 @@
$language = array( $language = array(
"ACCEPT" => "Akzeptieren", "ACCEPT" => "Akzeptieren",
"DECLINE" => "Ablehnen", "DECLINE" => "Ablehnen",
"DECLINE_REASON" => "Grund für die Ablehnung",
"SUBMIT" => "Abschicken",
"MAKE_A_SELECTION" => "Treffe eine Auswahl",
"SUCCESS" => "Erfolgreich", "SUCCESS" => "Erfolgreich",
"FIRST_NAME" => "Vorname", "FIRST_NAME" => "Vorname",
"LAST_NAME" => "Nachname", "LAST_NAME" => "Nachname",
@@ -33,7 +36,7 @@ $language = array(
"UNKNOWN_TOKEN" => "Token ist unbekannt", "UNKNOWN_TOKEN" => "Token ist unbekannt",
"AUTHENTICATION_FAILED" => "Authentifizierung fehlgeschlagen", "AUTHENTICATION_FAILED" => "Authentifizierung fehlgeschlagen",
"WRONG_REGISTRATION_SHARED_SECRET" => "registration_shared_secret fehlerhaft", "WRONG_REGISTRATION_SHARED_SECRET" => "registration_shared_secret fehlerhaft",
"USERNAME_INVALID" => "Nutzername muss aus 3 bis 20 Kleinbuchstaben bestehen", "USERNAME_INVALID" => "Nutzername muss aus 3 bis 20 Kleinbuchstaben und Zahlen bestehen",
"USERNAME_NOT_ALNUM" => "Nutzername ist nicht alphanumerisch", "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_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", "USERNAME_REGISTERED" => "Dieser Nutzername wurde bereits registriert. Bitte wähle einen anderen Nutzernamen",

View File

@@ -17,6 +17,9 @@
$language = array( $language = array(
"ACCEPT" => "Accept", "ACCEPT" => "Accept",
"DECLINE" => "Decline", "DECLINE" => "Decline",
"DECLINE_REASON" => "Reason for declining",
"SUBMIT" => "Submit",
"MAKE_A_SELECTION" => "Make a selection",
"SUCCESS" => "Success", "SUCCESS" => "Success",
"FIRST_NAME" => "First name", "FIRST_NAME" => "First name",
"LAST_NAME" => "Last name", "LAST_NAME" => "Last name",
@@ -33,7 +36,7 @@ $language = array(
"UNKNOWN_TOKEN" => "Token is unknown", "UNKNOWN_TOKEN" => "Token is unknown",
"AUTHENTICATION_FAILED" => "Authentication failed", "AUTHENTICATION_FAILED" => "Authentication failed",
"WRONG_REGISTRATION_SHARED_SECRET" => "wrong registration_shared_secret", "WRONG_REGISTRATION_SHARED_SECRET" => "wrong registration_shared_secret",
"USERNAME_INVALID" => "Username has to consist of 3 to 20 small letters", "USERNAME_INVALID" => "Username has to consist of 3 to 20 small letters and numbers",
"USERNAME_NOT_ALNUM" => "Username is not alphanumeric", "USERNAME_NOT_ALNUM" => "Username is not alphanumeric",
"USERNAME_PENDING_REGISTRATION" => "This username is locked for registration. Try again later or try again with a different username", "USERNAME_PENDING_REGISTRATION" => "This username is locked for registration. Try again later or try again with a different username",
"USERNAME_REGISTERED" => "This username is already registered. Please try again with another username", "USERNAME_REGISTERED" => "This username is already registered. Please try again with another username",

View File

@@ -14,13 +14,6 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
function send_mail($receiver, $subject, $body) {
include(__DIR__ . "/../config.php");
$headers = "From: " . $config["register_email"] . "\r\n"
. "Content-Type: text/plain;charset=utf-8";
return mail($receiver, $subject, $body, $headers);
}
function send_mail_pending_verification($homeserver, $user, $receiver, $verify_url) { function send_mail_pending_verification($homeserver, $user, $receiver, $verify_url) {
$subject = "Bitte bestätige Registrierung auf $homeserver"; $subject = "Bitte bestätige Registrierung auf $homeserver";
$body = "Guten Tag " . $user . ", $body = "Guten Tag " . $user . ",

View File

@@ -14,13 +14,6 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
function send_mail($receiver, $subject, $body) {
include(__DIR__ . "/../config.php");
$headers = "From: " . $config["register_email"] . "\r\n"
. "Content-Type: text/plain;charset=utf-8";
return mail($receiver, $subject, $body, $headers);
}
function send_mail_pending_verification($homeserver, $user, $receiver, $verify_url) { function send_mail_pending_verification($homeserver, $user, $receiver, $verify_url) {
$subject = "Pleast approve your registration request on $homeserver"; $subject = "Pleast approve your registration request on $homeserver";
$body = "Dear " . $user . ", $body = "Dear " . $user . ",

View File

@@ -15,6 +15,54 @@
* limitations under the License. * limitations under the License.
*/ */
require_once(__DIR__ . "/config.php"); require_once(__DIR__ . "/config.php");
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
require_once(__DIR__ . "/vendor/autoload.php");
// standard mail implementation
function send_mail($receiver, $subject, $body) {
// somehow $config is not available when called again => reinit here
include(__DIR__ . "/config.php");
$mail = new PHPMailer(true);
try {
$mail->CharSet = 'utf-8'; // Enable utf-8 support for umlauts
if (is_array($config["smtp"])) {
$smtp_conf = $config["smtp"];
$mail->isSMTP(); // Set mailer to use SMTP
$mail->Host = $smtp_conf["host"]; // Specify main and backup SMTP servers
$mail->Port = $smtp_conf["port"]; // TCP port to connect to
if (!empty($smtp_conf["user"])) {
$mail->SMTPAuth = true; // Enable SMTP authentication
$mail->Username = $smtp_conf["user"]; // SMTP username
if (!empty($smtp_conf["password"])) {
$mail->Password = $smtp_conf["password"]; // SMTP password
}
}
if (!empty($smtp_conf["encryption"])) {
$mail->SMTPSecure = $smtp_conf["encryption"]; // Enable TLS encryption, `ssl` also accepted
}
} else {
// fallback to sendmail functionality (as before)
$mail->isSendmail();
}
//Recipients
$mail->setFrom($config["register_email"], 'Register Service');
$mail->addAddress($receiver);
$mail->Subject = $subject;
$mail->Body = $body;
$mail->send();
return True;
} catch (Exception $e) {
error_log('Message could not be sent. Mailer Error: ' . $mail->ErrorInfo);
return False;
}
}
$lang = $config["defaultLanguage"]; $lang = $config["defaultLanguage"];
if (isset($_GET['lang'])) { if (isset($_GET['lang'])) {
$lang = filter_var($_GET['lang'], FILTER_SANITIZE_STRING); $lang = filter_var($_GET['lang'], FILTER_SANITIZE_STRING);

View File

@@ -46,17 +46,19 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
// token not present or invalid // token not present or invalid
throw new Exception("UNKNOWN_SESSION"); throw new Exception("UNKNOWN_SESSION");
} }
if (!isset($_POST["username"])) { $username = filter_input(INPUT_POST, "username", FILTER_SANITIZE_STRING);
if (empty($username)) {
throw new Exception("UNKNOWN_USERNAME"); throw new Exception("UNKNOWN_USERNAME");
} }
if (strlen($_POST["username"]) > 20 || if (strlen($username) > 20 || strlen($username) < 3) {
strlen($_POST["username"]) < 3 ||
!ctype_lower($_POST["username"])) {
throw new Exception("USERNAME_INVALID"); throw new Exception("USERNAME_INVALID");
} }
if (ctype_alnum($_POST['username']) != true) { if (!ctype_alnum($username)) {
throw new Exception("USERNAME_NOT_ALNUM"); throw new Exception("USERNAME_NOT_ALNUM");
} }
if (strcmp($username, strtolower($username)) !== 0) {
throw new Exception("USERNAME_INVALID");
}
if ($storePassword && (!isset($_POST["password"]) || !isset($_POST["password_confirm"]))) { if ($storePassword && (!isset($_POST["password"]) || !isset($_POST["password_confirm"]))) {
throw new Exception("PASSWORD_NOT_PROVIDED"); throw new Exception("PASSWORD_NOT_PROVIDED");
} }
@@ -83,7 +85,6 @@ if ($_SERVER["REQUEST_METHOD"] == "POST") {
$first_name = $last_name = ""; $first_name = $last_name = "";
} }
$username = filter_var($_POST["username"], FILTER_SANITIZE_STRING);
$password = ""; $password = "";
if ($storePassword && isset($_POST["password"])) { if ($storePassword && isset($_POST["password"])) {
$password = filter_var($_POST["password"], FILTER_SANITIZE_STRING); $password = filter_var($_POST["password"], FILTER_SANITIZE_STRING);

View File

@@ -33,23 +33,19 @@ try {
if ($_SERVER["REQUEST_METHOD"] != "GET") { if ($_SERVER["REQUEST_METHOD"] != "GET") {
throw new Exception("Method not allowed"); throw new Exception("Method not allowed");
} }
if (!isset($_GET["t"])) { $token = filter_input(INPUT_GET, "t", FILTER_SANITIZE_STRING);
if (empty($token)) {
throw new Exception("UNKNOWN_TOKEN"); throw new Exception("UNKNOWN_TOKEN");
} }
$token = filter_var($_GET["t"], FILTER_SANITIZE_STRING);
require_once(__DIR__ . "/../database.php"); require_once(__DIR__ . "/../database.php");
$action = NULL; $param_action = filter_input(INPUT_GET, "d", FILTER_SANITIZE_STRING);
if (isset($_GET["allow"])) { if ($param_action == "allow") {
$action = RegisterState::RegistrationAccepted; $action = RegisterState::RegistrationAccepted;
} } elseif ($param_action == "deny") {
$decline_reason = NULL;
if (isset($_GET["deny"])) {
$action = RegisterState::RegistrationDeclined; $action = RegisterState::RegistrationDeclined;
if (isset($_GET["reason"])) { $decline_reason = filter_input(INPUT_GET, "decline_reason", FILTER_SANITIZE_STRING);
$decline_reason = filter_var($_GET["reason"], FILTER_SANITIZE_STRING);
}
} }
$user = $mx_db->getUserForApproval($token); $user = $mx_db->getUserForApproval($token);
@@ -139,7 +135,6 @@ try {
print("<h1>" . $language["ADMIN_VERIFY_SITE_TITLE"] . "</h1>"); print("<h1>" . $language["ADMIN_VERIFY_SITE_TITLE"] . "</h1>");
print("<p>" . $language["ADMIN_REGISTER_DECLINED_BODY"] . "</p>"); print("<p>" . $language["ADMIN_REGISTER_DECLINED_BODY"] . "</p>");
} else { } else {
print("<title>" . $language["ADMIN_VERIFY_SITE_TITLE"] . "</title>"); print("<title>" . $language["ADMIN_VERIFY_SITE_TITLE"] . "</title>");
?> ?>
<link href="//netdna.bootstrapcdn.com/bootstrap/3.1.0/css/bootstrap.min.css" rel="stylesheet"> <link href="//netdna.bootstrapcdn.com/bootstrap/3.1.0/css/bootstrap.min.css" rel="stylesheet">
@@ -168,7 +163,7 @@ try {
<h3 class="panel-title"><?php echo $language["ADMIN_VERIFY_SITE_TITLE"]; ?></h3> <h3 class="panel-title"><?php echo $language["ADMIN_VERIFY_SITE_TITLE"]; ?></h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<form name="appForm" role="form" action="verify_admin.php" method="GET"> <form name="appForm" role="form" onsubmit="return submitForm()" action="verify_admin.php" method="GET">
<?php <?php
if (isset($config["operationMode"]) && $config["operationMode"] === "local") { if (isset($config["operationMode"]) && $config["operationMode"] === "local") {
// this values will not be used when using the register operation type // this values will not be used when using the register operation type
@@ -196,9 +191,16 @@ try {
<input type="text" id="username" class="form-control input-sm" <input type="text" id="username" class="form-control input-sm"
value="<?php echo $username; ?>" disabled=true> value="<?php echo $username; ?>" disabled=true>
</div> </div>
<div class="form-group">
<input type="hidden" name="decline_reason" class="form-control input-sm"
placeholder="<?php echo $language["DECLINE_REASON"]; ?>">
</div>
<input type="hidden" name="t" id="token" value="<?php echo $token; ?>"> <input type="hidden" name="t" id="token" value="<?php echo $token; ?>">
<input type="submit" name="allow" value="<?php echo $language["ACCEPT"]; ?>" class="btn btn-info btn-block"> <div class="form-group">
<input type="submit" name="deny" value="<?php echo $language["DECLINE"]; ?>" class="btn btn-info btn-block"> <input type="radio" name="d" value="allow"><?php echo $language["ACCEPT"]; ?>
<input type="radio" name="d" value="deny"><?php echo $language["DECLINE"]; ?>
</div>
<input type="submit" value="<?php echo $language["SUBMIT"]; ?>" class="btn btn-info btn-block">
</form> </form>
</div> </div>
</div> </div>
@@ -206,7 +208,30 @@ try {
</div> </div>
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
var rad = document.appForm.d;
function isSelected() {
for (var i=0; i<rad.length; i++)
if (rad[i].checked)
return true;
return false;
}
function submitForm() {
if (isSelected()) {
return true;
}
alert("<?php echo $language["MAKE_A_SELECTION"];?>");
return false;
}
for(var i = 0; i < rad.length; i++) {
rad[i].onclick = function() {
if (this.value === "deny") {
document.appForm.decline_reason.type = "text";
} else {
document.appForm.decline_reason.type = "hidden";
}
};
}
</script>
<?php <?php
} // else - no action provided } // else - no action provided
} catch (Exception $e) { } catch (Exception $e) {