Implemented Tamer & Cache Drivers (WIP)

This commit is contained in:
Netkas 2023-06-18 21:12:42 -04:00
parent 26f0f31cc6
commit d346c4d23d
No known key found for this signature in database
GPG key ID: 5DAF58535614062B
39 changed files with 2211 additions and 913 deletions

View file

@ -19,6 +19,9 @@
<option value="X-Args-3" />
<option value="X-Args-4" />
<option value="X-Args-5" />
<option value="X-Temperature" />
<option value="X-Model" />
<option value="X-OPENAI-API-KEY" />
</set>
</option>
</inspection_tool>

3
.idea/php.xml generated
View file

@ -24,6 +24,7 @@
<path value="/var/ncc/packages/com.php_school.cli_menu=4.3.0" />
<path value="/var/ncc/packages/com.php_school.terminal=0.2.1" />
<path value="/var/ncc/packages/com.malios.php_to_ascii_table=3.0.0" />
<path value="/var/ncc/packages/net.nosial.tamerlib=2.0.0" />
</include_path>
</component>
<component name="PhpProjectSharedConfiguration" php_language_level="8.1" />
@ -73,12 +74,10 @@
<extension name="mbstring" enabled="false" />
<extension name="mcrypt" enabled="false" />
<extension name="memcache" enabled="false" />
<extension name="memcached" enabled="false" />
<extension name="ming" enabled="false" />
<extension name="mongo" enabled="false" />
<extension name="mosquitto-php" enabled="false" />
<extension name="mqseries" enabled="false" />
<extension name="msgpack" enabled="true" />
<extension name="mssql" enabled="false" />
<extension name="mysql" enabled="false" />
<extension name="mysql_xdevapi" enabled="false" />

View file

@ -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

View file

@ -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",

View file

@ -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));
}
}
}

View file

@ -1,190 +0,0 @@
<?php
namespace FederationCLI\InteractiveMode;
use AsciiTable\Builder;
use Exception;
use FederationCLI\InteractiveMode;
use FederationCLI\Utilities;
use FederationLib\Enums\FilterOrder;
use FederationLib\Enums\Filters\ListClientsFilter;
use FederationLib\Exceptions\DatabaseException;
use FederationLib\Objects\Client;
class ClientManager
{
/**
* @param string $input
* @return void
*/
public static function processCommand(string $input): void
{
$parsed_input = Utilities::parseShellInput($input);
try
{
switch(strtolower($parsed_input['command']))
{
case 'register':
self::registerClient();
break;
case 'list':
// list [optional: page] [optional: filter] [optional: order] [optional: max_items]
self::listClients($parsed_input['args']);
break;
case 'total':
self::totalClients();
break;
case 'total_pages':
self::totalPages($parsed_input['args']);
break;
case 'get':
self::getClient($parsed_input['args']);
break;
default:
print(sprintf('Unknown command: %s', $parsed_input['command']) . PHP_EOL);
break;
}
}
catch(Exception $e)
{
Utilities::printExceptionStack($e);
}
}
/**
* Displays the help message for the client manager
*
* @return void
*/
public static function help(): void
{
print('Client manager commands:' . PHP_EOL);
print(' register - registers a new client with the federation' . PHP_EOL);
print(' list [optional: page (default 1)] [optional: filter (default created_timestamp)] [optional: order (default asc)] [optional: max_items (default 100)] - lists clients' . PHP_EOL);
print(' total - gets the total number of clients' . PHP_EOL);
print(' total_pages [optional: max_items (default 100)] - gets the total number of pages of clients' . PHP_EOL);
print(' get [client uuid] - gets a client by UUID' . PHP_EOL);
}
/**
* Registers a new client with the federation. prints the UUID of the client if successful.
*
* @return void
*/
private static function registerClient(): void
{
$client = new Client();
$client->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);
}
}

View file

@ -1,113 +0,0 @@
<?php
namespace FederationCLI\InteractiveMode;
use Exception;
use FederationCLI\Utilities;
use FederationLib\Classes\Configuration;
class ConfigurationManager
{
/**
* @param string $input
* @return void
*/
public static function processCommand(string $input): void
{
$parsed_input = Utilities::parseShellInput($input);
switch(strtolower($parsed_input['command']))
{
case 'read':
self::read($parsed_input['args'][0] ?? null);
break;
case 'write':
self::write($parsed_input['args'][0] ?? null, $parsed_input['args'][1] ?? null);
break;
case 'save':
self::save();
break;
default:
print(sprintf('Unknown command: %s', $parsed_input['command']) . PHP_EOL);
break;
}
}
/**
* Displays the help message for the client manager
*
* @return void
*/
public static function help(): void
{
print('Configuration manager commands:' . PHP_EOL);
print(' read - reads the current configuration' . PHP_EOL);
print(' read <key> - reads the value of the specified configuration key' . PHP_EOL);
print(' write <key> <value> - 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);
}
}
}

View file

@ -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;
}
}
}
}
}

View file

@ -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'];
}
}

View file

@ -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
*/

View file

@ -0,0 +1,71 @@
<?php
namespace FederationLib\Classes\Configuration;
use TamerLib\Objects\ServerConfiguration;
class TamerLibConfiguration
{
/**
* @var int
*/
private $cli_workers;
/**
* @var int
*/
private $node_workers;
/**
* @var ServerConfiguration
*/
private $server_configuration;
/**
* TamerLibConfiguration constructor.
*
* @param array $configuration
*/
public function __construct(array $configuration)
{
$this->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;
}
}

View file

@ -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();

View file

@ -1,18 +0,0 @@
<?php
namespace FederationLib\Classes;
class Memcached
{
/**
* @var int|null
*/
private static $memcached_last_connection_time;
/**
* @var \Memcached|null
*/
private static $memcached_connection;
}

View file

@ -5,6 +5,10 @@
namespace FederationLib\Classes;
use Exception;
use FederationLib\Classes\Configuration\CacheServerConfiguration;
use FederationLib\Enums\Misc;
use FederationLib\Exceptions\CacheConnectionException;
use FederationLib\Exceptions\CacheDriverException;
use LogLib\Log;
use RedisException;
@ -13,70 +17,195 @@
/**
* @var int|null
*/
private static $redis_last_connection_time;
private $redis_last_connection_time;
/**
* @var \Redis|null
*/
private static $redis_connection;
private $redis_connection;
/**
* @var CacheServerConfiguration
*/
private $configuration;
/**
* @var bool
*/
private $connection_error;
/**
* Redis constructor.
*
* @param CacheServerConfiguration $configuration
*/
public function __construct(CacheServerConfiguration $configuration)
{
$this->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;
}
}

View file

@ -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;
}
}
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace FederationLib\Enums;
use FederationLib\Classes\CommandApplets\DateCommand;
use FederationLib\Classes\CommandApplets\HostnameCommand;
use FederationLib\Classes\CommandApplets\WhoamiCommand;
final class CommandApplets
{
const WHOAMI = [WhoamiCommand::class, 'whoami'];
const HOSTNAME = [HostnameCommand::class, 'hostname'];
const DATE = [DateCommand::class, 'date'];
const ALL = [
self::WHOAMI,
self::HOSTNAME,
self::DATE
];
}

View file

@ -7,7 +7,17 @@
/**
* An internal server error occurred.
*/
public const INTERNAL_SERVER_ERROR = 0;
public const INTERNAL_SERVER_ERROR = -1000;
/**
* The invoker does not have permission to perform the requested action.
*/
public const ACCESS_DENIED = -1001;
/**
* The requested method is disabled.
*/
public const METHOD_DISABLED = -1002;
@ -37,19 +47,13 @@
public const CLIENT_DISABLED = 1004;
/**
* The requested user entity was not found.
*/
public const PEER_NOT_FOUND = 2000;
/**
* The requested peer association was not found.
*/
public const PEER_ASSOCIATION_NOT_FOUND = 3000;
/**
* The requested peer association type is invalid.
*/
public const INVALID_PEER_ASSOCIATION_TYPE = 3001;
public const ALL = [
self::INTERNAL_SERVER_ERROR,
self::ACCESS_DENIED,
self::CLIENT_NOT_FOUND,
self::INVALID_CLIENT_NAME,
self::INVALID_CLIENT_DESCRIPTION,
self::SIGNATURE_VERIFICATION_FAILED,
self::CLIENT_DISABLED
];
}

View file

@ -0,0 +1,24 @@
<?php
namespace FederationLib\Enums\Standard;
final class Methods
{
public const PING = 'ping';
public const WHOAMI = 'whoami';
public const CREATE_CLIENT = 'create_client';
public const GET_CLIENT = 'get_client';
public const CHANGE_CLIENT_NAME = 'change_client_name';
public const CHANGE_CLIENT_DESCRIPTION = 'change_client_description';
public const ALL = [
self::PING,
self::WHOAMI,
self::CREATE_CLIENT,
self::GET_CLIENT,
self::CHANGE_CLIENT_NAME,
self::CHANGE_CLIENT_DESCRIPTION
];
}

View file

@ -0,0 +1,22 @@
<?php
namespace FederationLib\Enums\Standard;
final class PermissionRole
{
public const ROOT = 0;
public const ADMIN = 1;
public const OPERATOR = 2;
public const AGENT = 3;
public const CLIENT = 4;
public const GUEST = 5;
public const ALL = [
self::ROOT => 'root',
self::ADMIN => 'admin',
self::OPERATOR => 'operator',
self::AGENT => 'agent',
self::CLIENT => 'client',
self::GUEST => 'guest'
];
}

View file

@ -0,0 +1,19 @@
<?php
namespace FederationLib\Exceptions;
use Exception;
use Throwable;
class CacheConnectionException extends Exception
{
/**
* @param string $message
* @param int $code
* @param Throwable|null $previous
*/
public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace FederationLib\Exceptions;
use Exception;
use Throwable;
class CacheDriverException extends Exception
{
/**
* @param string $message
* @param int $code
* @param Throwable|null $previous
*/
public function __construct(string $message = "", ?Throwable $previous = null)
{
parent::__construct($message, $previous);
}
}

View file

@ -0,0 +1,15 @@
<?php
namespace FederationLib\Exceptions\Standard;
use Exception;
use FederationLib\Enums\Standard\ErrorCodes;
use Throwable;
class AccessDeniedException extends Exception
{
public function __construct(string $message = "", ?Throwable $previous = null)
{
parent::__construct($message, ErrorCodes::ACCESS_DENIED, $previous);
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace FederationLib\Exceptions\Standard;
use Exception;
use FederationLib\Enums\Standard\ErrorCodes;
use Throwable;
class InternalServerException extends Exception
{
/**
* @param string $message
* @param Throwable|null $previous
*/
public function __construct(string $message = "", ?Throwable $previous = null)
{
parent::__construct($message, ErrorCodes::INTERNAL_SERVER_ERROR, $previous);
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace FederationLib\Exceptions\Standard;
use Exception;
use FederationLib\Enums\Standard\ErrorCodes;
use Throwable;
class InvalidClientDescriptionException extends Exception
{
/**
* @param string $message
* @param Throwable|null $previous
*/
public function __construct(string $message = "", ?Throwable $previous = null)
{
parent::__construct($message, ErrorCodes::INVALID_CLIENT_DESCRIPTION, $previous);
}
}

View file

@ -0,0 +1,15 @@
<?php
namespace FederationLib\Exceptions\Standard;
use Exception;
use Throwable;
class InvalidPermissionRoleException extends Exception
{
public function __construct(string $message = "", ?Throwable $previous = null)
{
parent::__construct($message, 0, $previous);
}
}

View file

@ -2,8 +2,25 @@
namespace FederationLib;
use Exception;
use FederationLib\Classes\Configuration;
use FederationLib\Enums\Misc;
use FederationLib\Enums\Standard\ErrorCodes;
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\Managers\ClientManager;
use FederationLib\Managers\EventLogManager;
use FederationLib\Objects\Client;
use FederationLib\Objects\ResolvedIdentity;
use FederationLib\Objects\Standard\ClientIdentity;
use LogLib\Log;
use TamerLib\Enums\TamerMode;
use TamerLib\tm;
use Throwable;
class FederationLib
{
@ -12,35 +29,241 @@
*/
private ClientManager $client_manager;
/**
* @var EventLogManager
*/
private EventLogManager $event_log_manager;
/**
* FederationLib constructor.
*/
public function __construct()
{
$this->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';
}
}

View file

@ -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;
}

View file

@ -0,0 +1,24 @@
<?php
namespace FederationLib\Interfaces;
use FederationLib\Objects\InvokeResults;
interface CommandAppletInterface
{
/**
* Returns the command to execute.
*
* @return string
*/
public static function getCommand(): string;
/**
* Executes the command and returns the results.
*
* @param string $uid
* @param array $args
* @return InvokeResults
*/
public static function execute(string $uid, array $args): InvokeResults;
}

View file

@ -2,22 +2,28 @@
namespace FederationLib\Managers;
use Doctrine\DBAL\ParameterType;
use Exception;
use FederationLib\Classes\Configuration;
use FederationLib\Classes\Database;
use FederationLib\Classes\Redis;
use FederationLib\Classes\Security;
use FederationLib\Classes\Utilities;
use FederationLib\Classes\Validate;
use FederationLib\Enums\DatabaseTables;
use FederationLib\Enums\EventPriority;
use FederationLib\Enums\FilterOrder;
use FederationLib\Enums\Filters\ListClientsFilter;
use FederationLib\Enums\Standard\EventCode;
use FederationLib\Exceptions\DatabaseException;
use FederationLib\Exceptions\Standard\ClientNotFoundException;
use FederationLib\Exceptions\Standard\InvalidClientDescriptionException;
use FederationLib\Exceptions\Standard\InvalidClientNameException;
use FederationLib\Exceptions\Standard\InvalidPermissionRoleException;
use FederationLib\FederationLib;
use FederationLib\Objects\Client;
use LogLib\Log;
use Symfony\Component\Uid\Uuid;
use TamerLib\Enums\TamerMode;
use TamerLib\tm;
class ClientManager
{
@ -36,66 +42,65 @@
$this->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.

View file

@ -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);
}
}
}

View file

@ -0,0 +1,177 @@
<?php
namespace FederationLib\Managers;
use Exception;
use FederationLib\Classes\Configuration;
use FederationLib\Classes\Redis;
use FederationLib\Classes\Utilities;
use FederationLib\Exceptions\CacheConnectionException;
use FederationLib\Exceptions\CacheDriverException;
use InvalidArgumentException;
class RedisConnectionManager
{
/**
* @var Redis[]|null
*/
private static $connections;
/**
* Returns the requested redis connection to use
*
* WARNING: Very advanced, terrifying code ahead - proceed with caution
* This code is responsible for determining which redis connection to use
* based on the configuration and the state of the connections
* It's a bit of a mess, but it works
*
* @param string|null $name
* @param string|null $fallback
* @return \Redis
* @throws CacheConnectionException
* @throws CacheDriverException
*/
public static function getConnection(?string $name=null, ?string $fallback=null): \Redis
{
if(self::$connections === null)
{
self::$connections = [];
foreach(Configuration::getCacheServers() as $configuration)
{
self::$connections[$configuration->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
}
}

View file

@ -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']);

View file

@ -0,0 +1,50 @@
<?php
/** @noinspection PhpMissingFieldTypeInspection */
namespace FederationLib\Objects;
class InvokeResults
{
/**
* @var int
*/
private $exit_code;
/**
* @var string
*/
private $output;
/**
* InvokeResults constructor.
*
* @param int $exit_code
* @param string $output
*/
public function __construct(int $exit_code, string $output)
{
$this->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;
}
}

View file

@ -4,6 +4,8 @@
namespace FederationLib\Objects;
use InvalidArgumentException;
class ParsedFederatedAddress
{
/**
@ -30,6 +32,12 @@
{
preg_match("/(?<source>[a-z0-9]+)\.(?<type>[a-z0-9]+):(?<id>.+)/", $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'];

View file

@ -0,0 +1,98 @@
<?php
namespace FederationLib\Objects;
use FederationLib\Classes\Configuration;
use FederationLib\Enums\Standard\PermissionRole;
use FederationLib\Exceptions\Standard\AccessDeniedException;
class ResolvedIdentity
{
/**
* @var Client|null
*/
private $client;
/**
* @var Peer|null
*/
private $peer;
/**
* @var int
*/
private $permission_role;
/**
* ResolvedIdentity constructor.
*
* @param Client|null $client
* @param Peer|null $peer
*/
public function __construct(?Client $client, ?Peer $peer=null, bool $allow_root=false)
{
$this->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;
}
}

View file

@ -0,0 +1,173 @@
<?php
namespace FederationLib\Objects\Standard;
use FederationLib\Interfaces\SerializableObjectInterface;
class Client implements SerializableObjectInterface
{
/**
* @var string
*/
private $uuid;
/**
* @var bool
*/
private $enabled;
/**
* @var string
*/
private $name;
/**
* @var string|null
*/
private $description;
/**
* @var int
*/
private $permission_role;
/**
* @var int
*/
private $created_timestamp;
/**
* @var int
*/
private $updated_timestamp;
/**
* @var int
*/
private $seen_timestamp;
/**
* Client constructor.
*
* @param \FederationLib\Objects\Client|null $client
*/
public function __construct(?\FederationLib\Objects\Client $client=null)
{
if($client === null)
{
return;
}
$this->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;
}
}

View file

@ -0,0 +1,84 @@
<?php
/** @noinspection PhpMissingFieldTypeInspection */
namespace FederationLib\Objects\Standard;
use FederationLib\Interfaces\SerializableObjectInterface;
class ClientIdentity implements SerializableObjectInterface
{
/**
* @var string
*/
private $client_uuid;
/**
* @var string|null
*/
private $client_totp_signature;
/**
* @var string|null
*/
private $peer;
/**
* Returns the client UUID
*
* @return string
*/
public function getClientUuid(): string
{
return $this->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;
}
}

22
src/FederationLib/subproc Normal file
View file

@ -0,0 +1,22 @@
<?php
require 'ncc';
import('net.nosial.federationlib');
\TamerLib\tm::initialize(\TamerLib\Enums\TamerMode::WORKER);
$federation_lib = new \FederationLib\FederationLib();
$federation_lib->registerFunctions();
try
{
\TamerLib\tm::run();
}
catch(Exception $e)
{
\LogLib\Log::error(\FederationLib\Enums\Misc::FEDERATIONLIB, $e->getMessage(), $e);
}
finally
{
exit(0);
}