Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
This commit is contained in:
Maxence Lange 2023-01-23 18:29:25 -01:00
Родитель 26045cab82
Коммит f9913a04b5
9 изменённых файлов: 355 добавлений и 2 удалений

Просмотреть файл

@ -41,6 +41,15 @@ CREATE TABLE IF NOT EXISTS `users` (
KEY `federationId` (`federationId`(191))
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
DROP TABLE IF EXISTS `instances`;
CREATE TABLE IF NOT EXISTS `instances` (
`id` int(6) UNSIGNED NOT NULL AUTO_INCREMENT,
`instance` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `instance` (`instance`(191))
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
DROP TABLE IF EXISTS `toVerify`;
CREATE TABLE IF NOT EXISTS `toVerify` (
`id` int(11) NOT NULL AUTO_INCREMENT,

Просмотреть файл

@ -65,6 +65,12 @@ $CONFIG = [
'CONSUMER_SECRET' => '',
'ACCESS_TOKEN' => '',
'ACCESS_TOKEN_SECRET' => '',
],
// enforce listing of instance instead of auto-generating it based on users' account
'INSTANCES' => [
// 'i001.example.net',
// 'i002.example.net',
]
];

Просмотреть файл

@ -101,6 +101,18 @@ $app->delete('/gs/users', $r_batchDelete);
$app->delete('/index.php/gs/users', $r_batchDelete);
$r_instances = function (ServerRequestInterface $request, ResponseInterface $response, array $args) {
/** @var InstanceManager $instanceManager */
$instanceManager = $this->get('InstanceManager');
return $instanceManager->getInstances($request, $response);
};
$app->get('/gs/instances', $r_instances);
$app->get('/index.php/gs/instances', $r_instances);
$app->post('/instances', $r_instances); // retro compatibility until nc26
$app->post('/index.php/instances', $r_instances); // retro compatibility until nc26
$r_validateEmail = function (ServerRequestInterface $request, ResponseInterface $response, array $args) {
/** @var Email $emailValidator */
$emailValidator = $this->get('EmailValidator');

Просмотреть файл

@ -0,0 +1,13 @@
<?php
namespace LookupServer\Exceptions;
use Exception;
/**
* Class SignedRequestException
*
* @package LookupServer\Exceptions
*/
class SignedRequestException extends Exception {
}

Просмотреть файл

@ -0,0 +1,259 @@
<?php
namespace LookupServer;
use PDO;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class InstanceManager {
private PDO $db;
private SignatureHandler $signatureHandler;
private bool $globalScaleMode = false;
private string $authKey = '';
private array $instances = [];
public function __construct(
PDO $db,
SignatureHandler $signatureHandler,
bool $globalScaleMode,
string $authKey,
?array $instances
) {
$this->db = $db;
$this->signatureHandler = $signatureHandler;
$this->globalScaleMode = $globalScaleMode;
$this->authKey = $authKey;
if (is_array($instances)) {
$this->instances = $instances;
}
}
public function insert(string $instance) {
$stmt = $this->db->prepare('SELECT id, instance, timestamp FROM instances WHERE instance=:instance');
$stmt->bindParam(':instance', $instance, PDO::PARAM_STR);
$stmt->execute();
$data = $stmt->fetch();
if ($data === false) {
$time = time();
$insert = $this->db->prepare(
'INSERT INTO instances (instance, timestamp) VALUES (:instance, FROM_UNIXTIME(:timestamp))'
);
$insert->bindParam(':instance', $instance, PDO::PARAM_STR);
$insert->bindParam(':timestamp', $time, PDO::PARAM_INT);
$insert->execute();
}
}
/**
* let Nextcloud servers obtains the full list of registered instances in the global scale scenario
* If result is empty, sync from the users list
*
* @param Request $request
* @param Response $response
*
* @return Response
*/
public function getInstances(Request $request, Response $response): Response {
if ($this->globalScaleMode !== true) {
$response->withStatus(404);
return $response;
}
$body = json_decode($request->getBody(), true);
if ($body === null || !isset($body['authKey'])) {
$response->withStatus(400);
return $response;
}
if ($body['authKey'] !== $this->authKey) {
$response->withStatus(403);
return $response;
}
$instances = $this->getAll();
if (empty($instances)) {
$this->syncInstances();
$instances = $this->getAll();
}
$response->getBody()
->write(json_encode($instances));
return $response;
}
/**
* @return array
*/
public function getAll(): array {
if (is_array($this->instances) && !empty($this->instances)) {
return $this->instances;
}
$stmt = $this->db->prepare('SELECT instance FROM instances');
$stmt->execute();
$instances = [];
while ($data = $stmt->fetch()) {
$instances[] = $data['instance'];
}
$stmt->closeCursor();
return $instances;
}
public function getAllFromConfig(): array {
return $this->instances;
}
/**
* sync the instances from the users table
*/
public function syncInstances(): void {
$stmt = $this->db->prepare('SELECT federationId FROM users');
$stmt->execute();
$instances = [];
while ($data = $stmt->fetch()) {
$pos = strrpos($data['federationId'], '@');
$instance = substr($data['federationId'], $pos + 1);
if (substr($instance, 0, 7) === 'http://') {
$instance = substr($instance, 7);
}
if (!in_array($instance, $instances)) {
$instances[] = $instance;
}
}
$stmt->closeCursor();
foreach ($instances as $instance) {
$this->insert($instance);
}
$this->removeDeprecatedInstances($instances);
}
/**
* @param string|null $instance
* @param bool $removeUsers
*/
public function remove(string $instance, bool $removeUsers = false): void {
$stmt = $this->db->prepare('DELETE FROM instances WHERE instance = :instance');
$stmt->bindParam(':instance', $instance);
$stmt->execute();
$stmt->closeCursor();
if ($removeUsers) {
$this->removeUsers($instance);
}
}
/**
* @param string $instance
*/
private function removeUsers(string $instance) {
$search = '%@' . $this->escapeWildcard($instance);
$stmt = $this->db->prepare('SELECT id FROM users WHERE federationId LIKE :search');
$stmt->bindParam(':search', $search);
$stmt->execute();
while ($data = $stmt->fetch()) {
$this->removeUser($data['id']);
}
$stmt->closeCursor();
$this->removingEmptyInstance($instance);
}
/**
* @param int $userId
*/
private function removeUser(int $userId) {
$stmt = $this->db->prepare('DELETE FROM users WHERE id = :id');
$stmt->bindParam(':id', $userId);
$stmt->execute();
$stmt = $this->db->prepare('DELETE FROM store WHERE userId = :id');
$stmt->bindParam(':id', $userId);
$stmt->execute();
}
/**
* @param string $input
*
* @return string
*/
private function escapeWildcard(string $input): string {
$output = str_replace('%', '\%', $input);
$output = str_replace('_', '\_', $output);
return $output;
}
/**
* @param string $cloudId
*/
public function newUser(string $cloudId): void {
$pos = strrpos($cloudId, '@');
$instance = substr($cloudId, $pos + 1);
$this->insert($instance);
}
/**
* @param string $cloudId
*/
public function removingUser(string $cloudId): void {
$pos = strrpos($cloudId, '@');
$instance = substr($cloudId, $pos + 1);
$this->removingEmptyInstance($instance);
}
/**
* @param string $instance
*/
private function removingEmptyInstance(string $instance) {
$search = '%@' . $this->escapeWildcard($instance);
$stmt = $this->db->prepare('SELECT federationId FROM users WHERE federationId LIKE :search');
$stmt->bindParam(':search', $search);
$stmt->execute();
if ($stmt->fetch() === false) {
$this->remove($instance);
}
}
/**
* @param array $instances
*/
private function removeDeprecatedInstances(array $instances): void {
$current = $this->getAll();
foreach ($current as $item) {
if (!in_array($item, $instances)) {
$this->remove($item);
}
}
}
}

Просмотреть файл

@ -34,6 +34,7 @@ namespace LookupServer\Service;
use Abraham\TwitterOAuth\TwitterOAuth;
use DI\Container;
use Exception;
use LookupServer\InstanceManager;
use LookupServer\Replication;
use LookupServer\SignatureHandler;
use LookupServer\Tools\Traits\TArrayTools;
@ -77,12 +78,23 @@ class DependenciesService {
});
$container->set('InstanceManager', function (Container $c) {
return new InstanceManager(
$c->get('db'),
$c->get('SignatureHandler'),
$this->getBool('settings.global_scale', $c->get('Settings')),
$this->get('settings.auth_key', $c->get('Settings')),
$this->getArray('settings.instances', $c->get('Settings'))
);
});
$container->set('UserManager', function (Container $c) {
return new UserManager(
$c->get('db'),
$c->get('EmailValidator'),
$c->get('WebsiteValidator'),
$c->get('TwitterValidator'),
$c->get('InstanceManager'),
$c->get('SignatureHandler'),
$this->getBool('settings.global_scale', $c->get('Settings')),
$this->get('settings.auth_key', $c->get('Settings'))

Просмотреть файл

@ -29,9 +29,10 @@ declare(strict_types=1);
namespace LookupServer;
use BadMethodCallException;
use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use LookupServer\Exceptions\SignedRequestException;
use Psr\Http\Message\ServerRequestInterface as Request;
class SignatureHandler {
@ -97,4 +98,33 @@ class SignatureHandler {
return [$user, $host];
}
/**
* @param Request $request
*
* @throws SignedRequestException
*/
public function verifyRequest(Request $request): string {
$body = json_decode($request->getBody(), true);
if ($body === null || !isset($body['message']) || !isset($body['message']['data'])
|| !isset($body['message']['data']['federationId'])
|| !isset($body['signature'])
|| !isset($body['message']['timestamp'])) {
throw new SignedRequestException();
}
$cloudId = $body['message']['data']['federationId'];
try {
$verified = $this->verify($cloudId, $body['message'], $body['signature']);
if ($verified) {
list(, $host) = $this->splitCloudId($body['message']['data']['federationId']);
return $host;
}
} catch (\Exception $e) {
}
throw new SignedRequestException();
}
}

Просмотреть файл

@ -21,6 +21,7 @@ class UserManager {
private Email $emailValidator;
private Website $websiteValidator;
private Twitter $twitterValidator;
private InstanceManager $instanceManager;
private SignatureHandler $signatureHandler;
private int $maxVerifyTries = 10;
private bool $globalScaleMode;
@ -33,6 +34,7 @@ class UserManager {
* @param Email $emailValidator
* @param Website $websiteValidator
* @param Twitter $twitterValidator
* @param InstanceManager $instanceManager
* @param SignatureHandler $signatureHandler
* @param bool $globalScaleMode
* @param string $authKey
@ -42,6 +44,7 @@ class UserManager {
Email $emailValidator,
Website $websiteValidator,
Twitter $twitterValidator,
InstanceManager $instanceManager,
SignatureHandler $signatureHandler,
bool $globalScaleMode,
string $authKey
@ -50,6 +53,7 @@ class UserManager {
$this->emailValidator = $emailValidator;
$this->websiteValidator = $websiteValidator;
$this->twitterValidator = $twitterValidator;
$this->instanceManager = $instanceManager;
$this->signatureHandler = $signatureHandler;
$this->globalScaleMode = $globalScaleMode;
$this->authKey = $authKey;
@ -211,7 +215,10 @@ LIMIT :limit'
$users = [];
while ($data = $stmt->fetch()) {
$users[] = $this->getForUserId((int)$data['userId']);
$entry = $this->getForUserId((int)$data['userId']);
if (!empty($entry)) {
$users[] = $entry;
}
}
$stmt->closeCursor();
@ -306,6 +313,8 @@ LIMIT :limit'
$this->emailValidator->emailUpdated($data[$field], $storeId);
}
}
$this->instanceManager->newUser($cloudId);
}
/**
@ -767,6 +776,8 @@ WHERE
$stmt->execute();
$stmt->closeCursor();
$this->instanceManager->removingUser($cloudId);
return true;
}
}

Просмотреть файл

@ -24,5 +24,6 @@ return [
'access_token' => $CONFIG['TWITTER']['ACCESS_TOKEN'],
'access_token_secret' => $CONFIG['TWITTER']['ACCESS_TOKEN_SECRET'],
],
'instances' => $CONFIG['INSTANCES'] ?? []
]
];