From d346c4d23d18a4d2f45758c2f831d9fe121475cf Mon Sep 17 00:00:00 2001 From: Netkas Date: Sun, 18 Jun 2023 21:12:42 -0400 Subject: [PATCH] Implemented Tamer & Cache Drivers (WIP) --- .idea/inspectionProfiles/Project_Default.xml | 3 + .idea/php.xml | 3 +- STANDARD.md | 125 ++++++++ project.json | 12 + src/FederationCLI/InteractiveMode.php | 292 +++++++++++------- .../InteractiveMode/ClientManager.php | 190 ------------ .../InteractiveMode/ConfigurationManager.php | 113 ------- src/FederationCLI/Utilities.php | 41 +++ src/FederationLib/Classes/Configuration.php | 132 ++++++-- .../CacheServerConfiguration.php | 33 +- .../Configuration/TamerLibConfiguration.php | 71 +++++ src/FederationLib/Classes/Database.php | 4 +- src/FederationLib/Classes/Memcached.php | 18 -- src/FederationLib/Classes/Redis.php | 222 ++++++++++--- src/FederationLib/Classes/Utilities.php | 31 ++ src/FederationLib/Classes/Validate.php | 45 +++ src/FederationLib/Enums/CommandApplets.php | 22 ++ .../Enums/Standard/ErrorCodes.php | 36 ++- src/FederationLib/Enums/Standard/Methods.php | 24 ++ .../Enums/Standard/PermissionRole.php | 22 ++ .../Exceptions/CacheConnectionException.php | 19 ++ .../Exceptions/CacheDriverException.php | 20 ++ .../Standard/AccessDeniedException.php | 15 + .../Standard/InternalServerException.php | 19 ++ .../InvalidClientDescriptionException.php | 19 ++ .../InvalidPermissionRoleException.php | 15 + src/FederationLib/FederationLib.php | 251 ++++++++++++++- .../Interfaces/CacheDriverInterface.php | 107 ++++++- .../Interfaces/CommandAppletInterface.php | 24 ++ src/FederationLib/Managers/ClientManager.php | 292 +++++++++++++----- src/FederationLib/Managers/PeerManager.php | 155 +--------- .../Managers/RedisConnectionManager.php | 177 +++++++++++ src/FederationLib/Objects/Client.php | 137 +------- src/FederationLib/Objects/InvokeResults.php | 50 +++ .../Objects/ParsedFederatedAddress.php | 8 + .../Objects/ResolvedIdentity.php | 98 ++++++ src/FederationLib/Objects/Standard/Client.php | 173 +++++++++++ .../Objects/Standard/ClientIdentity.php | 84 +++++ src/FederationLib/subproc | 22 ++ 39 files changed, 2211 insertions(+), 913 deletions(-) delete mode 100644 src/FederationCLI/InteractiveMode/ClientManager.php delete mode 100644 src/FederationCLI/InteractiveMode/ConfigurationManager.php create mode 100644 src/FederationLib/Classes/Configuration/TamerLibConfiguration.php delete mode 100644 src/FederationLib/Classes/Memcached.php create mode 100644 src/FederationLib/Enums/CommandApplets.php create mode 100644 src/FederationLib/Enums/Standard/Methods.php create mode 100644 src/FederationLib/Enums/Standard/PermissionRole.php create mode 100644 src/FederationLib/Exceptions/CacheConnectionException.php create mode 100644 src/FederationLib/Exceptions/CacheDriverException.php create mode 100644 src/FederationLib/Exceptions/Standard/AccessDeniedException.php create mode 100644 src/FederationLib/Exceptions/Standard/InternalServerException.php create mode 100644 src/FederationLib/Exceptions/Standard/InvalidClientDescriptionException.php create mode 100644 src/FederationLib/Exceptions/Standard/InvalidPermissionRoleException.php create mode 100644 src/FederationLib/Interfaces/CommandAppletInterface.php create mode 100644 src/FederationLib/Managers/RedisConnectionManager.php create mode 100644 src/FederationLib/Objects/InvokeResults.php create mode 100644 src/FederationLib/Objects/ResolvedIdentity.php create mode 100644 src/FederationLib/Objects/Standard/Client.php create mode 100644 src/FederationLib/Objects/Standard/ClientIdentity.php create mode 100644 src/FederationLib/subproc diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index ea8af9c..a854623 100755 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -19,6 +19,9 @@ diff --git a/.idea/php.xml b/.idea/php.xml index 432a2aa..fea1810 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -24,6 +24,7 @@ + @@ -73,12 +74,10 @@ - - diff --git a/STANDARD.md b/STANDARD.md index 6dfa733..0478669 100755 --- a/STANDARD.md +++ b/STANDARD.md @@ -18,6 +18,12 @@ spam and malicious content on various communication channels such as email, soci * [Client TOTP Signature](#client-totp-signature) * [Client Object](#client-object) * [Federation Standard](#federation-standard) + * [Available Types](#available-types) + * [ClientIdentity Object](#clientidentity-object) + * [Invokable Methods](#invokable-methods) + * [ping Method](#ping-method) + * [whoami Method](#whoami-method) + * [create_client Method](#createclient-method) * [Standard Federated Addresses](#standard-federated-addresses) * [Query Document](#query-document) * [QueryDocument Versioning](#querydocument-versioning) @@ -73,6 +79,125 @@ TODO: Write this section The federation standard is a standard which defines how clients should communicate with servers, the standard +## Available Types + + +### ClientIdentity Object + +The ClientIdentity object is used to identify the client, this standard object contains information about what client +is invoking the method, this object is required to be provided by all clients when invoking a method, this object +is used by the server to determine if the client is authorized to invoke the method and to determine what permissions +the client has. + +In such cases where a peer is attempting to invoke a method through a client such as, the server will identify the +invoker as the peer on behalf of the client, this is done by providing the peer's standard federated address in the +`peer` field of the ClientIdentity object + +| Parameter | Type | Required | Description | +|-------------------------|----------|----------|--------------------------------------------------------------------------------------------------------------------------------------| +| `client_uuid` | `string` | Yes | The UUID of the client that is invoking the request | +| `client_totp_signature` | `string` | No | Optional. The client's TOTP signature that proves the request came from the client, only applicable if the client uses authorization | +| `peer` | `string` | No | Optional. The peer that's invoking the command on behalf of the client | + + > Note: If a server has strict permissions enabled, in such cases where the peer has higher permissions than the + client, the server will default to maximum permissions as the client rather than the peer, this is to + prevent clients without high permissions from abusing the system by using a peer with higher permissions. + +------------------------------------------------------------------------------------------------------------------------ + +## Invokable Methods + +Invokable Methods are used by clients, peers or the server to invoke a method on the server, the server will use the +provided information to determine who's invoking the method and if the method is allowed to be invoked by the peer. + +All these methods are standard and required to be implemented by all servers, however, approach would allow for servers +to implement additional methods to allow for additional functionality that may not be available in the standard, +however, this would require the client to be aware of the additional methods and the server to be aware of the +additional methods. + +These methods are supposed to invokable by a virtual shell or API endpoint, however, this means while the client does +not need to implement or use the shell to invoke these methods but can use the API to achieve the same result as the +shell. + + +### ping Method + +The ping method is used to check if the server is online and to check the server's version, the ping method is +available to all peers. Returns True if the execution was successful, otherwise the client should assume the server +is offline or the server is temporarily unreachable. + +JSON-RPC Example: + +```jsonl + > {"jsonrpc": "2.0", "method": "ping", "id": 1} + < {"jsonrpc": "2.0", "result": true, "id": 1} +``` + +Shell Example: + +```shell +$ ping +true +``` + +Default Permission Table: + +| root | admin | operator | agent | client | guest | +|:----:|:-----:|:--------:|:-----:|:------:|:-----:| +| ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | + + +------------------------------------------------------------------------------------------------------------------------ + + +### whoami Method + +The whoami method is used to check the client's permissions and other information, the whoami method is available +to all peers. Returns the identified "*uid*" of whoever is invoking the method, this can either be `root` if the +function is being invoked by the server or from the root shell, or the client's UID if the function is being invoked +by a client or finally the peer's Standard Federated Address if the function is being invoked by a peer. + +JSON-RPC Example: + +```jsonl + > {"jsonrpc": "2.0", "method": "whoami", "id": 1} + < {"jsonrpc": "2.0", "result": "root", "id": 1} +``` + +Shell Example: + +```shell +$ whoami +root +``` + +Default Permission Table: + +| root | admin | operator | agent | client | guest | +|:----:|:-----:|:--------:|:-----:|:------:|:-----:| +| ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | + + +------------------------------------------------------------------------------------------------------------------------ + + +### create_client Method + +The create_client method is used to create a new client, the create_client method is only available to the root & admin +user by default, however, this can be changed by the server's configuration. Returns the UID of the newly created client +if the execution was successful + +Parameters: + +| Name | Type | Required | Description | +|------------------|----------|----------|:-------------------------------------------| +| `uid` | `string` | Yes | The user ID of that is invoking the method | +| `totp_signature` | `string` | Yes | The TOTP signature for authentication | +| `name` | `string` | Yes | The username of the client | + + +------------------------------------------------------------------------------------------------------------------------ + ## Standard Federated Addresses A standard federated address is a universally unique identifier which can represent multiple types of peers from diff --git a/project.json b/project.json index cbc7e37..3f157b5 100755 --- a/project.json +++ b/project.json @@ -43,6 +43,18 @@ "source_type": "remote", "source": "nosial/libs.config=latest@n64" }, + { + "name": "net.nosial.optslib", + "version": "latest", + "source_type": "remote", + "source": "nosial/libs.opts=latest@n64" + }, + { + "name": "net.nosial.tamerlib", + "version": "latest", + "source_type": "remote", + "source": "nosial/libs.tamer=latest@n64" + }, { "name": "net.nosial.loglib", "version": "latest", diff --git a/src/FederationCLI/InteractiveMode.php b/src/FederationCLI/InteractiveMode.php index 710ac95..470e031 100644 --- a/src/FederationCLI/InteractiveMode.php +++ b/src/FederationCLI/InteractiveMode.php @@ -4,146 +4,220 @@ namespace FederationCLI; + use Exception; + use FederationLib\Classes\Configuration; + use FederationLib\Enums\Standard\Methods; + use FederationLib\Exceptions\DatabaseException; + use FederationLib\Exceptions\Standard\AccessDeniedException; + use FederationLib\Exceptions\Standard\ClientNotFoundException; + use FederationLib\Exceptions\Standard\InternalServerException; + use FederationLib\Exceptions\Standard\InvalidClientDescriptionException; + use FederationLib\Exceptions\Standard\InvalidClientNameException; use FederationLib\FederationLib; + use JsonException; + use OptsLib\Parse; + use TamerLib\Enums\TamerMode; + use TamerLib\Exceptions\ServerException; + use TamerLib\Exceptions\WorkerFailedException; + use TamerLib\tm; class InteractiveMode { /** - * The current menu the user is in - * - * @var string + * @var FederationLib */ - private static $current_menu = 'Main'; - - /** - * An array of menu pointers to functions - * - * @var string[] - */ - private static $menu_pointers = [ - 'ClientManager' => 'FederationCLI\InteractiveMode\ClientManager::processCommand', - 'ConfigManager' => 'FederationCLI\InteractiveMode\ConfigurationManager::processCommand' - ]; - - private static $help_pointers =[ - 'ClientManager' => 'FederationCLI\InteractiveMode\ClientManager::help', - 'ConfigManager' => 'FederationCLI\InteractiveMode\ConfigurationManager::help' - ]; - - /** - * @var FederationLib|null - */ - private static $federation_lib = null; + private static $federation_lib; /** * Main entry point for the interactive mode * * @param array $args * @return void + * @throws ServerException + * @throws WorkerFailedException */ public static function main(array $args=[]): void { + tm::initialize(TamerMode::CLIENT, Configuration::getTamerLibConfiguration()->getServerConfiguration()); + tm::createWorker(Configuration::getTamerLibConfiguration()->getCliWorkers(), FederationLib::getSubprocessorPath()); + + self::$federation_lib = new FederationLib(); + while(true) { - print(sprintf('federation@%s:~$ ', self::$current_menu)); + print(sprintf('%s@%s:~$ ', 'root', Configuration::getHostName())); $input = trim(fgets(STDIN)); - self::processCommand($input); + try + { + switch(strtolower(explode(' ', $input)[0])) + { + case Methods::PING: + self::ping(); + break; + case Methods::WHOAMI: + self::whoami(); + break; + + case Methods::CREATE_CLIENT: + self::createClient($input); + break; + case Methods::GET_CLIENT: + self::getClient($input); + break; + case Methods::CHANGE_CLIENT_NAME: + self::changeClientName($input); + break; + + default: + print(sprintf('Command \'%s\' not found' . PHP_EOL, $input)); + break; + } + } + catch(Exception $e) + { + print(sprintf('Error: %s' . PHP_EOL, $e->getMessage())); + } } } /** - * Processes a command from the user + * Invokes the ping method + * + * @return void + * @throws AccessDeniedException + * @throws ClientNotFoundException + * @throws InternalServerException + */ + private static function ping(): void + { + if(self::$federation_lib->ping(null)) + { + print('OK' . PHP_EOL); + return; + } + + print('ERROR' . PHP_EOL); + } + + /** + * Invokes the whoami method and prints the result + * + * @return void + * @throws AccessDeniedException + * @throws ClientNotFoundException + * @throws InternalServerException + */ + private static function whoami(): void + { + print(self::$federation_lib->whoami(null) . PHP_EOL); + } + + /** + * @param string $input + * @return void + * @throws AccessDeniedException + * @throws ClientNotFoundException + * @throws DatabaseException + * @throws InternalServerException + * @throws InvalidClientDescriptionException + * @throws InvalidClientNameException + */ + private static function createClient(string $input): void + { + $parsed_arguments = Parse::parseArgument($input); + + $name = $parsed_arguments['name'] ?? $parsed_arguments['n'] ?? null; + $description = $parsed_arguments['description'] ?? $parsed_arguments['d'] ?? null; + + print(self::$federation_lib->createClient(null, $name, $description) . PHP_EOL); + } + + /** + * @param string $input + * @return void + * @throws AccessDeniedException + * @throws DatabaseException + * @throws InternalServerException + * @throws JsonException + * @noinspection PhpMultipleClassDeclarationsInspection + */ + private static function getClient(string $input): void + { + $parsed_arguments = Parse::parseArgument($input); + + $uuid = $parsed_arguments['uuid'] ?? $parsed_arguments['u'] ?? null; + $raw = $parsed_arguments['raw'] ?? $parsed_arguments['r'] ?? false; + + if($uuid === null | $uuid === '') + { + print('Missing required argument \'uuid\'' . PHP_EOL); + return; + } + + try + { + $client = self::$federation_lib->getClient(null, $uuid); + + if($raw) + { + print(json_encode($client->toArray(), JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT) . PHP_EOL); + return; + } + + Utilities::printData('CLIENT LOOKUP RESULTS', [ + 'UUID' => $client->getUuid(), + 'NAME' => $client->getName(), + 'DESCRIPTION' => $client->getDescription() ?? 'N/A', + 'PERMISSION_ROLE' => $client->getPermissionRole(), + 'CREATED_AT' => date('Y-m-d H:i:s', $client->getCreatedTimestamp()), + 'UPDATED_AT' => date('Y-m-d H:i:s', $client->getUpdatedTimestamp()), + 'SEEN_AT' => date('Y-m-d H:i:s', $client->getSeenTimestamp()) + ]); + } + catch(ClientNotFoundException) + { + print(sprintf('Client with UUID \'%s\' not found' . PHP_EOL, $uuid)); + } + } + + /** + * Changes the name of a client with the given UUID * * @param string $input * @return void + * @throws AccessDeniedException + * @throws DatabaseException + * @throws InternalServerException + * @throws InvalidClientNameException */ - private static function processCommand(string $input): void + private static function changeClientName(string $input): void { - $parsed_input = Utilities::parseShellInput($input); + $parsed_arguments = Parse::parseArgument($input); - switch(strtolower($parsed_input['command'])) + $uuid = $parsed_arguments['uuid'] ?? $parsed_arguments['u'] ?? null; + $name = $parsed_arguments['name'] ?? $parsed_arguments['n'] ?? null; + + if($uuid === null | $uuid === '') { - case 'help': - print('Available commands:' . PHP_EOL); - print(' help - displays the help menu for the current menu and global commands' . PHP_EOL); - print(' client_manager - enter client manager mode' . PHP_EOL); - print(' config_manager - enter config manager mode' . PHP_EOL); - print(' clear - clears the screen' . PHP_EOL); - print(' exit - exits the current menu, if on the main menu, exits the program' . PHP_EOL); - - if(isset(self::$help_pointers[self::$current_menu])) - { - call_user_func(self::$help_pointers[self::$current_menu]); - } - - break; - - case 'client_manager': - self::$current_menu = 'ClientManager'; - break; - - case 'config_manager': - self::$current_menu = 'ConfigManager'; - break; - - case 'clear': - print(chr(27) . "[2J" . chr(27) . "[;H"); - break; - - case 'exit': - if(self::$current_menu != 'Main') - { - self::$current_menu = 'Main'; - break; - } - - exit(0); - - default: - if(!isset(self::$menu_pointers[self::$current_menu])) - { - print(sprintf('Unknown command: %s', $parsed_input['command']) . PHP_EOL); - break; - } - - call_user_func(self::$menu_pointers[self::$current_menu], $input); - break; - } - } - - /** - * Returns the current menu - * - * @return string - */ - public static function getCurrentMenu(): string - { - return self::$current_menu; - } - - /** - * Sets the current menu to the specified value - * - * @param string $current_menu - */ - public static function setCurrentMenu(string $current_menu): void - { - self::$current_menu = $current_menu; - } - - /** - * Returns the FederationLib instance - * - * @return FederationLib - */ - public static function getFederationLib(): FederationLib - { - if(self::$federation_lib == null) - { - self::$federation_lib = new FederationLib(); + print('Missing required argument \'uuid\'' . PHP_EOL); + return; } - return self::$federation_lib; + if($name === null | $name === '') + { + print('Missing required argument \'name\'' . PHP_EOL); + return; + } + + try + { + self::$federation_lib->changeClientName(null, $uuid, $name); + print('OK' . PHP_EOL); + } + catch(ClientNotFoundException) + { + print(sprintf('Client with UUID \'%s\' not found' . PHP_EOL, $uuid)); + } } } \ No newline at end of file diff --git a/src/FederationCLI/InteractiveMode/ClientManager.php b/src/FederationCLI/InteractiveMode/ClientManager.php deleted file mode 100644 index 84174d6..0000000 --- a/src/FederationCLI/InteractiveMode/ClientManager.php +++ /dev/null @@ -1,190 +0,0 @@ -setName(Utilities::promptInput('Client name (default: Random): ')); - $client->setDescription(Utilities::promptInput('Client description (default: N/A): ')); - $client->setQueryPermission((int)Utilities::parseBoolean(Utilities::promptInput('Query permission (default: 1): '))); - $client->setUpdatePermission((int)Utilities::parseBoolean(Utilities::promptInput('Update permission (default: 0): '))); - - - try - { - $client_uuid = InteractiveMode::getFederationLib()->getClientManager()->registerClient($client); - } - catch(Exception $e) - { - print('Failed to register client: ' . $e->getMessage() . PHP_EOL); - Utilities::printExceptionStack($e); - return; - } - - print(sprintf('Client registered successfully, UUID: %s', $client_uuid) . PHP_EOL); - } - - /** - * @param array $args - * @return void - * @throws DatabaseException - * @throws \Doctrine\DBAL\Exception - * @throws \RedisException - */ - private static function listClients(array $args): void - { - $page = $args[0] ?? 1; - $filter = $args[1] ?? ListClientsFilter::CREATED_TIMESTAMP; - $order = $args[2] ?? FilterOrder::ASCENDING; - $max_items = $args[3] ?? 100; - - $clients = InteractiveMode::getFederationLib()->getClientManager()->listClients($page, $filter, $order, $max_items); - - if(count($clients) === 0) - { - print('No clients found' . PHP_EOL); - } - else - { - $builder = new Builder(); - foreach($clients as $client) - { - $builder->addRow($client->toArray()); - } - $builder->setTitle(sprintf('Clients (page %d, filter %s, order %s, max items %d)', $page, $filter, $order, $max_items)); - print($builder->renderTable() . PHP_EOL); - } - } - - private static function getClient(mixed $args) - { - $client_uuid = $args[0] ?? null; - - if(is_null($client_uuid)) - { - print('Client UUID required' . PHP_EOL); - return; - } - - try - { - $client = InteractiveMode::getFederationLib()->getClientManager()->getClient($client_uuid); - } - catch(Exception $e) - { - print('Failed to get client: ' . $e->getMessage() . PHP_EOL); - Utilities::printExceptionStack($e); - return; - } - - foreach($client->toArray() as $key => $value) - { - print match ($key) - { - 'id' => (sprintf(' UUID: %s', $value) . PHP_EOL), - 'enabled' => (sprintf(' Enabled: %s', (Utilities::parseBoolean($value) ? 'Yes' : 'No')) . PHP_EOL), - 'name' => (sprintf(' Name: %s', $value ?? 'N/A') . PHP_EOL), - 'description' => (sprintf(' Description: %s', $value ?? 'N/A') . PHP_EOL), - 'secret_totp' => (sprintf(' Secret TOTP: %s', $value ?? 'N/A') . PHP_EOL), - 'query_permission' => (sprintf(' Query permission Level: %s', $value) . PHP_EOL), - 'update_permission' => (sprintf(' Update permission Level: %s', $value) . PHP_EOL), - 'flags' => (sprintf(' Flags: %s', $value) . PHP_EOL), - 'created_timestamp' => (sprintf(' Created: %s', date('Y-m-d H:i:s', $value)) . PHP_EOL), - 'updated_timestamp' => (sprintf(' Updated: %s', date('Y-m-d H:i:s', $value)) . PHP_EOL), - 'seen_timestamp' => (sprintf(' Last seen: %s', date('Y-m-d H:i:s', $value)) . PHP_EOL), - default => (sprintf(' %s: %s', $key, $value) . PHP_EOL), - }; - } - - } - - private static function totalClients() - { - print(sprintf('Total clients: %d', InteractiveMode::getFederationLib()->getClientManager()->getTotalClients()) . PHP_EOL); - } - - private static function totalPages(mixed $args) - { - $max_items = $args[0] ?? 100; - - print(sprintf('Total pages: %d', InteractiveMode::getFederationLib()->getClientManager()->getTotalPages($max_items)) . PHP_EOL); - } - } \ No newline at end of file diff --git a/src/FederationCLI/InteractiveMode/ConfigurationManager.php b/src/FederationCLI/InteractiveMode/ConfigurationManager.php deleted file mode 100644 index 7d5c0c2..0000000 --- a/src/FederationCLI/InteractiveMode/ConfigurationManager.php +++ /dev/null @@ -1,113 +0,0 @@ - - reads the value of the specified configuration key' . PHP_EOL); - print(' write - writes the value of the specified configuration key' . PHP_EOL); - print(' save - saves the current configuration to disk' . PHP_EOL); - } - - /** - * Reads the current configuration or the value of a specific key - * - * @param string|null $key - * @return void - */ - private static function read(?string $key=null): void - { - if($key === null) - { - $value = Configuration::getConfiguration(); - } - else - { - $value = Configuration::getConfigurationObject()->get($key); - } - - if(is_null($value)) - { - print('No value found for key: ' . $key . PHP_EOL); - } - elseif(is_array($value)) - { - print(json_encode($value, JSON_PRETTY_PRINT) . PHP_EOL); - } - else - { - print($value . PHP_EOL); - } - } - - /** - * Writes the value of a specific configuration key - * - * @param string $key - * @param string $value - * @return void - */ - private static function write(string $key, string $value): void - { - Configuration::getConfigurationObject()->set($key, $value); - } - - /** - * Writes the current configuration to disk - * - * @return void - */ - private static function save(): void - { - try - { - Configuration::getConfigurationObject()->save(); - } - catch(Exception $e) - { - print('Failed to save configuration: ' . $e->getMessage() . PHP_EOL); - Utilities::printExceptionStack($e); - } - } - } \ No newline at end of file diff --git a/src/FederationCLI/Utilities.php b/src/FederationCLI/Utilities.php index 325afeb..ae9c848 100644 --- a/src/FederationCLI/Utilities.php +++ b/src/FederationCLI/Utilities.php @@ -98,4 +98,45 @@ return self::parseBoolean($input); } + + /** + * Prints data in a formatted manner + * + * @param $banner_text + * @param $data + * @param int $body_width + * @return void + */ + public static function printData($banner_text, $data, int $body_width = 70) + { + // Padding and wrap for the longest key + $max_key_len = max(array_map('strlen', array_keys($data))); + + // Adjust padding for body_width + $padding = $body_width - ($max_key_len + 4); // Additional 2 spaces for initial padding + + // Banner + $banner_width = $body_width + 2; + echo str_repeat("*", $banner_width) . PHP_EOL; + echo "* " . str_pad($banner_text, $banner_width - 4, ' ', STR_PAD_RIGHT) . " *" . PHP_EOL; + echo str_repeat("*", $banner_width) . PHP_EOL; + + // Print data + foreach ($data as $key => $value) { + // Split value into lines if it's too long + $lines = str_split($value, $padding); + + // Print lines + foreach ($lines as $i => $line) { + if ($i == 0) { + // First line - print key and value + echo ' ' . str_pad(strtoupper($key), $max_key_len, ' ', STR_PAD_RIGHT) . ' ' . $line . PHP_EOL; + } else { + // Additional lines - only value + echo str_repeat(' ', $max_key_len + 4) . $line . PHP_EOL; + } + } + } + } + } \ No newline at end of file diff --git a/src/FederationLib/Classes/Configuration.php b/src/FederationLib/Classes/Configuration.php index 05315ff..87320df 100644 --- a/src/FederationLib/Classes/Configuration.php +++ b/src/FederationLib/Classes/Configuration.php @@ -6,6 +6,9 @@ use Exception; use FederationLib\Classes\Configuration\CacheServerConfiguration; + use FederationLib\Classes\Configuration\TamerLibConfiguration; + use FederationLib\Enums\Standard\Methods; + use FederationLib\Enums\Standard\PermissionRole; use RuntimeException; class Configuration @@ -15,6 +18,11 @@ */ private static $configuration; + /** + * @var TamerLibConfiguration|null + */ + private static $tamerlib_configuration; + /** * Returns the full raw configuration array. * @@ -37,46 +45,55 @@ self::$configuration->setDefault('database.reconnect_interval', 1800); /** Multi-Cache Configuration */ - self::$configuration->setDefault('cache_system.enabled', true); // Cache System Configuration + self::$configuration->setDefault('cache_system.enabled', true); + self::$configuration->setDefault('cache_system.opened_connection_priority', 20); // Higher is better + self::$configuration->setDefault('cache_system.error_connection_priority', -30); // Lower is better // Client Objects self::$configuration->setDefault('cache_system.cache.client_objects_enabled', true); self::$configuration->setDefault('cache_system.cache.client_objects_ttl', 200); self::$configuration->setDefault('cache_system.cache.client_objects_server_preference', 'redis_master'); - self::$configuration->setDefault('cache_system.cache.client_objects_server_fallback', 'any'); + self::$configuration->setDefault('cache_system.cache.client_objects_server_fallback', 'redis_slave'); // Peer Objects self::$configuration->setDefault('cache_system.cache.peer_objects_enabled', true); self::$configuration->setDefault('cache_system.cache.peer_objects_ttl', 200); self::$configuration->setDefault('cache_system.cache.peer_objects_server_preference', 'redis_master'); - self::$configuration->setDefault('cache_system.cache.peer_objects_server_fallback', 'any'); - // Redis Configuration + self::$configuration->setDefault('cache_system.cache.peer_objects_server_fallback', 'redis_slave'); + /** Multi-Cache Server Configuration */ + // Redis Master Configuration self::$configuration->setDefault('cache_system.servers.redis_master.enabled', true); self::$configuration->setDefault('cache_system.servers.redis_master.host', 'localhost'); self::$configuration->setDefault('cache_system.servers.redis_master.port', 6379); - self::$configuration->setDefault('cache_system.servers.redis_master.driver', 'redis'); - self::$configuration->setDefault('cache_system.servers.redis_master.priority', 1); - self::$configuration->setDefault('cache_system.servers.redis_master.username', null); + self::$configuration->setDefault('cache_system.servers.redis_master.priority', 100); self::$configuration->setDefault('cache_system.servers.redis_master.password', null); self::$configuration->setDefault('cache_system.servers.redis_master.database', null); self::$configuration->setDefault('cache_system.servers.redis_master.reconnect_interval', 1800); - // Memcached Configuration - self::$configuration->setDefault('cache_system.servers.memcached_master.enabled', false); - self::$configuration->setDefault('cache_system.servers.memcached_master.host', 'localhost'); - self::$configuration->setDefault('cache_system.servers.memcached_master.port', 11211); - self::$configuration->setDefault('cache_system.servers.memcached_master.driver', 'memcached'); - self::$configuration->setDefault('cache_system.servers.memcached_master.priority', 1); - self::$configuration->setDefault('cache_system.servers.memcached_master.username', null); - self::$configuration->setDefault('cache_system.servers.memcached_master.password', null); - self::$configuration->setDefault('cache_system.servers.memcached_master.database', null); - self::$configuration->setDefault('cache_system.servers.memcached_master.reconnect_interval', 1800); - - + // Redis Slave Configuration + self::$configuration->setDefault('cache_system.servers.redis_slave.enabled', false); + self::$configuration->setDefault('cache_system.servers.redis_slave.host', 'localhost'); + self::$configuration->setDefault('cache_system.servers.redis_slave.port', 11211); + self::$configuration->setDefault('cache_system.servers.redis_slave.priority', 50); + self::$configuration->setDefault('cache_system.servers.redis_slave.password', null); + self::$configuration->setDefault('cache_system.servers.redis_slave.database', null); + self::$configuration->setDefault('cache_system.servers.redis_slave.reconnect_interval', 1800); /** Federation Configuration */ + self::$configuration->setDefault('federation.hostname', 'FederationLib'); // Server Hostname self::$configuration->setDefault('federation.events_retention', 1209600); // Two Weeks - self::$configuration->setDefault('federation.events_enabled.generic', true); - self::$configuration->setDefault('federation.events_enabled.client_Created', true); + self::$configuration->setDefault('federation.security.strict_permissions', true); // Security Feature, prevents clients & peers from elevating their permissions + self::$configuration->setDefault('federation.security.method_permissions.ping', 5); // Guest or above + self::$configuration->setDefault('federation.security.method_permissions.whoami', 5); // Guest or above + self::$configuration->setDefault('federation.security.method_permissions.create_client', 2); // Admin or above + self::$configuration->setDefault('federation.security.method_permissions.get_client', 2); // Admin or above + self::$configuration->setDefault('federation.security.method_permissions.update_client_name', 2); // Admin or above + /** TamerLib Configuration */ + self::$configuration->setDefault('federation.tamer_lib.cli_workers', 8); + self::$configuration->setDefault('federation.tamer_lib.node_workers', 20); + self::$configuration->setDefault('federation.tamer_lib.server.host', '127.0.0.1'); + self::$configuration->setDefault('federation.tamer_lib.server.port', 6379); + self::$configuration->setDefault('federation.tamer_lib.server.password', null); + self::$configuration->setDefault('federation.tamer_lib.server.database', null); /** Save the configuration's default values if they don't exist */ try @@ -107,6 +124,19 @@ return self::$configuration; } + /** + * @return TamerLibConfiguration + */ + public static function getTamerLibConfiguration(): TamerLibConfiguration + { + if(self::$tamerlib_configuration === null) + { + self::$tamerlib_configuration = new TamerLibConfiguration(self::getConfiguration()); + } + + return self::$tamerlib_configuration; + } + /** * Returns driver of the database. * @@ -194,6 +224,46 @@ return (int)self::getConfiguration()['database']['reconnect_interval']; } + /** + * Returns the hostname of the server. + * + * @return string + */ + public static function getHostName(): string + { + return self::getConfiguration()['federation']['hostname']; + } + + /** + * Returns True if the strict permission system is enabled and False if not. + * + * @return bool + */ + public static function strictPermissionEnabled(): bool + { + return (bool)self::getConfiguration()['federation']['security']['strict_permissions']; + } + + /** + * Returns the permission level required to execute a command. + * + * @param string $command + * @return int + */ + public static function getMethodPermission(string $command): int + { + if(isset(self::getConfiguration()['federation']['security']['method_permissions'][$command])) + { + return (int)self::getConfiguration()['federation']['security']['method_permissions'][$command]; + } + + return match ($command) + { + Methods::CREATE_CLIENT => PermissionRole::ADMIN, + default => PermissionRole::GUEST, + }; + } + /** * Returns True if the cache system is enabled for FederationLib * and False if not, based on the configuration. @@ -223,4 +293,24 @@ return $results; } + + /** + * Returns the additional priority to add/remove if the connection is opened. + * + * @return int + */ + public static function getCacheOpenedConnectionPriority(): int + { + return (int)self::getConfiguration()['cache_system']['opened_connection_priority']; + } + + /** + * Returns additional priority to add/remove if the connection is closed. + * + * @return int + */ + public static function getCacheErrorConnectionPriority(): int + { + return (int)self::getConfiguration()['cache_system']['error_connection_priority']; + } } \ No newline at end of file diff --git a/src/FederationLib/Classes/Configuration/CacheServerConfiguration.php b/src/FederationLib/Classes/Configuration/CacheServerConfiguration.php index 3c880cd..3a51aa8 100644 --- a/src/FederationLib/Classes/Configuration/CacheServerConfiguration.php +++ b/src/FederationLib/Classes/Configuration/CacheServerConfiguration.php @@ -24,21 +24,11 @@ */ private ?int $port; - /** - * @var string|null - */ - private ?string $driver; - /** * @var int|null */ private ?int $priority; - /** - * @var string|null - */ - private ?string $username; - /** * @var string|null */ @@ -57,6 +47,7 @@ /** * CacheServerConfiguration constructor. * + * @param string $name * @param array $configuration */ public function __construct(string $name, array $configuration) @@ -65,9 +56,7 @@ $this->enabled = $configuration['enabled'] ?? false; $this->host = $configuration['host'] ?? null; $this->port = $configuration['port'] ?? null; - $this->driver = $configuration['driver'] ?? null; $this->priority = $configuration['priority'] ?? null; - $this->username = $configuration['username'] ?? null; $this->password = $configuration['password'] ?? null; $this->database = $configuration['database'] ?? null; $this->reconnect_interval = $configuration['reconnect_interval'] ?? null; @@ -76,7 +65,7 @@ /** * Returns the name of the cache server * - * @return string + * @return string|null */ public function getName(): ?string { @@ -88,7 +77,7 @@ * * @return bool */ - public function getEnabled(): bool + public function isEnabled(): bool { return $this->enabled ?? false; } @@ -111,14 +100,6 @@ return $this->port; } - /** - * @return string|null - */ - public function getDriver(): ?string - { - return $this->driver; - } - /** * @return int|null */ @@ -127,14 +108,6 @@ return $this->priority; } - /** - * @return string|null - */ - public function getUsername(): ?string - { - return $this->username; - } - /** * @return string|null */ diff --git a/src/FederationLib/Classes/Configuration/TamerLibConfiguration.php b/src/FederationLib/Classes/Configuration/TamerLibConfiguration.php new file mode 100644 index 0000000..8880075 --- /dev/null +++ b/src/FederationLib/Classes/Configuration/TamerLibConfiguration.php @@ -0,0 +1,71 @@ +cli_workers = $configuration['federation.tamer_lib.cli_workers'] ?? 8; + $this->node_workers = $configuration['federation.tamer_lib.node_workers'] ?? 20; + + $this->server_configuration = new ServerConfiguration( + $configuration['federation.tamer_lib.server.host'] ?? '127.0.0.1', + $configuration['federation.tamer_lib.server.port'] ?? 6379, + $configuration['federation.tamer_lib.server.password'] ?? null, + $configuration['federation.tamer_lib.server.database'] ?? 0 + ); + } + + /** + * Returns the total number of CLI workers to use. + * + * @return int|mixed + */ + public function getCliWorkers(): mixed + { + return $this->cli_workers; + } + + /** + * Returns the total number of Node workers to use. + * + * @return int|mixed + */ + public function getNodeWorkers(): mixed + { + return $this->node_workers; + } + + /** + * Returns the ServerConfiguration object for TamerLib. + * + * @return ServerConfiguration + */ + public function getServerConfiguration(): ServerConfiguration + { + return $this->server_configuration; + } + } \ No newline at end of file diff --git a/src/FederationLib/Classes/Database.php b/src/FederationLib/Classes/Database.php index 2d81f98..b1e19d4 100644 --- a/src/FederationLib/Classes/Database.php +++ b/src/FederationLib/Classes/Database.php @@ -36,7 +36,7 @@ { try { - Log::info('net.nosial.federationlib', sprintf('Connecting to the database: %s://%s@%s:%s/%s', Configuration::getDatabaseDriver(), Configuration::getDatabaseUsername(), Configuration::getDatabaseHost(), Configuration::getDatabasePort(), Configuration::getDatabaseName())); + Log::debug('net.nosial.federationlib', sprintf('Connecting to the database: %s://%s@%s:%s/%s', Configuration::getDatabaseDriver(), Configuration::getDatabaseUsername(), Configuration::getDatabaseHost(), Configuration::getDatabasePort(), Configuration::getDatabaseName())); $connection = DriverManager::getConnection([ 'driver' => Configuration::getDatabaseDriver(), 'host' => Configuration::getDatabaseHost(), @@ -63,7 +63,7 @@ /** @noinspection NestedPositiveIfStatementsInspection */ if(time() - self::$sql_last_connection_time > Configuration::getDatabaseReconnectInterval()) { - Log::info('net.nosial.federationlib', sprintf('Interval to reconnect to the %s server has been reached, reconnecting...', Configuration::getDatabaseDriver())); + Log::debug('net.nosial.federationlib', sprintf('Interval to reconnect to the %s server has been reached, reconnecting...', Configuration::getDatabaseDriver())); // Reconnect to the database. self::$sql_connection->close(); diff --git a/src/FederationLib/Classes/Memcached.php b/src/FederationLib/Classes/Memcached.php deleted file mode 100644 index 98bac1f..0000000 --- a/src/FederationLib/Classes/Memcached.php +++ /dev/null @@ -1,18 +0,0 @@ -configuration = $configuration; + $this->connection_error = false; + } + + /** + * Indicates if the redis server is available to connect to + * + * @return bool + */ + public function isAvailable(): bool + { + if(!$this->configuration->isEnabled()) + { + return false; + } + + return true; + } + + /** + * Determines if the redis server is connected. + * + * @return bool + */ + public function isConnected(): bool + { + if(!$this->configuration->isEnabled()) + { + return false; + } + + return $this->redis_connection !== null; + } + + /** + * @return bool + */ + public function isConnectionError(): bool + { + return $this->connection_error; + } + + /** + * Disconnects from the redis server if it's connected. + * + * @param bool $reset_error + * @return void + */ + public function disconnect(bool $reset_error=true): void + { + if(!$this->isConnected()) + { + return; + } + + try + { + $this->redis_connection->close(); + } + catch(Exception $e) + { + Log::warning(Misc::FEDERATIONLIB, sprintf('Failed to disconnect from redis server: %s', $e->getMessage())); + } + + if($reset_error) + { + $this->connection_error = false; + } + + $this->redis_connection = null; + } + + /** + * Establishes a connection to the redis server. + * + * @param bool $throw_exception + * @return void + * @throws CacheConnectionException + * @throws CacheDriverException + */ + public function connect(bool $throw_exception=true): void + { + if(!$this->configuration->isEnabled()) + { + if($throw_exception) + { + throw new CacheDriverException(sprintf('Failed to connect to the redis server \'%s\' because it\'s disabled.', $this->configuration->getName())); + } + } + + if($this->redis_connection !== null) + { + if($this->redis_last_connection_time === null) + { + return; + } + + if($this->redis_last_connection_time < (time() - $this->configuration->getReconnectInterval())) + { + Log::verbose(Misc::FEDERATIONLIB, sprintf('Interval limit of %s seconds to reconnect to the redis server \'%s\' has been reached, reconnecting...', $this->configuration->getReconnectInterval(), $this->configuration->getName())); + $this->disconnect(); + } + else + { + return; + } + } + + try + { + Log::info(Misc::FEDERATIONLIB, sprintf('Connecting to the redis server \'%s\': %s:%s', $this->configuration->getName(), $this->configuration->getHost(), $this->configuration->getPort())); + $redis = new \Redis(); + $redis->connect($this->configuration->getHost(), $this->configuration->getPort()); + if($this->configuration->getPassword() !== null) + { + $redis->auth($this->configuration->getPassword()); + } + $redis->select($this->configuration->getDatabase()); + } + catch(Exception $e) + { + $this->connection_error = true; + + if($throw_exception) + { + throw new CacheConnectionException(sprintf('Failed to connect to the redis server \'%s\': %s', $this->configuration->getName(), $e->getMessage()), $e->getCode(), $e); + } + + Log::warning(Misc::FEDERATIONLIB, sprintf('Failed to connect to the redis server \'%s\': %s', $this->configuration->getName(), $e->getMessage())); + return; + } + + $this->redis_connection = $redis; + $this->redis_last_connection_time = time(); + } /** * Returns/Establishes a connection to the redis server. * Returns null if redis is disabled. * + * @param bool $throw_exception * @return \Redis|null - * @throws RedisException + * @throws CacheConnectionException + * @throws CacheDriverException */ - public static function getConnection(): ?\Redis + public function getConnection(bool $throw_exception=true): ?\Redis { - if(!Configuration::isRedisEnabled()) + if(!$this->configuration->isEnabled()) { return null; } - if(self::$redis_connection === null) + if(!$this->isConnected()) { - try - { - Log::info('net.nosial.federationlib', sprintf('Connecting to the redis server: %s:%s', Configuration::getRedisHost(), Configuration::getRedisPort())); - $redis = new \Redis(); - $redis->connect(Configuration::getRedisHost(), Configuration::getRedisPort()); - if(Configuration::getRedisPassword() !== null) - { - $redis->auth(Configuration::getRedisPassword()); - } - $redis->select(Configuration::getRedisDatabase()); - } - catch(Exception $e) - { - throw new RedisException('Failed to connect to the redis server: ' . $e->getMessage(), $e->getCode(), $e); - } - - self::$redis_connection = $redis; - self::$redis_last_connection_time = time(); + $this->connect($throw_exception); } else { - if(self::$redis_last_connection_time === null || self::$redis_last_connection_time < (time() - Configuration::getRedisReconnectInterval())) + if($this->redis_last_connection_time < (time() - $this->configuration->getReconnectInterval())) { - Log::info('net.nosial.federationlib', 'Interval to reconnect to the redis server has been reached, reconnecting...'); - - try - { - self::$redis_connection->close(); - } - catch(Exception $e) - { - // Do nothing - unset($e); - } - - self::$redis_connection = null; - return self::getConnection(); + $this->connect($throw_exception); } } - return self::$redis_connection; + return $this->redis_connection; } /** @@ -84,8 +213,17 @@ * * @return int|null */ - public static function getRedisLastConnectionTime(): ?int + public function getRedisLastConnectionTime(): ?int { - return self::$redis_last_connection_time; + return $this->redis_last_connection_time; } + + /** + * @return CacheServerConfiguration + */ + public function getConfiguration(): CacheServerConfiguration + { + return $this->configuration; + } + } \ No newline at end of file diff --git a/src/FederationLib/Classes/Utilities.php b/src/FederationLib/Classes/Utilities.php index 69ff3f5..d861a07 100644 --- a/src/FederationLib/Classes/Utilities.php +++ b/src/FederationLib/Classes/Utilities.php @@ -4,6 +4,7 @@ namespace FederationLib\Classes; + use Exception; use FederationLib\Enums\SerializationMethod; use FederationLib\Interfaces\SerializableObjectInterface; use InvalidArgumentException; @@ -180,4 +181,34 @@ } return $outliers; } + + public static function weightedRandomPick( array $data): string + { + $totalWeight = array_sum($data); + if($totalWeight == 0) + { + throw new InvalidArgumentException('Total weight cannot be 0'); + } + + // Normalize weights to 0-1 + foreach ($data as $item => $weight) + { + $data[$item] = $weight / $totalWeight; + } + + // Generate a random number between 0 and 1 + $rand = mt_rand() / getrandmax(); + + // Select an item + $cumulativeWeight = 0.0; + foreach ($data as $item => $weight) + { + $cumulativeWeight += $weight; + + if ($rand < $cumulativeWeight) + { + return $item; + } + } + } } \ No newline at end of file diff --git a/src/FederationLib/Classes/Validate.php b/src/FederationLib/Classes/Validate.php index 9c13184..2ed75df 100644 --- a/src/FederationLib/Classes/Validate.php +++ b/src/FederationLib/Classes/Validate.php @@ -5,6 +5,7 @@ use FederationLib\Enums\Standard\PeerType; use FederationLib\Enums\Standard\InternetPeerType; use FederationLib\Enums\Standard\PeerAssociationType; + use FederationLib\Enums\Standard\PermissionRole; use FederationLib\Enums\Standard\UserPeerType; class Validate @@ -57,4 +58,48 @@ return false; } + + /** + * Validates a client name based on certain criteria. + * + * The client name must be alphanumeric, allowing spaces, periods, dashes, and underscores, + * with a minimum length of 3 characters and a maximum length of 42 characters. + * + * @param string $name The client name to validate + * @return bool Returns true if the client name is valid, false otherwise + */ + public static function clientName(string $name): bool + { + if (!preg_match('/^[a-zA-Z0-9\s\.\-_]+$/', $name)) + { + return false; + } + + $length = strlen($name); + return !($length < 3 || $length > 42); + } + + /** + * Validates a client description based on certain criteria. + * + * @param string $description The client description to validate + * @return bool Returns true if the client description is valid, false otherwise + */ + public static function clientDescription(string $description): bool + { + $length = strlen($description); + return !($length < 3 || $length > 255); + } + + /** + * Validates if the given permission role is valid. + * + * @param string|int $role + * @return bool + */ + public static function permissionRole(string|int $role): bool + { + return (int)$role >= 0 && (int)$role <= 5; + } + } \ No newline at end of file diff --git a/src/FederationLib/Enums/CommandApplets.php b/src/FederationLib/Enums/CommandApplets.php new file mode 100644 index 0000000..054e78d --- /dev/null +++ b/src/FederationLib/Enums/CommandApplets.php @@ -0,0 +1,22 @@ + 'root', + self::ADMIN => 'admin', + self::OPERATOR => 'operator', + self::AGENT => 'agent', + self::CLIENT => 'client', + self::GUEST => 'guest' + ]; + } \ No newline at end of file diff --git a/src/FederationLib/Exceptions/CacheConnectionException.php b/src/FederationLib/Exceptions/CacheConnectionException.php new file mode 100644 index 0000000..c4be109 --- /dev/null +++ b/src/FederationLib/Exceptions/CacheConnectionException.php @@ -0,0 +1,19 @@ +client_manager = new ClientManager($this); - $this->event_log_manager = new EventLogManager($this); } /** - * Returns the Client manager instance + * Registers functions to the TamerLib instance, if applicable * - * @return ClientManager + * @return void */ - public function getClientManager(): ClientManager + public function registerFunctions(): void { - return $this->client_manager; + if(tm::getMode() !== TamerMode::WORKER) + { + return; + } + + $this->client_manager->registerFunctions(); } /** - * @return EventLogManager + * Resolves the permission role from the given identity and attempts to check if the identity has the + * required permission to perform the given method + * + * @param ClientIdentity|null $identity + * @return ResolvedIdentity + * @throws AccessDeniedException + * @throws ClientNotFoundException + * @throws InternalServerException */ - public function getEventLogManager(): EventLogManager + private function resolveIdentity(?ClientIdentity $identity): ResolvedIdentity { - return $this->event_log_manager; + if($identity === null) + { + return new ResolvedIdentity(null, null, true); + } + + $get_client = tm::do('client_getClient', [$identity->getClientUuid()]); + $peer = null; + + try + { + $client = tm::waitFor($get_client); + } + catch(ClientNotFoundException $e) + { + tm::clear(); + throw new ClientNotFoundException('The client you are trying to access does not exist', $e); + } + catch(Exception|Throwable $e) + { + tm::clear(); + throw new InternalServerException('There was an error while trying to access the client', $e); + } + + tm::dof('client_updateLastSeen'); + return new ResolvedIdentity($client, $peer); } + + /** + * Checks if the given identity has the required permission to perform the given method + * + * @param string $method + * @param ResolvedIdentity $resolved_identity + * @return bool + */ + private function checkPermission(string $method, ResolvedIdentity $resolved_identity): bool + { + return $resolved_identity->getPermissionRole() <= Configuration::getMethodPermission($method); + } + + /** + * Pings the client + * + * @param ClientIdentity|null $identity + * @return bool + * @throws AccessDeniedException + * @throws ClientNotFoundException + * @throws InternalServerException + */ + public function ping(?ClientIdentity $identity): bool + { + if(!$this->checkPermission(Methods::PING, $this->resolveIdentity($identity))) + { + throw new Exceptions\Standard\AccessDeniedException('You do not have permission to perform this action'); + } + + return true; + } + + /** + * @param ClientIdentity|null $identity + * @return string + * @throws AccessDeniedException + * @throws ClientNotFoundException + * @throws InternalServerException + */ + public function whoami(?ClientIdentity $identity): string + { + $resolved_identity = $this->resolveIdentity($identity); + + if(!$this->checkPermission(Methods::WHOAMI, $resolved_identity)) + { + throw new Exceptions\Standard\AccessDeniedException('You do not have permission to perform this action'); + } + + if($resolved_identity->getPeer() !== null) + { + return $resolved_identity->getPeer()->getFederatedAddress(); + } + + if($resolved_identity->getClient() !== null) + { + return $resolved_identity->getClient()->getUuid(); + } + + return 'root'; + } + + /** + * Registers a new client into the database + * + * @param ClientIdentity|null $identity + * @param string|null $name + * @param string|null $description + * @return string + * @throws AccessDeniedException + * @throws ClientNotFoundException + * @throws DatabaseException + * @throws InternalServerException + * @throws InvalidClientDescriptionException + * @throws InvalidClientNameException + */ + public function createClient(?ClientIdentity $identity, ?string $name=null, ?string $description=null): string + { + if(!$this->checkPermission(Methods::CREATE_CLIENT, $this->resolveIdentity($identity))) + { + throw new Exceptions\Standard\AccessDeniedException('You do not have sufficient permission to create a client'); + } + + try + { + return $this->client_manager->registerClient($name, $description); + } + catch(Exception $e) + { + if(in_array($e->getCode(), ErrorCodes::ALL, true)) + { + throw $e; + } + + throw new Exceptions\Standard\InternalServerException('There was an error while creating the client', $e); + } + } + + /** + * Returns an existing client from the database + * + * @param ClientIdentity|null $identity + * @param string|Client $client_uuid + * @throws AccessDeniedException + * @throws ClientNotFoundException + * @throws DatabaseException + * @throws InternalServerException + * @return Objects\Standard\Client + */ + public function getClient(?ClientIdentity $identity, string|Client $client_uuid): Objects\Standard\Client + { + if(!$this->checkPermission(Methods::GET_CLIENT, $this->resolveIdentity($identity))) + { + throw new Exceptions\Standard\AccessDeniedException('You do not have sufficient permission to fetch a client from the database'); + } + + try + { + // Return the standard client object + return new Objects\Standard\Client($this->client_manager->getClient($client_uuid)); + } + catch(Exception $e) + { + if(in_array($e->getCode(), ErrorCodes::ALL, true)) + { + throw $e; + } + + throw new Exceptions\Standard\InternalServerException('There was an error while getting the client', $e); + } + } + + /** + * Updates the name of an existing client, return True if successful + * + * @param ClientIdentity|null $identity + * @param string $client_uuid + * @param string $new_name + * @return bool + * @throws AccessDeniedException + * @throws ClientNotFoundException + * @throws DatabaseException + * @throws InternalServerException + * @throws InvalidClientNameException + */ + public function changeClientName(?ClientIdentity $identity, string $client_uuid, string $new_name): bool + { + if(!$this->checkPermission(Methods::CHANGE_CLIENT_NAME, $this->resolveIdentity($identity))) + { + throw new Exceptions\Standard\AccessDeniedException('You do not have sufficient permission to change the name of a client'); + } + + try + { + $this->client_manager->changeClientName($client_uuid, $new_name); + } + catch(Exception $e) + { + if(in_array($e->getCode(), ErrorCodes::ALL, true)) + { + throw $e; + } + + throw new Exceptions\Standard\InternalServerException('There was an error while changing the client name', $e); + } + + return true; + } + + /** + * @return string + */ + public static function getSubprocessorPath(): string + { + return __DIR__ . DIRECTORY_SEPARATOR . 'subproc'; + } + } \ No newline at end of file diff --git a/src/FederationLib/Interfaces/CacheDriverInterface.php b/src/FederationLib/Interfaces/CacheDriverInterface.php index 50e5503..bc8e194 100644 --- a/src/FederationLib/Interfaces/CacheDriverInterface.php +++ b/src/FederationLib/Interfaces/CacheDriverInterface.php @@ -3,8 +3,8 @@ namespace FederationLib\Interfaces; use FederationLib\Classes\Configuration\CacheServerConfiguration; - use FederationLib\Classes\Memcached; - use FederationLib\Classes\Redis; + use Memcached; + use Redis; interface CacheDriverInterface { @@ -29,6 +29,108 @@ */ public function getConnection(): Redis|Memcached; + /** + * Returns the values of the specified key + * + * For every key that does not hold a string value or does not exist, the special value false is returned. + * Because of this, the operation never fails. + * + * @param string $key + * @return mixed + */ + public function get(string $key): mixed; + + /** + * Gets a value from the hash stored at key. If the hash table doesn't exist, + * or the key doesn't exist, FALSE is returned + * + * @param string $key + * @param string $field + * @return mixed + */ + public function hGet(string $key, string $hashKey): mixed; + + /** + * Returns the whole hash, as an array of strings indexed by string + * + * @param string $key + * @return array + */ + public function hGetAll(string $key): array; + + /** + * Sets a value in the cache server + * + * @param string $key + * @param mixed $value + * @return void + */ + public function set(string $key, mixed $value): void; + + /** + * Fills in a whole hash. Non-string values are converted to string, using the standard (string) cast. + * NULL values are stored as empty string + * + * @param string $key + * @param array $values + * @return void + */ + public function hMSet(string $key, array $values): void; + + /** + * Adds a value to the hash stored at key. If this value is already in the hash, FALSE is returned. + * + * @param string $key + * @param string $hash_key + * @param mixed $value + * @return void + */ + public function hSet(string $key, string $hash_key, mixed $value): void; + + /** + * Verify if the specified key exists + * + * @param string $key + * @return bool + */ + public function exists($key): bool; + + /** + * Remove specified keys + * + * @param $key1 + * @param mixed ...$other_keys + * @return void + */ + public function delete($key1, ...$other_keys): void; + + /** + * Sets a key to expire in a certain amount of seconds + * + * @param string $key + * @param int $seconds + * @return void + */ + public function expire(string $key, int $seconds): void; + + /** + * Determines if the cache server is available (does not necessarily mean that the server is connected) + * This is useful for checking if the cache server is available before attempting to use it + * + * @return bool + */ + public function isAvailable(): bool; + + /** + * Determines if the cache server is currently connected + * If the ping parameter is set to true, this will ping the server to ensure that it is connected + * otherwise it will assume the connection is stable if a connection has been established + * + * @param bool $ping This will ping the server to check if it is connected + * @return bool + */ + public function isConnected(bool $ping=false): bool; + /** * Connects to the cache server * @@ -43,5 +145,4 @@ */ public function disconnect(): void; - } \ No newline at end of file diff --git a/src/FederationLib/Interfaces/CommandAppletInterface.php b/src/FederationLib/Interfaces/CommandAppletInterface.php new file mode 100644 index 0000000..840e56f --- /dev/null +++ b/src/FederationLib/Interfaces/CommandAppletInterface.php @@ -0,0 +1,24 @@ +federationLib = $federationLib; } + public function registerFunctions(): void + { + if(tm::getMode() !== TamerMode::WORKER) + { + return; + } + + tm::addFunction('client_registerClient', [$this, 'registerClient']); + tm::addFunction('client_getClient', [$this, 'getClient']); + tm::addFunction('client_changeClientName', [$this, 'changeClientName']); + tm::addFunction('client_changeClientDescription', [$this, 'changeClientDescription']); + tm::addFunction('client_changeClientPermissionRole', [$this, 'changeClientPermissionRole']); + tm::addFunction('client_updateClient', [$this, 'updateClient']); + tm::addFunction('client_updateLastSeen', [$this, 'updateLastSeen']); + tm::addFunction('client_listClients', [$this, 'listClients']); + tm::addFunction('client_getTotalClients', [$this, 'getTotalClients']); + tm::addFunction('client_getTotalPages', [$this, 'getTotalPages']); + tm::addFunction('client_deleteClient', [$this, 'deleteClient']); + } + /** * Registers a client into the database, returns the UUID that was generated for the client. * - * @param Client $client + * @param string|null $name + * @param string|null $description * @return string * @throws DatabaseException + * @throws InvalidClientDescriptionException + * @throws InvalidClientNameException */ - public function registerClient(Client $client): string + public function registerClient(?string $name=null, ?string $description=null): string { $qb = Database::getConnection()->createQueryBuilder(); $qb->insert(DatabaseTables::CLIENTS); $uuid = Uuid::v4()->toRfc4122(); - foreach($client->toArray() as $key => $value) + if($name === null) { - switch($key) + $name = Utilities::generateName(4); + } + else + { + if(!Validate::clientName($name)) { - case 'id': - $qb->setValue($key, ':' . $key); - $qb->setParameter($key, $uuid); - break; - - case 'name': - if($value === null || strlen($value) === 0 || !preg_match('/^[a-zA-Z0-9_\-]+$/', $value )) - { - $value = Utilities::generateName(4); - Log::debug('net.nosial.federationlib', sprintf('generated name for client: %s', $value)); - } - - $qb->setValue($key, ':' . $key); - $qb->setParameter($key, substr($value, 0, 64)); - break; - - case 'description': - if($value !== null) - { - $qb->setValue($key, ':' . $key); - $qb->setParameter($key, substr($value, 0, 255)); - } - break; - - case 'enabled': - $qb->setValue($key, ':' . $key); - $qb->setParameter($key, $value ? 1 : 0); - break; - - case 'seen_timestamp': - case 'updated_timestamp': - case 'created_timestamp': - $qb->setValue($key, ':' . $key); - $qb->setParameter($key, time()); - break; - - default: - $qb->setValue($key, ':' . $key); - $qb->setParameter($key, $value); - break; + throw new InvalidClientNameException(sprintf('Invalid client name: %s', $name)); } } + if($description !== null && strlen($description) > 128) + { + throw new InvalidClientDescriptionException(sprintf('Invalid client description: %s', $description)); + } + + $qb->setValue('uuid', $qb->createNamedParameter($uuid)); + $qb->setValue('name', $qb->createNamedParameter($name)); + $qb->setValue('description', $qb->createNamedParameter($description, (is_null($description) ? ParameterType::NULL : ParameterType::STRING))); + $qb->setValue('secret_totp', $qb->createNamedParameter(Security::generateSecret())); + $qb->setValue('flags', $qb->createNamedParameter(null, ParameterType::NULL)); + try { $qb->executeStatement(); @@ -105,11 +110,6 @@ throw new DatabaseException('Failed to register client: ' . $e->getMessage(), $e); } - $this->federationLib->getEventLogManager()->logEvent( - EventCode::CLIENT_CREATED, EventPriority::LOW, null, - sprintf('Registered client with UUID %s', $uuid) - ); - Log::info('net.nosial.federationlib', sprintf('Registered client with UUID %s', $uuid)); return $uuid; } @@ -117,38 +117,24 @@ /** * Returns an existing client from the database. * - * @param string|Client $uuid + * @param string|Client $client_uuid * @return Client * @throws ClientNotFoundException * @throws DatabaseException */ - public function getClient(string|Client $uuid): Client + public function getClient(string|Client $client_uuid): Client { - if($uuid instanceof Client) + if($client_uuid instanceof Client) { - $uuid = $uuid->getUuid(); - } - - if(Configuration::isRedisCacheClientObjectsEnabled()) - { - try - { - if(Redis::getConnection()?->exists(sprintf('Client<%s>', $uuid))) - { - return Client::fromArray(Redis::getConnection()?->hGetAll(sprintf('Client<%s>', $uuid))); - } - } - catch(Exception $e) - { - Log::warning('net.nosial.federationlib', sprintf('Failed to get Client from redis: %s', $e->getMessage())); - } + $client_uuid = $client_uuid->getUuid(); } $qb = Database::getConnection()->createQueryBuilder(); $qb->select('*'); $qb->from(DatabaseTables::CLIENTS); $qb->where('uuid = :uuid'); - $qb->setParameter('uuid', $uuid); + $qb->setParameter('uuid', $client_uuid); + $qb->setMaxResults(1); try { @@ -156,7 +142,7 @@ if($result->rowCount() === 0) { - throw new ClientNotFoundException($uuid); + throw new ClientNotFoundException($client_uuid); } $client = Client::fromArray($result->fetchAssociative()); @@ -170,22 +156,166 @@ throw new DatabaseException('Failed to get Client: ' . $e->getMessage(), $e); } - if(Configuration::isRedisCacheClientObjectsEnabled()) + return $client; + } + + /** + * Changes the name of a client. + * + * @param string|Client $client_uuid + * @param string|null $name + * @return void + * @throws ClientNotFoundException + * @throws DatabaseException + * @throws InvalidClientNameException + */ + public function changeClientName(string|Client $client_uuid, ?string $name=null): void + { + if($client_uuid instanceof Client) { - try + $client_uuid = $client_uuid->getUuid(); + } + + if($name === null) + { + $name = Utilities::generateName(4); + } + else + { + if(!Validate::clientName($name)) { - Redis::getConnection()?->hMSet((string)$client, $client->toArray()); - Redis::getConnection()?->expire((string)$client, Configuration::getRedisCacheClientObjectsTTL()); - } - catch(Exception $e) - { - Log::warning('net.nosial.federationlib', sprintf('Failed to cache Client in redis: %s', $e->getMessage())); + throw new InvalidClientNameException(sprintf('Invalid client name: %s', $name)); } } - return $client; + $qb = Database::getConnection()->createQueryBuilder(); + $qb->update(DatabaseTables::CLIENTS); + $qb->set('name', ':name'); + $qb->setParameter('name', $name); + $qb->set('updated_timestamp', ':updated_timestamp'); + $qb->setParameter('updated_timestamp', time(), ParameterType::INTEGER); + $qb->where('uuid = :uuid'); + $qb->setParameter('uuid', $client_uuid); + $qb->setMaxResults(1); + + try + { + $affected_rows = $qb->executeStatement(); + } + catch(Exception $e) + { + throw new DatabaseException('Failed to change client name: ' . $e->getMessage(), $e); + } + + if($affected_rows === 0) + { + throw new ClientNotFoundException($client_uuid); + } + + Log::verbose('net.nosial.federationlib', sprintf('Changed client name for client %s to %s', $client_uuid, $name)); } + /** + * Changes the description of a client + * + * @param string|Client $client_uuid + * @param string|null $description + * @return void + * @throws ClientNotFoundException + * @throws DatabaseException + * @throws InvalidClientDescriptionException + */ + public function changeClientDescription(string|Client $client_uuid, ?string $description=null): void + { + if($client_uuid instanceof Client) + { + $client_uuid = $client_uuid->getUuid(); + } + + if($description !== null && strlen($description) > 128) + { + throw new InvalidClientDescriptionException(sprintf('Invalid client description: %s', $description)); + } + + $qb = Database::getConnection()->createQueryBuilder(); + $qb->update(DatabaseTables::CLIENTS); + $qb->set('description', ':description'); + $qb->setParameter('description', $description, (is_null($description) ? ParameterType::NULL : ParameterType::STRING)); + $qb->set('updated_timestamp', ':updated_timestamp'); + $qb->setParameter('updated_timestamp', time(), ParameterType::INTEGER); + $qb->where('uuid = :uuid'); + $qb->setParameter('uuid', $client_uuid); + $qb->setMaxResults(1); + + try + { + $affected_rows = $qb->executeStatement(); + } + catch(Exception $e) + { + throw new DatabaseException('Failed to change client description: ' . $e->getMessage(), $e); + } + + if($affected_rows === 0) + { + throw new ClientNotFoundException($client_uuid); + } + + Log::verbose('net.nosial.federationlib', sprintf('Changed client description for client %s to %s', $client_uuid, $description)); + } + + /** + * Updates the permission role of a client. + * + * @param string|Client $client_uuid + * @param int $permission_role + * @return void + * @throws ClientNotFoundException + * @throws DatabaseException + * @throws InvalidPermissionRoleException + */ + public function changeClientPermissionRole(string|Client $client_uuid, int $permission_role): void + { + if($client_uuid instanceof Client) + { + $client_uuid = $client_uuid->getUuid(); + } + + if(!Validate::permissionRole($permission_role)) + { + throw new InvalidPermissionRoleException(sprintf('Invalid permission role: %s', $permission_role)); + } + + $time = time(); + + $qb = Database::getConnection()->createQueryBuilder(); + $qb->update(DatabaseTables::CLIENTS); + $qb->set('permission_role', ':permission_role'); + $qb->setParameter('permission_role', $permission_role, ParameterType::INTEGER); + $qb->set('updated_timestamp', ':updated_timestamp'); + $qb->setParameter('updated_timestamp', $time, ParameterType::INTEGER); + $qb->where('uuid = :uuid'); + $qb->setParameter('uuid', $client_uuid); + $qb->setMaxResults(1); + + try + { + $affected_rows = $qb->executeStatement(); + } + catch(Exception $e) + { + throw new DatabaseException('Failed to change client permission role: ' . $e->getMessage(), $e); + } + + if($affected_rows === 0) + { + throw new ClientNotFoundException($client_uuid); + } + + Log::verbose('net.nosial.federationlib', sprintf('Changed client permission role for client %s to %s', $client_uuid, $permission_role)); + } + + /** * Updates a client record in the database, if the client does not exist it will be created. * This function is cache aware, if the client is cached it will only update the changed values. diff --git a/src/FederationLib/Managers/PeerManager.php b/src/FederationLib/Managers/PeerManager.php index 29e906d..ea8af86 100644 --- a/src/FederationLib/Managers/PeerManager.php +++ b/src/FederationLib/Managers/PeerManager.php @@ -2,171 +2,22 @@ namespace FederationLib\Managers; - use Doctrine\DBAL\ParameterType; - use Exception; - use FederationLib\Classes\Configuration; - use FederationLib\Classes\Database; - use FederationLib\Enums\DatabaseTables; - use FederationLib\Enums\Misc; - use FederationLib\Enums\Standard\InternetPeerType; - use FederationLib\Enums\Standard\PeerType; - use FederationLib\Enums\Standard\UserPeerType; - use FederationLib\Exceptions\DatabaseException; - use FederationLib\Exceptions\Standard\PeerNotFoundException; - use FederationLib\Exceptions\Standard\UnsupportedPeerType; use FederationLib\FederationLib; - use FederationLib\Objects\Client; - use FederationLib\Objects\ParsedFederatedAddress; - use FederationLib\Objects\Peer; - use LogLib\Log; class PeerManager { /** * @var FederationLib */ - private $federationLib; + private FederationLib $federationLib; /** + * PeerManager constructor. + * * @param FederationLib $federationLib */ public function __construct(FederationLib $federationLib) { $this->federationLib = $federationLib; } - - /** - * Returns the Peer Type of the federated address, returns "unknown" if the - * type is not supported by the server - * - * @param string $type - * @return string - */ - private function getPeerType(string $type): string - { - if(in_array(strtolower($type), InternetPeerType::ALL)) - return PeerType::INTERNET; - - if(in_array(strtolower($type), UserPeerType::ALL)) - return PeerType::USER; - - return PeerType::UNKNOWN; - } - - /** - * Parses a raw federated address and returns a ParsedFederatedAddress object - * - * @param string $federated_address - * @return ParsedFederatedAddress - * @throws UnsupportedPeerType - */ - private function parseAddress(string $federated_address): ParsedFederatedAddress - { - $parsed_address = new ParsedFederatedAddress($federated_address); - - if($this->getPeerType($parsed_address->getPeerType()) === PeerType::UNKNOWN) - { - throw new UnsupportedPeerType($parsed_address->getPeerType()); - } - - return $parsed_address; - } - - /** - * Registers a new peer into the database - * - * @param string|Client $client_uuid - * @param string $federated_address - * @return void - * @throws DatabaseException - * @throws UnsupportedPeerType - */ - public function registerPeer(string|Client $client_uuid, string $federated_address): void - { - // If the client_uuid is a Client object, get the UUID from it - if ($client_uuid instanceof Client) - { - $client_uuid = $client_uuid->getUuid(); - } - - // Check if the peer type is supported by the server - $parsed_address = $this->parseAddress($federated_address); - - try - { - // Generate a query to insert the peer into the database - $query_builder = Database::getConnection()->createQueryBuilder(); - $query_builder->insert(DatabaseTables::PEERS); - $timestamp = time(); - - $query_builder->values([ - 'federated_address' => $query_builder->createNamedParameter($parsed_address->getAddress()), - 'client_first_seen' => $query_builder->createNamedParameter($client_uuid), - 'client_last_seen' => $query_builder->createNamedParameter($client_uuid), - 'active_restriction' => $query_builder->createNamedParameter(null, ParameterType::NULL), - 'discovered_timestamp' => $query_builder->createNamedParameter($timestamp, ParameterType::INTEGER), - 'seen_timestamp' => $query_builder->createNamedParameter($timestamp, ParameterType::INTEGER), - ]); - - $query_builder->executeStatement(); - } - catch(Exception $e) - { - throw new DatabaseException(sprintf('Failed to register peer %s: %s', $parsed_address->getAddress(), $e->getMessage()), $e); - } - - Log::info(Misc::FEDERATIONLIB, sprintf('Registered new peer: %s', $parsed_address->getAddress())); - } - - public function cachePeerObject(Peer $peer): void - { - if(!Configuration::isRedisEnabled() && !Configuration::isPeerObjectsCached()) - { - return; - } - - - } - - /** - * Fetches a peer from the database by its federated address - * - * @param string $federated_address - * @return Peer - * @throws DatabaseException - * @throws PeerNotFoundException - * @throws UnsupportedPeerType - */ - public function getPeer(string $federated_address): Peer - { - // Check if the peer type is supported by the server - $parsed_address = $this->parseAddress($federated_address); - - try - { - $query_builder = Database::getConnection()->createQueryBuilder(); - $query_builder->select('*'); - $query_builder->from(DatabaseTables::PEERS); - $query_builder->where('federated_address = :federated_address'); - $query_builder->setParameter('federated_address', $parsed_address->getAddress()); - $query_builder->setMaxResults(1); - - $result = $query_builder->executeQuery(); - - if($result->rowCount() === 0) - { - throw new PeerNotFoundException($parsed_address->getAddress()); - } - - return Peer::fromArray($result->fetchAssociative()); - } - catch(PeerNotFoundException $e) - { - throw $e; - } - catch(Exception $e) - { - throw new DatabaseException(sprintf('Failed to get peer %s: %s', $parsed_address->getAddress(), $e->getMessage()), $e); - } - } } \ No newline at end of file diff --git a/src/FederationLib/Managers/RedisConnectionManager.php b/src/FederationLib/Managers/RedisConnectionManager.php new file mode 100644 index 0000000..ed2c916 --- /dev/null +++ b/src/FederationLib/Managers/RedisConnectionManager.php @@ -0,0 +1,177 @@ +getName()] = new Redis($configuration); + } + } + + // If the name isn't null or "any", return the connection with that name + if($name !== null || strtolower($name) !== 'any') + { + if(!isset(self::$connections[$name])) + { + if($fallback !== null) + { + return self::getConnection($fallback); + } + + throw new InvalidArgumentException("Redis connection with name '$name' not found"); + } + + if(!self::$connections[$name]->isAvailable()) + { + if($fallback !== null) + { + return self::getConnection($fallback); + } + + throw new CacheConnectionException("Redis connection with name '$name' is not available"); + } + + try + { + return self::$connections[$name]->getConnection(); + } + catch(Exception $e) + { + if($fallback !== null) + { + return self::getConnection($fallback); + } + + throw new CacheConnectionException(sprintf("Failed to retrieve the connection for \'%s\'", $name), 0, $e); + } + } + + // Assuming we're here, we're looking for any connection + + // Build the weights array + $weights = []; + /** @var Redis $connection */ + foreach(self::$connections as $connection) + { + if($connection->isAvailable()) + { + $priority = $connection->getConfiguration()->getPriority(); + + // Calculate the priority based on the connection state and the configuration + if(Configuration::getCacheErrorConnectionPriority() !== 0 && $connection->isConnectionError()) + { + $priority = ((Configuration::getCacheErrorConnectionPriority()) + ($priority)); + } + elseif(Configuration::getCacheOpenedConnectionPriority() !== 0 && $connection->isConnected()) + { + $priority = ((Configuration::getCacheOpenedConnectionPriority()) + ($priority)); + } + + if((int)$priority > 100) + { + $priority = 100; + } + elseif((int)$priority < 0) + { + $priority = 0; + } + + $weights[$connection->getConfiguration()->getName()] = (int)$priority; + } + } + + if(count($weights) === 0) + { + // If there are no available connections, this may be based off of the configuration + // In this case, resort to the default. + /** @var Redis $connection */ + foreach(self::$connections as $connection) + { + if($connection->isAvailable()) + { + $weights[$connection->getConfiguration()->getName()] = $connection->getConfiguration()->getPriority(); + } + } + + // If there are still no available connections, throw an exception + // It's clearly the user's fault at this point lol + if (count($weights) === 0) + { + if($fallback !== null) + { + return self::getConnection($fallback); + } + + throw new CacheConnectionException("No available Redis connections"); + } + + if(count($weights) === 1) + { + // If there's only one available connection, just use that lmao ez + return self::$connections[array_key_first($weights)]->getConnection(); + } + } + elseif(count($weights) === 1) + { + // Same as above + return self::$connections[array_key_first($weights)]->getConnection(); + } + + $selected_connection = Utilities::weightedRandomPick($weights); + + try + { + return self::$connections[$selected_connection]->getConnection(); + } + catch(Exception $e) + { + if($fallback !== null) + { + // After all that, at least we have a fallback + return self::getConnection($fallback); + } + + } + finally + { + // Or not :( + throw new CacheConnectionException(sprintf("Failed to retrieve the connection for \'%s\'", $selected_connection), 0, $e); + } + + // Voila! je suis fier de ça + } + } \ No newline at end of file diff --git a/src/FederationLib/Objects/Client.php b/src/FederationLib/Objects/Client.php index 2ec4897..01e5bb7 100755 --- a/src/FederationLib/Objects/Client.php +++ b/src/FederationLib/Objects/Client.php @@ -6,6 +6,7 @@ use FederationCLI\Utilities; use FederationLib\Classes\Security; + use FederationLib\Enums\Standard\PermissionRole; use FederationLib\Interfaces\SerializableObjectInterface; class Client implements SerializableObjectInterface @@ -37,13 +38,9 @@ /** * @var int + * @see PermissionRole */ - private $query_permission; - - /** - * @var int - */ - private $update_permission; + private $permission_role; /** * @var string[] @@ -72,8 +69,7 @@ { $this->enabled = true; $this->flags = []; - $this->query_permission = 1; - $this->update_permission = 0; + $this->permission_role = PermissionRole::CLIENT; } /** @@ -95,15 +91,9 @@ } /** - * @param bool $enabled - */ - public function setEnabled(bool $enabled): void - { - $this->enabled = $enabled; - } - - /** - * @return string|null + * Returns the client's name. + * + * @return string */ public function getName(): string { @@ -111,14 +101,8 @@ } /** - * @param string|null $name - */ - public function setName(?string $name): void - { - $this->name = $name; - } - - /** + * Optional. Returns the client's description. + * * @return string|null */ public function getDescription(): ?string @@ -127,32 +111,13 @@ } /** - * @param string|null $description - */ - public function setDescription(?string $description): void - { - $this->description = $description; - } - - /** - * Enables authentication for the client, returns the secret. + * Indicates whether or not the client requires authentication. * - * @return string + * @return bool */ - public function enableAuthentication(): string + public function requiresAuthentication(): bool { - $this->secret_totp = Security::generateSecret(); - return $this->secret_totp; - } - - /** - * Disables authentication for the client, wipes the secret. - * - * @return void - */ - public function disableAuthentication(): void - { - $this->secret_totp = null; + return $this->secret_totp !== null; } /** @@ -185,39 +150,9 @@ * * @return int */ - public function getQueryPermission(): int + public function getPermissionRole(): int { - return $this->query_permission; - } - - /** - * Sets the client's query permission level. - * - * @param int $query_permission - */ - public function setQueryPermission(int $query_permission): void - { - $this->query_permission = $query_permission; - } - - /** - * Returns the client's update permission level. - * - * @return int - */ - public function getUpdatePermission(): int - { - return $this->update_permission; - } - - /** - * Sets the client's update permission. - * - * @param int $update_permission - */ - public function setUpdatePermission(int $update_permission): void - { - $this->update_permission = $update_permission; + return $this->permission_role; } /** @@ -230,42 +165,6 @@ return $this->flags; } - /** - * Sets an array of flags for the client. - * This function overrides any existing flags. - * - * @param string[] $flags - */ - public function setFlags(array $flags): void - { - $this->flags = $flags; - } - - /** - * Appends a flag to the client's flags. - * - * @param string $flag - * @return void - */ - public function appendFlag(string $flag): void - { - if(!in_array($flag, $this->flags)) - { - $this->flags[] = $flag; - } - } - - /** - * Removes a flag from the client's flags. - * - * @param string $flag - * @return void - */ - public function removeFlag(string $flag): void - { - $this->flags = array_diff($this->flags, [$flag]); - } - /** * Returns True if the client has the given flag. * @@ -327,8 +226,7 @@ 'name' => $this->name, 'description' => $this->description, 'secret_totp' => $this->secret_totp, - 'query_permission' => $this->query_permission, - 'update_permission' => $this->update_permission, + 'permission_role' => $this->permission_role, 'flags' => $flags, 'created_timestamp' => $this->created_timestamp, 'updated_timestamp' => $this->updated_timestamp, @@ -351,8 +249,7 @@ $client->name = $array['name'] ?? null; $client->description = $array['description'] ?? null; $client->secret_totp = $array['secret_totp'] ?? null; - $client->query_permission = $array['query_permission'] ?? 0; - $client->update_permission = $array['update_permission'] ?? 0; + $client->permission_role = $array['permission_role'] ?? PermissionRole::CLIENT; if(isset($array['flags'])) { $client->flags = explode(',', $array['flags']); diff --git a/src/FederationLib/Objects/InvokeResults.php b/src/FederationLib/Objects/InvokeResults.php new file mode 100644 index 0000000..441ff45 --- /dev/null +++ b/src/FederationLib/Objects/InvokeResults.php @@ -0,0 +1,50 @@ +exit_code = $exit_code; + $this->output = $output; + } + + /** + * Returns the exit code of the command. + * + * @return int + */ + public function getExitCode(): int + { + return $this->exit_code; + } + + /** + * Returns the output of the command. + * + * @return string + */ + public function getOutput(): string + { + return $this->output; + } + } \ No newline at end of file diff --git a/src/FederationLib/Objects/ParsedFederatedAddress.php b/src/FederationLib/Objects/ParsedFederatedAddress.php index 0388d96..f10f275 100644 --- a/src/FederationLib/Objects/ParsedFederatedAddress.php +++ b/src/FederationLib/Objects/ParsedFederatedAddress.php @@ -4,6 +4,8 @@ namespace FederationLib\Objects; + use InvalidArgumentException; + class ParsedFederatedAddress { /** @@ -30,6 +32,12 @@ { preg_match("/(?[a-z0-9]+)\.(?[a-z0-9]+):(?.+)/", $federated_address, $matches); + // Validate the federated address + if (empty($matches)) + { + throw new InvalidArgumentException('Invalid Federated Address'); + } + $this->source = $matches['source']; $this->peer_type = $matches['type']; $this->unique_identifier = $matches['id']; diff --git a/src/FederationLib/Objects/ResolvedIdentity.php b/src/FederationLib/Objects/ResolvedIdentity.php new file mode 100644 index 0000000..38c8cba --- /dev/null +++ b/src/FederationLib/Objects/ResolvedIdentity.php @@ -0,0 +1,98 @@ +client = $client; + $this->peer = $peer; + + if($this->client === null) + { + if(!$allow_root) + { + throw new AccessDeniedException('Missing Client Identity'); + } + + $this->permission_role = PermissionRole::ROOT; + return; + } + + if(!Configuration::strictPermissionEnabled()) + { + if($this->peer === null) + { + $this->permission_role = $this->client->getPermissionRole(); + return; + } + + $this->permission_role = $this->peer->getPermissionRole(); + return; + } + + if($this->peer === null) + { + $this->permission_role = $this->client->getPermissionRole(); + return; + } + + if($this->client->getPermissionRole() > $this->peer->getPermissionRole()) + { + $this->permission_role = $this->client->getPermissionRole(); + return; + } + + $this->permission_role = $this->peer->getPermissionRole(); + } + + /** + * @return Client|null + */ + public function getClient(): ?Client + { + return $this->client; + } + + /** + * @return Peer|null + */ + public function getPeer(): ?Peer + { + return $this->peer; + } + + /** + * @return int + */ + public function getPermissionRole(): int + { + return $this->permission_role; + } + } \ No newline at end of file diff --git a/src/FederationLib/Objects/Standard/Client.php b/src/FederationLib/Objects/Standard/Client.php new file mode 100644 index 0000000..f34f310 --- /dev/null +++ b/src/FederationLib/Objects/Standard/Client.php @@ -0,0 +1,173 @@ +uuid = $client->getUuid(); + $this->enabled = $client->isEnabled(); + $this->name = $client->getName(); + $this->description = $client->getDescription(); + $this->permission_role = $client->getPermissionRole(); + $this->created_timestamp = $client->getCreatedTimestamp(); + $this->updated_timestamp = $client->getUpdatedTimestamp(); + $this->seen_timestamp = $client->getSeenTimestamp(); + } + + /** + * @return string + */ + public function getUuid(): string + { + return $this->uuid; + } + + /** + * @return bool + */ + public function isEnabled(): bool + { + return $this->enabled; + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @return string|null + */ + public function getDescription(): ?string + { + return $this->description; + } + + /** + * @return int + */ + public function getPermissionRole(): int + { + return $this->permission_role; + } + + /** + * @return int + */ + public function getCreatedTimestamp(): int + { + return $this->created_timestamp; + } + + /** + * @return int + */ + public function getUpdatedTimestamp(): int + { + return $this->updated_timestamp; + } + + /** + * @return int + */ + public function getSeenTimestamp(): int + { + return $this->seen_timestamp; + } + + /** + * Returns an array representation of the object + * + * @return array + */ + public function toArray(): array + { + return [ + 'uuid' => $this->uuid, + 'enabled' => $this->enabled, + 'name' => $this->name, + 'description' => $this->description, + 'created_timestamp' => $this->created_timestamp, + 'updated_timestamp' => $this->updated_timestamp, + 'seen_timestamp' => $this->seen_timestamp + ]; + } + + /** + * Constructs an object from an array representation + * + * @param array $array + * @return SerializableObjectInterface + */ + public static function fromArray(array $array): SerializableObjectInterface + { + $object = new self(); + + $object->uuid = $array['uuid']; + $object->enabled = $array['enabled']; + $object->name = $array['name']; + $object->description = $array['description']; + $object->created_timestamp = $array['created_timestamp']; + $object->updated_timestamp = $array['updated_timestamp']; + $object->seen_timestamp = $array['seen_timestamp']; + + return $object; + } + } \ No newline at end of file diff --git a/src/FederationLib/Objects/Standard/ClientIdentity.php b/src/FederationLib/Objects/Standard/ClientIdentity.php new file mode 100644 index 0000000..2b014df --- /dev/null +++ b/src/FederationLib/Objects/Standard/ClientIdentity.php @@ -0,0 +1,84 @@ +client_uuid; + } + + /** + * Optional. Returns the client TOTP signature + * + * @return string|null + */ + public function getClientTotpSignature(): ?string + { + return $this->client_totp_signature; + } + + /** + * Optional. Returns the peer + * + * @return string|null + */ + public function getPeer(): ?string + { + return $this->peer; + } + + /** + * Returns an array representation of the object + * + * @return array + */ + public function toArray(): array + { + return [ + 'client_uuid' => $this->client_uuid, + 'client_totp_signature' => $this->client_totp_signature, + 'peer' => $this->peer + ]; + } + + /** + * Constructs the object from an array representation + * + * @param array $array + * @return ClientIdentity + */ + public static function fromArray(array $array): ClientIdentity + { + $object = new self(); + $object->client_uuid = $array['client_uuid'] ?? null; + $object->client_totp_signature = $array['client_totp_signature'] ?? null; + $object->peer = $array['peer'] ?? null; + return $object; + } + } \ No newline at end of file diff --git a/src/FederationLib/subproc b/src/FederationLib/subproc new file mode 100644 index 0000000..f218f25 --- /dev/null +++ b/src/FederationLib/subproc @@ -0,0 +1,22 @@ +registerFunctions(); + + try + { + \TamerLib\tm::run(); + } + catch(Exception $e) + { + \LogLib\Log::error(\FederationLib\Enums\Misc::FEDERATIONLIB, $e->getMessage(), $e); + } + finally + { + exit(0); + }