Initial Commit

This commit is contained in:
Netkas 2023-06-04 14:23:51 -04:00
parent 93a0b9be02
commit 6e599b2c0c
No known key found for this signature in database
GPG key ID: 5DAF58535614062B
99 changed files with 10836 additions and 4 deletions

View file

@ -0,0 +1,149 @@
<?php
/** @noinspection PhpMissingFieldTypeInspection */
namespace FederationCLI;
use FederationLib\FederationLib;
class InteractiveMode
{
/**
* The current menu the user is in
*
* @var string
*/
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;
/**
* Main entry point for the interactive mode
*
* @param array $args
* @return void
*/
public static function main(array $args=[]): void
{
while(true)
{
print(sprintf('federation@%s:~$ ', self::$current_menu));
$input = trim(fgets(STDIN));
self::processCommand($input);
}
}
/**
* Processes a command from the user
*
* @param string $input
* @return void
*/
private static function processCommand(string $input): void
{
$parsed_input = Utilities::parseShellInput($input);
switch(strtolower($parsed_input['command']))
{
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();
}
return self::$federation_lib;
}
}

View file

@ -0,0 +1,190 @@
<?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

@ -0,0 +1,113 @@
<?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

@ -0,0 +1,41 @@
<?php
namespace FederationCLI;
use ncc\Runtime;
class Program
{
/**
* Main entry point for the CLI
*
* @param array $args
* @return void
*/
public static function main(array $args=[]): void
{
if (isset($args['shell']))
{
InteractiveMode::main($args);
}
self::help();
}
/**
* Displays the help message
*
* @return void
*/
public static function help(): void
{
print('FederationLib v' . Runtime::getConstant('net.nosial.federationlib', 'version') . PHP_EOL . PHP_EOL);
print('Usage: federationlib [command] [options]' . PHP_EOL);
print('Commands:' . PHP_EOL);
print(' help - show this help' . PHP_EOL);
print(' shell - enter interactive mode' . PHP_EOL);
exit(0);
}
}

View file

@ -0,0 +1,101 @@
<?php
namespace FederationCLI;
class Utilities
{
/**
* Parses the shell input into a command and arguments array
*
* @param string $input
* @return array
*/
public static function parseShellInput(string $input): array
{
$parsed = explode(' ', $input);
$command = array_shift($parsed);
$args = $parsed;
return [
'command' => $command,
'args' => $args
];
}
/**
* Parses a boolean value from a string
*
* @param $input
* @return bool
*/
public static function parseBoolean($input): bool
{
if(is_null($input))
return false;
if(is_bool($input))
return $input;
if(is_numeric($input))
return (bool) $input;
if(is_string($input))
$input = trim($input);
switch(strtolower($input))
{
case 'true':
case 'yes':
case 'y':
case '1':
return true;
default:
case 'false':
case 'no':
case 'n':
case '0':
return false;
}
}
/**
* Prompts the user for an input value
*
* @param string|null $prompt
* @param string|null $default_value
* @return string|null
*/
public static function promptInput(?string $prompt=null, ?string $default_value=null): ?string
{
if($prompt)
print($prompt . ' ');
$input = trim(fgets(STDIN));
if(!$input && $default_value)
$input = $default_value;
return $input;
}
/**
* Prompts the user for a boolean value
*
* @param string|null $prompt
* @param bool $default_value
* @return bool
*/
public static function promptYesNo(?string $prompt=null, bool $default_value=false): bool
{
if($prompt)
print($prompt . ' ');
$input = trim(fgets(STDIN));
if(!$input && $default_value)
$input = $default_value;
return self::parseBoolean($input);
}
}

View file

@ -0,0 +1,84 @@
<?php
namespace FederationLib\Classes;
class Base32
{
/**
* The base32 alphabet. (RFC 4648)
*
* @var string
*/
private static $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
/**
* Encodes a string to base32.
*
* @param $string
* @return string
*/
public static function encode($string): string
{
$binary = '';
$output = '';
foreach (str_split($string) as $char)
{
$binary .= str_pad(decbin(ord($char)), 8, '0', STR_PAD_LEFT);
}
$padding = 5 - (strlen($binary) % 5);
if ($padding !== 5)
{
$binary .= str_repeat('0', $padding);
}
$chunks = str_split($binary, 5);
foreach ($chunks as $chunk)
{
$output .= self::$alphabet[bindec($chunk)];
}
$padding = (strlen($output) % 8 === 0) ? 0 : (8 - (strlen($output) % 8));
if ($padding !== 0)
{
$output .= str_repeat('=', $padding);
}
return $output;
}
/**
* Decodes a base32 encoded string.
*
* @param $string
* @return string
*/
public static function decode($string): string
{
$binary = '';
$output = '';
foreach (str_split($string) as $char)
{
if ($char === '=')
break;
$binary .= str_pad(decbin(strpos(self::$alphabet, $char)), 5, '0', STR_PAD_LEFT);
}
$padding = 8 - (strlen($binary) % 8);
if ($padding !== 8)
{
$binary = substr($binary, 0, -$padding);
}
$chunks = str_split($binary, 8);
foreach ($chunks as $chunk)
{
$output .= chr(bindec($chunk));
}
return $output;
}
}

View file

@ -0,0 +1,7 @@
<?php
namespace FederationLib\Classes;
class CacheSystem
{
}

View file

@ -0,0 +1,226 @@
<?php
/** @noinspection PhpMissingFieldTypeInspection */
namespace FederationLib\Classes;
use Exception;
use FederationLib\Classes\Configuration\CacheServerConfiguration;
use RuntimeException;
class Configuration
{
/**
* @var \ConfigLib\Configuration|null
*/
private static $configuration;
/**
* Returns the full raw configuration array.
*
* @return array
*/
public static function getConfiguration(): array
{
if(self::$configuration === null)
{
self::$configuration = new \ConfigLib\Configuration('federation');
/** Database Configuration */
self::$configuration->setDefault('database.driver', 'mysqli');
self::$configuration->setDefault('database.host', 'localhost');
self::$configuration->setDefault('database.port', 3306);
self::$configuration->setDefault('database.name', 'federation');
self::$configuration->setDefault('database.username', 'root');
self::$configuration->setDefault('database.password', 'root');
self::$configuration->setDefault('database.charset', 'utf8mb4');
self::$configuration->setDefault('database.reconnect_interval', 1800);
/** Multi-Cache Configuration */
self::$configuration->setDefault('cache_system.enabled', true);
// Cache System Configuration
// 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');
// 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.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.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);
/** Federation Configuration */
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);
/** Save the configuration's default values if they don't exist */
try
{
self::$configuration->save();
}
catch(Exception $e)
{
throw new RuntimeException('Failed to save configuration: ' . $e->getMessage(), $e);
}
}
return self::$configuration->getConfiguration();
}
/**
* Returns the configuration object.
*
* @return \ConfigLib\Configuration
*/
public static function getConfigurationObject(): \ConfigLib\Configuration
{
if(self::$configuration === null)
{
self::getConfiguration();
}
return self::$configuration;
}
/**
* Returns driver of the database.
*
* @return string
*/
public static function getDatabaseDriver(): string
{
return self::getConfiguration()['database']['driver'];
}
/**
* Returns the host of the database.
*
* @return string
*/
public static function getDatabaseHost(): string
{
return self::getConfiguration()['database']['host'];
}
/**
* Returns the port of the database.
*
* @return int
*/
public static function getDatabasePort(): int
{
return (int)self::getConfiguration()['database']['port'];
}
/**
* Returns the name of the database.
*
* @return string
*/
public static function getDatabaseName(): string
{
return self::getConfiguration()['database']['name'];
}
/**
* Returns the username of the database.
*
* @return string
*/
public static function getDatabaseUsername(): string
{
return self::getConfiguration()['database']['username'];
}
/**
* Returns the password of the database.
*
* @return string|null
*/
public static function getDatabasePassword(): ?string
{
$password = self::getConfiguration()['database']['password'];
if($password === '')
{
return null;
}
return $password;
}
/**
* Returns the charset of the database.
*
* @return string
*/
public static function getDatabaseCharset(): string
{
return self::getConfiguration()['database']['charset'];
}
/**
* Returns the interval in seconds to reconnect to the database.
*
* @return int
*/
public static function getDatabaseReconnectInterval(): int
{
return (int)self::getConfiguration()['database']['reconnect_interval'];
}
/**
* Returns True if the cache system is enabled for FederationLib
* and False if not, based on the configuration.
*
* @return bool
*/
public static function isCacheSystemEnabled(): bool
{
return (bool)self::getConfiguration()['cache_system']['enabled'];
}
/**
* Returns the configuration for all the cache servers
*
* @return CacheServerConfiguration[]
*/
public static function getCacheServers(): array
{
$results = [];
foreach(self::getConfiguration()['cache_system']['servers'] as $server_name => $server)
{
if($server['enabled'] === true)
{
$results[] = new CacheServerConfiguration($server_name, $server);
}
}
return $results;
}
}

View file

@ -0,0 +1,162 @@
<?php
namespace FederationLib\Classes\Configuration;
class CacheServerConfiguration
{
/**
* @var string
*/
private string $name;
/**
* @var bool
*/
private bool $enabled;
/**
* @var string|null
*/
private ?string $host;
/**
* @var int|null
*/
private ?int $port;
/**
* @var string|null
*/
private ?string $driver;
/**
* @var int|null
*/
private ?int $priority;
/**
* @var string|null
*/
private ?string $username;
/**
* @var string|null
*/
private ?string $password;
/**
* @var string|null
*/
private ?string $database;
/**
* @var int|null
*/
private ?int $reconnect_interval;
/**
* CacheServerConfiguration constructor.
*
* @param array $configuration
*/
public function __construct(string $name, array $configuration)
{
$this->name = $configuration['name'] ?? $name;
$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;
}
/**
* Returns the name of the cache server
*
* @return string
*/
public function getName(): ?string
{
return $this->name;
}
/**
* Returns whether the cache server is enabled
*
* @return bool
*/
public function getEnabled(): bool
{
return $this->enabled ?? false;
}
/**
* Returns the host of the cache server
*
* @return string|null
*/
public function getHost(): ?string
{
return $this->host;
}
/**
* @return int|null
*/
public function getPort(): ?int
{
return $this->port;
}
/**
* @return string|null
*/
public function getDriver(): ?string
{
return $this->driver;
}
/**
* @return int|null
*/
public function getPriority(): ?int
{
return $this->priority;
}
/**
* @return string|null
*/
public function getUsername(): ?string
{
return $this->username;
}
/**
* @return string|null
*/
public function getPassword(): ?string
{
return $this->password;
}
/**
* @return string|null
*/
public function getDatabase(): ?string
{
return $this->database;
}
/**
* @return int|null
*/
public function getReconnectInterval(): ?int
{
return $this->reconnect_interval;
}
}

View file

@ -0,0 +1,144 @@
<?php
namespace FederationLib\Classes\Configuration;
class CacheSystemConfiguration
{
/**
* @var bool
*/
private $client_objects_enabled;
/**
* @var int
*/
private $client_objects_ttl;
/**
* @var string
*/
private $client_objects_server_preference;
/**
* @var string
*/
private $client_objects_server_fallback;
/**
* @var bool
*/
private $peer_objects_enabled;
/**
* @var int
*/
private $peer_objects_ttl;
/**
* @var string
*/
private $peer_objects_server_preference;
/**
* @var string
*/
private $peer_objects_server_fallback;
/**
* CacheSystemConfiguration constructor.
*
* @param array $configuration
*/
public function __construct(array $configuration)
{
$this->client_objects_enabled = $configuration['cache_system.cache.client_objects_enabled'] ?? false;
$this->client_objects_ttl = $configuration['cache_system.cache.client_objects_ttl'] ?? 200;
$this->client_objects_server_preference = $configuration['cache_system.cache.client_objects_server_preference'] ?? 'any';
$this->client_objects_server_fallback = $configuration['cache_system.cache.client_objects_server_fallback'] ?? 'any';
$this->peer_objects_enabled = $configuration['cache_system.cache.peer_objects_enabled'] ?? false;
$this->peer_objects_ttl = $configuration['cache_system.cache.peer_objects_ttl'] ?? 200;
$this->peer_objects_server_preference = $configuration['cache_system.cache.peer_objects_server_preference'] ?? 'any';
$this->peer_objects_server_fallback = $configuration['cache_system.cache.peer_objects_server_fallback'] ?? 'any';
}
/**
* Returns True if client cache objects are enabled, false otherwise
*
* @return bool
*/
public function getClientObjectsEnabled(): bool
{
return $this->client_objects_enabled;
}
/**
* Returns the TTL for client cache objects
*
* @return int
*/
public function getClientObjectsTtl(): int
{
return $this->client_objects_ttl;
}
/**
* Returns the server preference to where client cache objects should be stored
*
* @return string
*/
public function getClientObjectsServerPreference(): string
{
return $this->client_objects_server_preference;
}
/**
* Returns the server fallback to where client cache objects should be stored
*
* @return string
*/
public function getClientObjectsServerFallback(): string
{
return $this->client_objects_server_fallback;
}
/**
* Returns True if peer cache objects are enabled, false otherwise
*
* @return bool
*/
public function getPeerObjectsEnabled(): bool
{
return $this->peer_objects_enabled;
}
/**
* Returns the TTL for peer cache objects
*
* @return int
*/
public function getPeerObjectsTtl(): int
{
return $this->peer_objects_ttl;
}
/**
* Returns the server preference to where peer cache objects should be stored
*
* @return string
*/
public function getPeerObjectsServerPreference(): string
{
return $this->peer_objects_server_preference;
}
/**
* Returns the server fallback to where peer cache objects should be stored
*
* @return string
*/
public function getPeerObjectsServerFallback(): string
{
return $this->peer_objects_server_fallback;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,88 @@
<?php
/** @noinspection PhpMissingFieldTypeInspection */
namespace FederationLib\Classes;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Logging\Middleware;
use Exception;
use FederationLib\Exceptions\DatabaseException;
use LogLib\Log;
use LogLib\Psr;
class Database
{
/**
* @var int|null
*/
private static $sql_last_connection_time;
/**
* @var Connection|null
*/
private static $sql_connection;
/**
* Returns/Establishes a connection to the database.
*
* @return Connection
* @throws DatabaseException
*/
public static function getConnection(): Connection
{
if(self::$sql_connection === null)
{
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()));
$connection = DriverManager::getConnection([
'driver' => Configuration::getDatabaseDriver(),
'host' => Configuration::getDatabaseHost(),
'port' => Configuration::getDatabasePort(),
'dbname' => Configuration::getDatabaseName(),
'user' => Configuration::getDatabaseUsername(),
'password' => Configuration::getDatabasePassword(),
'charset' => Configuration::getDatabaseCharset()
]);
$connection->getConfiguration()->setMiddlewares([new Middleware(new Psr('com.dbal.doctrine'))]);
$connection->connect();
}
catch(Exception $e)
{
throw new DatabaseException('Failed to connect to the database: ' . $e->getMessage(), $e);
}
self::$sql_connection = $connection;
self::$sql_last_connection_time = time();
}
else
{
/** @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()));
// Reconnect to the database.
self::$sql_connection->close();
self::$sql_connection = null;
return self::getConnection();
}
}
return self::$sql_connection;
}
/**
* Returns the time of the last connection to the database. (null if no connection has been made)
*
* @return int|null
*/
public static function getSqlLastConnectionTime(): ?int
{
return self::$sql_last_connection_time;
}
}

View file

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

View file

@ -0,0 +1,91 @@
<?php
/** @noinspection PhpMissingFieldTypeInspection */
namespace FederationLib\Classes;
use Exception;
use LogLib\Log;
use RedisException;
class Redis
{
/**
* @var int|null
*/
private static $redis_last_connection_time;
/**
* @var \Redis|null
*/
private static $redis_connection;
/**
* Returns/Establishes a connection to the redis server.
* Returns null if redis is disabled.
*
* @return \Redis|null
* @throws RedisException
*/
public static function getConnection(): ?\Redis
{
if(!Configuration::isRedisEnabled())
{
return null;
}
if(self::$redis_connection === null)
{
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();
}
else
{
if(self::$redis_last_connection_time === null || self::$redis_last_connection_time < (time() - Configuration::getRedisReconnectInterval()))
{
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();
}
}
return self::$redis_connection;
}
/**
* Returns the last time the redis server was connected to.
*
* @return int|null
*/
public static function getRedisLastConnectionTime(): ?int
{
return self::$redis_last_connection_time;
}
}

View file

@ -0,0 +1,102 @@
<?php
/** @noinspection PhpMissingFieldTypeInspection */
namespace FederationLib\Classes;
use Exception;
use RuntimeException;
class Security
{
/**
* The number of digits in the generated code.
*
* @var int
*/
private static $digits = 6;
/**
* The number of seconds a code is valid.
*
* @var int
*/
private static $time_step = 30;
/**
* The number of seconds the clock is allowed to drift.
*
* @var int
*/
private static $time_offset = 0;
/**
* The hash algorithm used to generate the code.
*
* @var string
*/
private static $hash_algorithm = 'sha1';
/**
* Generates a secret TOTP key.
*
* @param $length
* @return string
*/
public static function generateSecret($length = 20): string
{
try
{
return Base32::encode(random_bytes($length));
}
catch(Exception $e)
{
throw new RuntimeException('Failed to generate secret: ' . $e->getMessage(), $e);
}
}
/**
* Generates a TOTP code based on a secret and timestamp.
*
* @param $secret
* @param $timestamp
* @return string
*/
public static function generateCode($secret, $timestamp=null): string
{
if (!$timestamp)
{
$timestamp = time();
}
$hash = hash_hmac(self::$hash_algorithm, pack('N*', 0, $timestamp / self::$time_step + self::$time_offset), Base32::decode($secret), true);
$offset = ord(substr($hash, -1)) & 0x0F;
$value = unpack('N', substr($hash, $offset, 4))[1] & 0x7FFFFFFF;
$modulo = 10 ** self::$digits;
return str_pad($value % $modulo, self::$digits, '0', STR_PAD_LEFT);
}
/**
* Validates a TOTP code.
*
* @param $secret
* @param $code
* @param $window
* @return bool
*/
public static function validateCode($secret, $code, $window = 1): bool
{
$timestamp = time();
for ($i = -$window; $i <= $window; $i++)
{
$generatedCode = self::generateCode($secret, $timestamp + $i * self::$time_step);
if ($code === $generatedCode)
{
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,183 @@
<?php
/** @noinspection PhpMissingFieldTypeInspection */
namespace FederationLib\Classes;
use FederationLib\Enums\SerializationMethod;
use FederationLib\Interfaces\SerializableObjectInterface;
use InvalidArgumentException;
use LogLib\Log;
use Throwable;
class Utilities
{
/**
* @var string[]|null
*/
private static $wordlist;
/**
* Returns an array of words from the wordlist.
*
* @param bool $cache True to cache the wordlist in memory, false to not cache the wordlist
* @return string[]
*/
public static function getWordlist(bool $cache=true): array
{
if(self::$wordlist !== null)
{
return self::$wordlist;
}
$wordlist = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'Data' . DIRECTORY_SEPARATOR . 'wordlist.txt');
$wordlist = array_filter(array_map('trim', explode("\n", $wordlist)));
if($cache)
{
self::$wordlist = $wordlist;
}
return $wordlist;
}
/**
* Generates a random name.
*
* @param int $word_count
* @param string $separator
* @param bool $capitalize
* @return string
*/
public static function generateName(int $word_count=3, string $separator=' ', bool $capitalize=true): string
{
$wordlist = self::getWordlist();
$name = '';
for($i = 0; $i < $word_count; $i++)
{
$name .= $wordlist[array_rand($wordlist)] . $separator;
}
$name = substr($name, 0, -1);
if($capitalize)
{
$name = ucwords($name);
}
return $name;
}
/**
* Parses a federated address into an array of parts.
* Example: entity:uid
*
* @param string $address
* @return array
*/
public static function parseFederatedAddress(string $address): array
{
if (preg_match($address, '/^(?P<entity>[a-zA-Z0-9_-]+):(?P<uid>[a-zA-Z0-9_-]+)$/', $matches, PREG_UNMATCHED_AS_NULL))
{
return [
'entity' => $matches['entity'],
'uid' => $matches['uid']
];
}
throw new InvalidArgumentException(sprintf('Invalid address provided: %s', $address));
}
/**
* Serializes an array into a string.
*
* @param array $data
* @param string $method
* @return string
*/
public static function serialize(array|SerializableObjectInterface $data, string $method): string
{
if($data instanceof SerializableObjectInterface)
{
$data = $data->toArray();
}
switch(strtolower($method))
{
case SerializationMethod::JSON:
return json_encode($data);
case SerializationMethod::MSGPACK:
return msgpack_pack($data);
default:
Log::warning('net.nosial.federationlib', sprintf('Unknown serialization method: %s, defaulting to msgpack', $method));
return msgpack_pack($data);
}
}
/**
* Recursively converts a Throwable into an array representation.
*
* @param Throwable $throwable
* @return array
*/
public static function throwableToArray(Throwable $throwable): array
{
$results = [
'message' => $throwable->getMessage(),
'code' => $throwable->getCode(),
'file' => $throwable->getFile(),
'line' => $throwable->getLine(),
'trace' => $throwable->getTrace(),
'previous' => $throwable->getPrevious()
];
if($results['previous'] instanceof Throwable)
{
$results['previous'] = self::throwableToArray($results['previous']);
}
return $results;
}
/**
* Uses the z-score method to detect anomalies in an array of numbers.
* The key of the returned array is the index of the number in the original array.
* The value of the returned array is the probability of the number being an anomaly.
* Negative values are anomalies, positive values are not.
* The higher the absolute value, the more likely it is to be an anomaly.
*
* @param array $data An array of numbers to check for anomalies
* @param int $threshold The threshold to use for detecting anomalies
* @param bool $filter True to only return anomalies, false to return all values
* @return array An array of probabilities
*/
public static function detectAnomalies(array $data, int $threshold = 2, bool $filter=true): array
{
$mean = array_sum($data) / count($data);
$squares = array_map(static function($x) use ($mean) { return ($x - $mean) ** 2; }, $data);
$standard_deviation = sqrt(array_sum($squares) / count($data));
$outliers = [];
foreach ($data as $key => $value)
{
$z_score = ($value - $mean) / $standard_deviation;
$probability = exp(-$z_score ** 2 / 2) / (sqrt(2 * M_PI) * $standard_deviation);
if($filter)
{
if ($z_score >= $threshold)
{
$outliers[$key] = -$probability;
}
}
else
{
$outliers[$key] = $probability;
}
}
return $outliers;
}
}

View file

@ -0,0 +1,60 @@
<?php
namespace FederationLib\Classes;
use FederationLib\Enums\Standard\PeerType;
use FederationLib\Enums\Standard\InternetPeerType;
use FederationLib\Enums\Standard\PeerAssociationType;
use FederationLib\Enums\Standard\UserPeerType;
class Validate
{
/**
* Determines the entity type based on the entity type string.
*
* @param string $entity_type
* @return string
*/
public static function getEntityType(string $entity_type): string
{
if(in_array($entity_type, InternetPeerType::ALL))
{
return PeerType::INTERNET;
}
if(in_array($entity_type, UserPeerType::ALL))
{
return PeerType::USER;
}
return PeerType::UNKNOWN;
}
/**
* Determines if the entity type is valid and supported.
*
* @param string $entity_type
* @return bool
*/
public static function validateEntityType(string $entity_type): bool
{
if(self::getEntityType($entity_type) === PeerType::UNKNOWN)
return false;
return true;
}
/**
* Validates the peer association type.
*
* @param string $type
* @return bool
*/
public static function peerAssociationType(string $type): bool
{
if(in_array(strtolower($type), PeerAssociationType::ALL))
return true;
return false;
}
}

View file

@ -0,0 +1,9 @@
<?php
namespace FederationLib\Enums;
final class CacheDriver
{
public const MEMCACHED = 'memcached';
public const REDIS = 'redis';
}

View file

@ -0,0 +1,30 @@
<?php
namespace FederationLib\Enums;
final class DatabaseTables
{
public const ANOMALY_TRACKING = 'anomaly_tracking';
public const ASSOCIATIONS = 'associations';
public const CLIENTS = 'clients';
public const EVENTS = 'events';
public const PEERS = 'peers';
public const PEERS_TELEGRAM_CHAT = 'peers_telegram_chat';
public const PEERS_TELEGRAM_USER = 'peers_telegram_user';
public const QUERY_DOCUMENTS = 'query_documents';
public const REPORTS = 'reports';
public const RESTRICTIONS = 'restrictions';
public const ALL = [
self::ANOMALY_TRACKING,
self::ASSOCIATIONS,
self::CLIENTS,
self::EVENTS,
self::PEERS,
self::PEERS_TELEGRAM_CHAT,
self::PEERS_TELEGRAM_USER,
self::QUERY_DOCUMENTS,
self::REPORTS,
self::RESTRICTIONS,
];
}

View file

@ -0,0 +1,11 @@
<?php
namespace FederationLib\Enums;
final class EventPriority
{
public const NONE = 0;
public const LOW = 1;
public const MEDIUM = 2;
public const HIGH = 3;
}

View file

@ -0,0 +1,9 @@
<?php
namespace FederationLib\Enums;
final class FilterOrder
{
public const ASCENDING = 'asc';
public const DESCENDING = 'desc';
}

View file

@ -0,0 +1,18 @@
<?php
namespace FederationLib\Enums\Filters;
final class ListClientsFilter
{
public const CREATED_TIMESTAMP = 'created_timestamp';
public const UPDATED_TIMESTAMP = 'updated_timestamp';
public const SEEN_TIMESTAMP = 'seen_timestamp';
public const ENABLED = 'enabled';
public const ALL = [
self::CREATED_TIMESTAMP,
self::UPDATED_TIMESTAMP,
self::SEEN_TIMESTAMP,
self::ENABLED
];
}

View file

@ -0,0 +1,8 @@
<?php
namespace FederationLib\Enums;
final class Misc
{
public const FEDERATIONLIB = 'net.nosial.federationlib';
}

View file

@ -0,0 +1,9 @@
<?php
namespace FederationLib\Enums;
final class SerializationMethod
{
public const JSON = 'json';
public const MSGPACK = 'msgpack';
}

View file

@ -0,0 +1,12 @@
<?php
namespace FederationLib\Enums\Standard;
final class AttachmentDataType
{
public const RAW = 'raw';
public const BASE64 = 'base64';
public const URL = 'url';
}

View file

@ -0,0 +1,15 @@
<?php
namespace FederationLib\Enums\Standard;
final class ContentType
{
public const NULL = 'null';
public const TEXT = 'text/plain';
public const HTML = 'text/html';
public const JSON = 'application/json';
public const XML = 'application/xml';
public const EMAIL = 'message/rfc822';
public const BASE64 = 'application/base64';
public const HTTP = 'application/http';
}

View file

@ -0,0 +1,12 @@
<?php
namespace FederationLib\Enums\Standard;
final class DocumentSubjectType
{
public const RECON = 'RECON';
public const ALL = [
self::RECON
];
}

View file

@ -0,0 +1,55 @@
<?php
namespace FederationLib\Enums\Standard;
final class ErrorCodes
{
/**
* An internal server error occurred.
*/
public const INTERNAL_SERVER_ERROR = 0;
/**
* The requested client was not found.
*/
public const CLIENT_NOT_FOUND = 1000;
/**
* The requested client name is invalid.
*/
public const INVALID_CLIENT_NAME = 1001;
/**
* The requested client description is invalid.
*/
public const INVALID_CLIENT_DESCRIPTION = 1002;
/**
* The requested client signature verification failed.
*/
public const SIGNATURE_VERIFICATION_FAILED = 1003;
/**
* The requested client is disabled.
*/
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;
}

View file

@ -0,0 +1,116 @@
<?php
namespace FederationLib\Enums\Standard;
final class EventCode
{
/**
* A generic event code indicates the event is not specific to a particular event type.
* This could apply to server events such as cleaning up old data or general system
* messages.
*/
public const GENERIC = 'GENERIC';
/**
* Indicates a client was created.
*/
public const CLIENT_CREATED = 'CLIENT_CREATED';
/**
* Indicates a client was deleted.
*/
public const CLIENT_DELETED = 'CLIENT_DELETED';
/**
* Indicates a client was enabled.
*/
public const CLIENT_ENABLED = 'CLIENT_ENABLED';
/**
* Indicates a client was disabled.
*/
public const CLIENT_DISABLED = 'CLIENT_DISABLED';
/**
* Indicates a client's authentication was enabled.
*/
public const CLIENT_AUTHENTICATION_ENABLED = 'CLIENT_AUTHENTICATION_ENABLED';
/**
* Indicates a client's authentication was disabled.
*/
public const CLIENT_AUTHENTICATION_DISABLED = 'CLIENT_AUTHENTICATION_DISABLED';
/**
* Indicates a client's query document was rejected
* (e.g. the query document couldn't be processed by the server, see the message for details).
*/
public const QUERY_DOCUMENT_REJECTED = 'QUERY_DOCUMENT_REJECTED';
/**
* Indicates an anomaly was detected in INCOMING events
* (e.g. a client is receiving too many requests/messages).
*/
public const ANOMALY_INCOMING = 'ANOMALY_INCOMING';
/**
* Indicates an anomaly was detected in OUTGOING events
* (e.g. a client is sending too many requests/messages).
*/
public const ANOMALY_OUTGOING = 'ANOMALY_OUTGOING';
/**
* Indicates an anomaly was detected in PEER JOIN events
* (e.g. a channel is receiving too many join requests/events).
*/
public const ANOMALY_PEER_JOIN = 'ANOMALY_PEER_JOIN';
/**
* Indicates a service cleanup event.
* This is a special event code that is not intended to be used by clients.
* (e.g. the server is cleaning up old data, results are explained in the message).
*/
public const SERVICE_CLEANUP = 'SERVICE_CLEANUP';
/**
* Indicates a service message event.
* This is a special event code that is not intended to be used by clients.
* (e.g. a message from the server).
*/
public const SERVICE_MESSAGE = 'SERVICE_MESSAGE';
/**
* Indicates a service error event.
* This is a special event code that is not intended to be used by clients.
* (e.g. an error occurred on the server, results are explained in the message).
*/
public const SERVICE_ERROR = 'SERVICE_ERROR';
/**
* Indicates a peer was restricted by the server.
* This is a special event code that is not intended to be used by clients.
* (e.g. a peer was restricted by the server, results are explained in the message).
*/
public const PEER_RESTRICTED = 'PEER_RESTRICTED';
/**
* An array of all event codes.
*/
public const ALL = [
self::GENERIC,
self::CLIENT_CREATED,
self::CLIENT_DELETED,
self::CLIENT_ENABLED,
self::CLIENT_DISABLED,
self::CLIENT_AUTHENTICATION_ENABLED,
self::CLIENT_AUTHENTICATION_DISABLED,
self::QUERY_DOCUMENT_REJECTED,
self::ANOMALY_INCOMING,
self::ANOMALY_OUTGOING,
self::ANOMALY_PEER_JOIN,
self::SERVICE_CLEANUP,
self::SERVICE_MESSAGE,
self::SERVICE_ERROR,
self::PEER_RESTRICTED
];
}

View file

@ -0,0 +1,16 @@
<?php
namespace FederationLib\Enums\Standard;
final class EventType
{
public const INFORMATION = 'INF';
public const WARNING = 'WRN';
public const ERROR = 'ERR';
public const ALL = [
self::INFORMATION,
self::WARNING,
self::ERROR
];
}

View file

@ -0,0 +1,15 @@
<?php
namespace FederationLib\Enums\Standard;
final class InternetPeerType
{
public const TELEGRAM_CHAT = 'telegram.chat';
public const TELEGRAM_CHANNEL = 'telegram.channel';
public const ALL = [
self::TELEGRAM_CHAT,
self::TELEGRAM_CHANNEL,
];
}

View file

@ -0,0 +1,48 @@
<?php
namespace FederationLib\Enums\Standard;
final class PeerAssociationType
{
/**
* Indicates the parent peer is the owner of the child peer.
*/
public const OWNER = 'owner';
/**
* Indicates the parent peer is the administrator of the child peer.
*/
public const ADMIN = 'admin';
/**
* Indicates the parent peer is a moderator of the child peer.
*/
public const MODERATOR = 'moderator';
/**
* Indicates the parent peer is a member of the child peer.
*/
public const MEMBER = 'member';
/**
* Indicates the parent peer is banned from the child peer.
*/
public const BANNED = 'banned';
/**
* Indicates the parent peer is an alternative address for the child peer.
*/
public const ALTERNATIVE = 'alternative';
/**
* An array of all peer association types.
*/
const ALL = [
self::OWNER,
self::ADMIN,
self::MODERATOR,
self::MEMBER,
self::BANNED,
self::ALTERNATIVE
];
}

View file

@ -0,0 +1,18 @@
<?php
namespace FederationLib\Enums\Standard;
final class PeerType
{
const USER = 'user';
const INTERNET = 'internet';
const UNKNOWN = 'unknown';
const ALL = [
self::USER,
self::INTERNET,
self::UNKNOWN
];
}

View file

@ -0,0 +1,76 @@
<?php
namespace FederationLib\Enums\Standard;
final class ReportType
{
/**
* This category can be used for general reports submitted by users regarding any issues they encounter on the platform.
*/
public const USER_REPORT = 'user_report';
/**
* This category can be used when reports are received from external sources, such as third-party organizations or security researchers.
*/
public const EXTERNAL_REPORT = 'external_report';
/**
* This category can be used for reporting unsolicited and unwanted commercial or promotional content.
*/
public const SPAM = 'spam';
/**
* This category can be used for reporting fraudulent activities, schemes, or attempts to deceive users for personal gain.
*/
public const SCAM = 'scam';
/**
* This category can be used for reporting attempts to acquire sensitive user information, such as passwords or financial details, through deceptive means.
*/
public const PHISHING = 'phishing';
/**
* This category can be used for reporting the presence or distribution of malicious software or code.
*/
public const MALWARE = 'malware';
/**
* This category can be used for reporting instances of unauthorized distribution or infringement of copyrighted material.
*/
public const PIRACY = 'piracy';
/**
* This category can be used for reporting the presence or activities of botnets, which are networks of compromised computers used for malicious purposes.
*/
public const BOTNET = 'botnet';
/**
* This category can be used for reporting the presence of explicit adult content that violates the platform's guidelines or policies.
*/
public const PORNOGRAPHY = 'pornography';
/**
* This category can be used for reporting the presence of graphic and violent content that may be disturbing or offensive.
*/
public const GORE = 'gore';
/**
* This category can be used for reporting any abuse or misuse of the platform's services or features.
*/
public const SERVICE_ABUSE = 'service_abuse';
/**
* This category can be used for reporting instances of child abuse, child pornography, or any content that exploits minors.
*/
public const CHILD_EXPLOITATION = 'child_exploitation';
/**
* This category can be used for reporting fraudulent activities conducted online, such as scams, fake websites, or misleading online marketplaces.
*/
public const FRAUD = 'fraud';
/**
* This category can be used for any reports that do not fit into the predefined categories but still require attention.
*/
public const OTHER = 'other';
}

View file

@ -0,0 +1,12 @@
<?php
namespace FederationLib\Enums\Standard;
final class ScanMode
{
/**
* Means that the scanner should only scan the header of the document.
*/
public const DOCUMENT = 'document';
}

View file

@ -0,0 +1,12 @@
<?php
namespace FederationLib\Enums\Standard;
final class UserPeerType
{
public const TELEGRAM_USER = 'telegram.user';
public const ALL = [
self::TELEGRAM_USER,
];
}

View file

@ -0,0 +1,8 @@
<?php
namespace FederationLib\Enums;
final class UserEntityRelationType
{
public const OWNER = 'owner';
}

View file

@ -0,0 +1,26 @@
<?php
namespace FederationLib\Exceptions;
use Exception;
use Throwable;
class DatabaseException extends Exception
{
/**
* @param string $message
* @param Throwable|null $previous
* @noinspection SenselessProxyMethodInspection
*/
public function __construct(string $message = "", ?Throwable $previous = null)
{
if (!empty($previous))
{
parent::__construct($message, $previous->getCode(), $previous);
}
else
{
parent::__construct($message);
}
}
}

View file

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

View file

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

View file

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

View file

@ -0,0 +1,20 @@
<?php
namespace FederationLib\Exceptions\Standard;
use Exception;
use Throwable;
class PeerNotFoundException extends Exception
{
/**
* PeerNotFoundException constructor.
*
* @param string $peer
* @param Throwable|null $previous
*/
public function __construct(string $peer, ?Throwable $previous = null)
{
parent::__construct("The peer '{$peer}' was not found", 0, $previous);
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace FederationLib\Exceptions\Standard;
use Exception;
use Throwable;
class UnsupportedPeerType extends Exception
{
/**
* UnsupportedPeerType constructor.
*
* @param string $peer_type
* @param Throwable|null $previous
*/
public function __construct(string $peer_type, ?Throwable $previous = null)
{
parent::__construct("The peer type '{$peer_type}' is not supported by this server", 0, $previous);
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace FederationLib\Exceptions;
use Exception;
use Throwable;
class UnsupportedEntityType 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);
}
}

38
src/FederationLib/FederationLib.php Normal file → Executable file
View file

@ -2,7 +2,45 @@
namespace FederationLib;
use FederationLib\Managers\ClientManager;
use FederationLib\Managers\EventLogManager;
class FederationLib
{
/**
* @var ClientManager
*/
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
*
* @return ClientManager
*/
public function getClientManager(): ClientManager
{
return $this->client_manager;
}
/**
* @return EventLogManager
*/
public function getEventLogManager(): EventLogManager
{
return $this->event_log_manager;
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace FederationLib\Interfaces;
use FederationLib\Classes\Configuration\CacheServerConfiguration;
use FederationLib\Classes\Memcached;
use FederationLib\Classes\Redis;
interface CacheDriverInterface
{
/**
* Constructs a cache server driver
*
* @param CacheServerConfiguration $configuration
*/
public function __construct(CacheServerConfiguration $configuration);
/**
* Returns the configuration for the driver
*
* @return CacheServerConfiguration
*/
public function getConfiguration(): CacheServerConfiguration;
/**
* Returns the driver connection instance.
*
* @return Redis|Memcached
*/
public function getConnection(): Redis|Memcached;
/**
* Connects to the cache server
*
* @return void
*/
public function connect(): void;
/**
* Disconnects from the cache server
*
* @return void
*/
public function disconnect(): void;
}

View file

@ -0,0 +1,20 @@
<?php
namespace FederationLib\Interfaces;
interface EntityObjectInterface extends SerializableObjectInterface
{
/**
* Validates the given object and returns true if it is valid.
*
* @return bool
*/
public function validate(): bool;
/**
* Returns the standard federated address of the entity.
*
* @return string
*/
public function getFederatedAddress(): string;
}

View file

@ -0,0 +1,21 @@
<?php
namespace FederationLib\Interfaces;
interface SerializableObjectInterface
{
/**
* Returns an array representation of the object.
*
* @return array
*/
public function toArray(): array;
/**
* Constructs object from an array representation.
*
* @param array $array
* @return SerializableObjectInterface
*/
public static function fromArray(array $array): SerializableObjectInterface;
}

View file

@ -0,0 +1,503 @@
<?php
namespace FederationLib\Managers;
use Exception;
use FederationLib\Classes\Configuration;
use FederationLib\Classes\Database;
use FederationLib\Classes\Redis;
use FederationLib\Classes\Utilities;
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\FederationLib;
use FederationLib\Objects\Client;
use LogLib\Log;
use Symfony\Component\Uid\Uuid;
class ClientManager
{
/**
* @var FederationLib
*/
private FederationLib $federationLib;
/**
* ClientManager constructor.
*
* @param FederationLib $federationLib
*/
public function __construct(FederationLib $federationLib)
{
$this->federationLib = $federationLib;
}
/**
* Registers a client into the database, returns the UUID that was generated for the client.
*
* @param Client $client
* @return string
* @throws DatabaseException
*/
public function registerClient(Client $client): string
{
$qb = Database::getConnection()->createQueryBuilder();
$qb->insert(DatabaseTables::CLIENTS);
$uuid = Uuid::v4()->toRfc4122();
foreach($client->toArray() as $key => $value)
{
switch($key)
{
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;
}
}
try
{
$qb->executeStatement();
}
catch(Exception $e)
{
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;
}
/**
* Returns an existing client from the database.
*
* @param string|Client $uuid
* @return Client
* @throws ClientNotFoundException
* @throws DatabaseException
*/
public function getClient(string|Client $uuid): Client
{
if($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()));
}
}
$qb = Database::getConnection()->createQueryBuilder();
$qb->select('*');
$qb->from(DatabaseTables::CLIENTS);
$qb->where('uuid = :uuid');
$qb->setParameter('uuid', $uuid);
try
{
$result = $qb->executeQuery();
if($result->rowCount() === 0)
{
throw new ClientNotFoundException($uuid);
}
$client = Client::fromArray($result->fetchAssociative());
}
catch(ClientNotFoundException $e)
{
throw $e;
}
catch(Exception $e)
{
throw new DatabaseException('Failed to get Client: ' . $e->getMessage(), $e);
}
if(Configuration::isRedisCacheClientObjectsEnabled())
{
try
{
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()));
}
}
return $client;
}
/**
* 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.
*
* @param Client $client
* @return void
* @throws DatabaseException
*/
public function updateClient(Client $client): void
{
$cached_client = null;
if(Configuration::isRedisCacheClientObjectsEnabled())
{
try
{
if(Redis::getConnection()?->exists(sprintf('Client<%s>', $client->getUuid())))
{
$cached_client = Client::fromArray(Redis::getConnection()?->hGetAll(sprintf('Client<%s>', $client->getUuid())));
}
}
catch(Exception $e)
{
Log::warning('net.nosial.federationlib', sprintf('Failed to get Client from redis: %s', $e->getMessage()));
}
}
$qb = Database::getConnection()->createQueryBuilder();
$qb->update(DatabaseTables::CLIENTS);
$qb->set('updated_timestamp', ':updated_timestamp');
$qb->setParameter('updated_timestamp', time());
$qb->where('uuid = :uuid');
$qb->setParameter('uuid', $client->getUuid());
if($cached_client instanceof Client)
{
$data = array_diff($client->toArray(), $cached_client->toArray());
}
else
{
$data = $client->toArray();
}
foreach($data as $key => $value)
{
switch($key)
{
case 'uuid':
case 'created_timestamp':
case 'updated_timestamp':
case 'seen_timestamp':
break;
case 'name':
if($value === null || strlen($value) === 0 || !preg_match('/^[a-zA-Z0-9_\-]+$/', $value ))
{
break;
}
$qb->set($key, ':' . $key);
$qb->setParameter($key, substr($value, 0, 64));
break;
case 'description':
if($value !== null)
{
$qb->set($key, ':' . $key);
$qb->setParameter($key, substr($value, 0, 255));
}
break;
case 'enabled':
$qb->set($key, ':' . $key);
$qb->setParameter($key, $value ? 1 : 0);
break;
default:
$qb->set($key, ':' . $key);
$qb->setParameter($key, $value);
break;
}
}
try
{
$qb->executeStatement();
}
catch(Exception $e)
{
throw new DatabaseException('Failed to update client: ' . $e->getMessage(), $e);
}
if(Configuration::isRedisCacheClientObjectsEnabled())
{
// Update the differences in the cache
if($cached_client instanceof Client)
{
try
{
Redis::getConnection()?->hMSet((string)$client, array_diff($client->toArray(), $cached_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()));
}
}
else
{
try
{
Redis::getConnection()?->hMSet((string)$client, $client->toArray());
Redis::getConnection()?->expire((string)$client, $client->getUuid(), Configuration::getRedisCacheClientObjectsTTL());
}
catch(Exception $e)
{
Log::warning('net.nosial.federationlib', sprintf('Failed to cache Client in redis: %s', $e->getMessage()));
}
}
}
}
/**
* Updates a client's last seen timestamp.
*
* @param string|Client $uuid
* @return void
* @throws DatabaseException
* @noinspection PhpUnused
*/
public function updateLastSeen(string|Client $uuid): void
{
if($uuid instanceof Client)
{
$uuid = $uuid->getUuid();
}
$timestamp = time();
$qb = Database::getConnection()->createQueryBuilder();
$qb->update(DatabaseTables::CLIENTS);
$qb->set('seen_timestamp', ':seen_timestamp');
$qb->setParameter('seen_timestamp', $timestamp);
$qb->where('uuid = :uuid');
$qb->setParameter('uuid', $uuid);
try
{
$qb->executeStatement();
}
catch(Exception $e)
{
throw new DatabaseException('Failed to update last seen timestamp: ' . $e->getMessage(), $e);
}
// Update the 'seen_timestamp' only in the hash table in redis
if(Configuration::isRedisCacheClientObjectsEnabled())
{
try
{
Redis::getConnection()?->hSet(sprintf('Client<%s>', $uuid), 'seen_timestamp', $timestamp);
}
catch(Exception $e)
{
Log::warning('net.nosial.federationlib', sprintf('Failed to update last seen timestamp in redis: %s', $e->getMessage()));
}
}
}
/**
* Returns an array of clients based on the filter, order, and page.
*
* @param int $page
* @param string $filter
* @param string $order
* @param int $max_items
* @return array
* @throws DatabaseException
*/
public function listClients(int $page, string $filter=ListClientsFilter::CREATED_TIMESTAMP, string $order=FilterOrder::ASCENDING, int $max_items=100): array
{
$qb = Database::getConnection()->createQueryBuilder();
$qb->select(
'uuid', 'enabled', 'name', 'description', 'secret_totp', 'query_permission', 'update_permission',
'flags', 'created_timestamp', 'updated_timestamp', 'seen_timestamp'
);
$qb->from(DatabaseTables::CLIENTS);
$qb->setFirstResult(($page - 1) * $max_items);
$qb->setMaxResults($max_items);
if($order !== FilterOrder::ASCENDING && $order !== FilterOrder::DESCENDING)
{
throw new DatabaseException('Invalid order: ' . $order);
}
switch($filter)
{
case ListClientsFilter::CREATED_TIMESTAMP:
$qb->orderBy('created_timestamp', strtoupper($order));
break;
case ListClientsFilter::UPDATED_TIMESTAMP:
$qb->orderBy('updated_timestamp', strtoupper($order));
break;
case ListClientsFilter::SEEN_TIMESTAMP:
$qb->orderBy('seen_timestamp', strtoupper($order));
break;
case ListClientsFilter::ENABLED:
$qb->orderBy('enabled', strtoupper($order));
break;
default:
throw new DatabaseException('Invalid filter: ' . $filter);
}
try
{
$result = $qb->executeQuery();
$clients = [];
while($row = $result->fetchAssociative())
{
$clients[] = Client::fromArray($row);
}
}
catch(Exception $e)
{
throw new DatabaseException('Failed to list clients: ' . $e->getMessage(), $e->getCode(), $e);
}
unset($client);
return $clients;
}
/**
* Returns the total number of clients.
*
* @return int
* @throws DatabaseException
*/
public function getTotalClients(): int
{
$qb = Database::getConnection()->createQueryBuilder();
$qb->select('COUNT(uuid)');
$qb->from(DatabaseTables::CLIENTS);
try
{
$result = $qb->executeQuery();
$row = $result->fetchAssociative();
}
catch(Exception $e)
{
throw new DatabaseException('Failed to get total clients: ' . $e->getMessage(), $e->getCode(), $e);
}
return (int)$row['COUNT(uuid)'];
}
/**
* Returns teh total number of pages.
*
* @param int $max_items
* @return int
* @throws DatabaseException
*/
public function getTotalPages(int $max_items=100): int
{
return (int)ceil($this->getTotalClients() / $max_items);
}
/**
* Deletes an existing client from the database.
*
* @param string|Client $uuid
* @return void
* @throws DatabaseException
*/
public function deleteClient(string|Client $uuid): void
{
if($uuid instanceof Client)
{
$uuid = $uuid->getUuid();
}
$qb = Database::getConnection()->createQueryBuilder();
$qb->delete(DatabaseTables::CLIENTS);
$qb->where('uuid = :uuid');
$qb->setParameter('uuid', $uuid);
try
{
$qb->executeStatement();
}
catch(Exception $e)
{
throw new DatabaseException('Failed to delete client: ' . $e->getMessage(), $e->getCode(), $e);
}
// Invalidate the cache
if(Configuration::isRedisCacheClientObjectsEnabled())
{
try
{
Redis::getConnection()?->del(sprintf('Client<%s>', $uuid));
}
catch(Exception $e)
{
Log::warning('net.nosial.federationlib', sprintf('Failed to invalidate client cache in redis: %s', $e->getMessage()));
}
}
}
}

View file

@ -0,0 +1,172 @@
<?php
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;
/**
* @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,380 @@
<?php
/** @noinspection PhpMissingFieldTypeInspection */
namespace FederationLib\Objects;
use FederationCLI\Utilities;
use FederationLib\Classes\Security;
use FederationLib\Interfaces\SerializableObjectInterface;
class Client implements SerializableObjectInterface
{
/**
* @var string
*/
private $uuid;
/**
* @var bool
*/
private $enabled;
/**
* @var string|null
*/
private $name;
/**
* @var string|null
*/
private $description;
/**
* @var string|null
*/
private $secret_totp;
/**
* @var int
*/
private $query_permission;
/**
* @var int
*/
private $update_permission;
/**
* @var string[]
*/
private $flags;
/**
* @var int
*/
private $created_timestamp;
/**
* @var int
*/
private $updated_timestamp;
/**
* @var int
*/
private $seen_timestamp;
/**
* Client constructor.
*/
public function __construct()
{
$this->enabled = true;
$this->flags = [];
$this->query_permission = 1;
$this->update_permission = 0;
}
/**
* Returns the client's unique ID.
*
* @return string
*/
public function getUuid(): string
{
return $this->uuid;
}
/**
* @return bool
*/
public function isEnabled(): bool
{
return $this->enabled;
}
/**
* @param bool $enabled
*/
public function setEnabled(bool $enabled): void
{
$this->enabled = $enabled;
}
/**
* @return string|null
*/
public function getName(): string
{
return $this->name;
}
/**
* @param string|null $name
*/
public function setName(?string $name): void
{
$this->name = $name;
}
/**
* @return string|null
*/
public function getDescription(): ?string
{
return $this->description;
}
/**
* @param string|null $description
*/
public function setDescription(?string $description): void
{
$this->description = $description;
}
/**
* Enables authentication for the client, returns the secret.
*
* @return string
*/
public function enableAuthentication(): string
{
$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;
}
/**
* Validates the given authentication code against the client's secret.
*
* @param string|null $authentication_code
* @param int|null $timestamp
* @return bool
*/
public function validateAuthentication(?string $authentication_code=null, ?int $timestamp=null): bool
{
if($this->secret_totp === null)
{
// Always authenticate if authentication is disabled.
return true;
}
if($authentication_code === null)
{
// Authentication code not provided but required.
return false;
}
// Authentication Code: sha1(client_id + totp_code)
return hash('sha1', $this->uuid . Security::generateCode($this->secret_totp, $timestamp)) === $authentication_code;
}
/**
* Returns the client's query permission level.
*
* @return int
*/
public function getQueryPermission(): 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;
}
/**
* Returns the client's flags.
*
* @return string[]
*/
public function getFlags(): array
{
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.
*
* @param string $flag
* @return bool
*/
public function hasFlag(string $flag): bool
{
return in_array($flag, $this->flags);
}
/**
* Returns the Unix Timestamp for when the client was last created on the server
*
* @return int
*/
public function getCreatedTimestamp(): int
{
return $this->created_timestamp;
}
/**
* Returns the Unix Timestamp for when the client was last updated on the server.
*
* @return int
*/
public function getUpdatedTimestamp(): int
{
return $this->updated_timestamp;
}
/**
* Returns the Unix Timestamp for when the client was last seen by the server.
*
* @return int
*/
public function getSeenTimestamp(): int
{
return $this->seen_timestamp;
}
/**
* Returns an array representation of the object
*
* @return array
*/
public function toArray(): array
{
$flags = null;
if($this->flags !== null && count($this->flags) > 0)
{
$flags = implode(',', $this->flags);
}
return [
'uuid' => $this->uuid,
'enabled' => $this->enabled,
'name' => $this->name,
'description' => $this->description,
'secret_totp' => $this->secret_totp,
'query_permission' => $this->query_permission,
'update_permission' => $this->update_permission,
'flags' => $flags,
'created_timestamp' => $this->created_timestamp,
'updated_timestamp' => $this->updated_timestamp,
'seen_timestamp' => $this->seen_timestamp,
];
}
/**
* Constructs object from an array representation.
*
* @param array $array
* @return Client
*/
public static function fromArray(array $array): Client
{
$client = new self();
$client->uuid = $array['uuid'] ?? null;
$client->enabled = Utilities::parseBoolean($array['enabled']);
$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;
if(isset($array['flags']))
{
$client->flags = explode(',', $array['flags']);
}
else
{
$client->flags = [];
}
$client->created_timestamp = $array['created_timestamp'] ?? 0;
$client->updated_timestamp = $array['updated_timestamp'] ?? 0;
$client->seen_timestamp = $array['seen_timestamp'] ?? 0;
return $client;
}
/**
* Returns the object identifier.
*
* @return string
*/
public function __toString(): string
{
return sprintf('Client<%s>', $this->uuid ?? spl_object_hash($this));
}
}

View file

@ -0,0 +1,78 @@
<?php
/** @noinspection PhpMissingFieldTypeInspection */
namespace FederationLib\Objects;
class ParsedFederatedAddress
{
/**
* @var string
*/
private $source;
/**
* @var string
*/
private $peer_type;
/**
* @var string
*/
private $unique_identifier;
/**
* Public Constructor, parses the federated address and sets the properties
*
* @param string $federated_address
*/
public function __construct(string $federated_address)
{
preg_match("/(?<source>[a-z0-9]+)\.(?<type>[a-z0-9]+):(?<id>.+)/", $federated_address, $matches);
$this->source = $matches['source'];
$this->peer_type = $matches['type'];
$this->unique_identifier = $matches['id'];
}
/**
* Returns the platform source of the peer
*
* @return string
*/
public function getSource(): string
{
return $this->source;
}
/**
* Returns the peer type of the platform
*
* @return string
*/
public function getPeerType(): string
{
return $this->peer_type;
}
/**
* Returns the Unique Identifier of the peer
*
* @return string
*/
public function getUniqueIdentifier(): string
{
return $this->unique_identifier;
}
/**
* Returns the Standard Federated Address of the peer
*
* @return string
*/
public function getAddress(): string
{
return sprintf('%s.%s:%s', $this->source, $this->peer_type, $this->unique_identifier);
}
}

View file

@ -0,0 +1,138 @@
<?php
/** @noinspection PhpMissingFieldTypeInspection */
namespace FederationLib\Objects;
use FederationLib\Interfaces\SerializableObjectInterface;
class Peer implements SerializableObjectInterface
{
/**
* @var string
*/
private $federated_address;
/**
* @var string
*/
private $client_first_seen;
/**
* @var string
*/
private $client_last_seen;
/**
* @var string|null
*/
private $active_restriction;
/**
* @var int
*/
private $discovered_timestamp;
/**
* @var int
*/
private $seen_timestamp;
/**
* Returns the Standard Federated Address for the peer
*
* @return string
*/
public function getFederatedAddress(): string
{
return $this->federated_address;
}
/**
* Returns the client UUID that first saw the peer
*
* @return string
*/
public function getClientFirstSeen(): string
{
return $this->client_first_seen;
}
/**
* Returns the client UUID that last saw the peer
*
* @return string
*/
public function getClientLastSeen(): string
{
return $this->client_last_seen;
}
/**
* Optional. Returns the active restriction ID for the peer
* This is only available if the peer is currently restricted
*
* @return string|null
*/
public function getActiveRestriction(): ?string
{
return $this->active_restriction;
}
/**
* Returns the Unix Timestamp for when the peer was first discovered by a client
*
* @return int
*/
public function getDiscoveredTimestamp(): int
{
return $this->discovered_timestamp;
}
/**
* Returns the Unix Timestamp for when the peer was last seen by a client
*
* @return int
*/
public function getSeenTimestamp(): int
{
return $this->seen_timestamp;
}
/**
* Returns an array representation of the object
*
* @return array
*/
public function toArray(): array
{
return [
'federated_address' => $this->federated_address,
'client_first_seen' => $this->client_first_seen,
'client_last_seen' => $this->client_last_seen,
'active_restriction' => $this->active_restriction,
'discovered_timestamp' => $this->discovered_timestamp,
'seen_timestamp' => $this->seen_timestamp,
];
}
/**
* Constructs object from an array representation
*
* @param array $array
* @return Peer
*/
public static function fromArray(array $array): Peer
{
$object = new self();
$object->federated_address = $array['federated_address'] ?? null;
$object->client_first_seen = $array['client_first_seen'] ?? null;
$object->client_last_seen = $array['client_last_seen'] ?? null;
$object->active_restriction = $array['active_restriction'] ?? null;
$object->discovered_timestamp = $array['discovered_timestamp'] ?? null;
$object->seen_timestamp = $array['seen_timestamp'] ?? null;
return $object;
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace FederationLib\Objects;
class QueryDocument
{
private $subject_type;
private $client_id;
private $client_totp_signature;
private $timstamp;
private $platform;
private $event_type;
private $channel_peer;
private $resent_from_peer;
private
}

14
src/README.md Normal file → Executable file
View file

@ -1,3 +1,15 @@
# FederationLib
Coming Soon ...
Spam is a persistent problem on the internet, affecting various communication channels such as email, social media,
messaging apps, and more. Spammers use different techniques to distribute spam, such as creating fake accounts, using
automated bots or exploiting vulnerabilities in the system, this extends towards bad actors who may use these techniques
to harm children, spread misinformation, or even commit financial fraud. In order to combat these issues, while different
systems have developed methods for identifying and classifying spam, there is no standard or centralized way to share
this information. This results in duplication of effort and inconsistencies in spam classification across different
systems.
The objective of this project is to develop a decentralized, open-source, and privacy-preserving spam detection and
classification system. This system will act as a decentralized authority for internet spam classification. The server
will allow different organizations and individuals to contribute to the common database, to which the public may query
the common database to check if the given content or entity could be classified as spam and provide a confidence score
alongside with evidence to support the classification.