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

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