Add VariableManager, RpcClient classes, and cache enhancements

This commit is contained in:
netkas 2024-09-30 03:00:02 -04:00
parent 38092a639e
commit e55f4d57f9
27 changed files with 606 additions and 56 deletions

View file

@ -5,6 +5,7 @@ namespace Socialbox\Classes\CacheLayer;
use Memcached;
use RuntimeException;
use Socialbox\Abstracts\CacheLayer;
use Socialbox\Classes\Configuration;
class MemcachedCacheLayer extends CacheLayer
{
@ -12,11 +13,8 @@ class MemcachedCacheLayer extends CacheLayer
/**
* Memcached cache layer constructor.
*
* @param string $host The Memcached server host.
* @param int $port The Memcached server port.
*/
public function __construct(string $host, int $port)
public function __construct()
{
if (!extension_loaded('memcached'))
{
@ -24,9 +22,10 @@ class MemcachedCacheLayer extends CacheLayer
}
$this->memcached = new Memcached();
if (!$this->memcached->addServer($host, $port))
$this->memcached->addServer(Configuration::getConfiguration()['cache']['host'], (int)Configuration::getConfiguration()['cache']['port']);
if(Configuration::getConfiguration()['cache']['username'] !== null || Configuration::getConfiguration()['cache']['password'] !== null)
{
throw new RuntimeException('Failed to connect to the Memcached server.');
$this->memcached->setSaslAuthData(Configuration::getConfiguration()['cache']['username'], Configuration::getConfiguration()['cache']['password']);
}
}
@ -80,6 +79,25 @@ class MemcachedCacheLayer extends CacheLayer
return $this->memcached->getResultCode() === Memcached::RES_SUCCESS;
}
/**
* @inheritDoc
*/
public function getPrefixCount(string $prefix): int
{
$stats = $this->memcached->getStats();
$count = 0;
foreach ($stats as $server => $data)
{
if (str_starts_with($server, $prefix))
{
$count += $data['curr_items'];
}
}
return $count;
}
/**
* @inheritDoc
*/

View file

@ -6,6 +6,7 @@ use Redis;
use RedisException;
use RuntimeException;
use Socialbox\Abstracts\CacheLayer;
use Socialbox\Classes\Configuration;
class RedisCacheLayer extends CacheLayer
{
@ -13,12 +14,8 @@ class RedisCacheLayer extends CacheLayer
/**
* Redis cache layer constructor.
*
* @param string $host The Redis server host.
* @param int $port The Redis server port.
* @param string|null $password Optional. The Redis server password.
*/
public function __construct(string $host, int $port, ?string $password=null)
public function __construct()
{
if (!extension_loaded('redis'))
{
@ -29,10 +26,15 @@ class RedisCacheLayer extends CacheLayer
try
{
$this->redis->connect($host, $port);
if ($password !== null)
$this->redis->connect(Configuration::getConfiguration()['cache']['host'], (int)Configuration::getConfiguration()['cache']['port']);
if (Configuration::getConfiguration()['cache']['password'] !== null)
{
$this->redis->auth($password);
$this->redis->auth(Configuration::getConfiguration()['cache']['password']);
}
if (Configuration::getConfiguration()['cache']['database'] !== 0)
{
$this->redis->select((int)Configuration::getConfiguration()['cache']['database']);
}
}
catch (RedisException $e)
@ -101,6 +103,21 @@ class RedisCacheLayer extends CacheLayer
}
}
/**
* @inheritDoc
*/
public function getPrefixCount(string $prefix): int
{
try
{
return count($this->redis->keys($prefix . '*'));
}
catch (RedisException $e)
{
throw new RuntimeException('Failed to get the count of keys with the specified prefix in the Redis cache.', 0, $e);
}
}
/**
* @inheritDoc
*/

View file

@ -2,13 +2,18 @@
namespace Socialbox\Classes\CliCommands;
use Exception;
use LogLib\Log;
use PDOException;
use Socialbox\Abstracts\CacheLayer;
use Socialbox\Classes\Configuration;
use Socialbox\Classes\Cryptography;
use Socialbox\Classes\Database;
use Socialbox\Classes\Resources;
use Socialbox\Enums\DatabaseObjects;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Interfaces\CliCommandInterface;
use Socialbox\Managers\VariableManager;
class InitializeCommand implements CliCommandInterface
{
@ -23,7 +28,14 @@ class InitializeCommand implements CliCommandInterface
return 1;
}
print("Initializing Socialbox...\n");
Log::info('net.nosial.socialbox', 'Initializing Socialbox...');
if(Configuration::getConfiguration()['cache']['enabled'])
{
Log::verbose('net.nosial.socialbox', 'Clearing cache layer...');
CacheLayer::getInstance()->clear();
}
foreach(DatabaseObjects::casesOrdered() as $object)
{
Log::verbose('net.nosial.socialbox', "Initializing database object {$object->value}");
@ -46,13 +58,34 @@ class InitializeCommand implements CliCommandInterface
return 1;
}
}
catch(\Exception $e)
catch(Exception $e)
{
Log::error('net.nosial.socialbox', "Failed to initialize database object {$object->value}: {$e->getMessage()}", $e);
return 1;
}
}
try
{
if(!VariableManager::variableExists('PUBLIC_KEY') || !VariableManager::variableExists('PRIVATE_KEY'))
{
Log::info('net.nosial.socialbox', 'Generating new key pair...');
$keyPair = Cryptography::generateKeyPair();
VariableManager::setVariable('PUBLIC_KEY', $keyPair->getPublicKey());
VariableManager::setVariable('PRIVATE_KEY', $keyPair->getPrivateKey());
Log::info('net.nosial.socialbox', 'Set the DNS TXT record for the public key to the following value:');
Log::info('net.nosial.socialbox', "socialbox-key={$keyPair->getPublicKey()}");
}
}
catch(DatabaseOperationException $e)
{
Log::error('net.nosial.socialbox', "Failed to generate key pair: {$e->getMessage()}", $e);
return 1;
}
Log::info('net.nosial.socialbox', 'Socialbox has been initialized successfully');
return 0;
}

View file

@ -22,6 +22,18 @@ class Configuration
$config->setDefault('database.username', 'root');
$config->setDefault('database.password', 'root');
$config->setDefault('database.name', 'test');
$config->setDefault('cache.enabled', false);
$config->setDefault('cache.engine', 'redis');
$config->setDefault('cache.host', '127.0.0.1');
$config->setDefault('cache.port', 6379);
$config->setDefault('cache.username', null);
$config->setDefault('cache.password', null);
$config->setDefault('cache.database', 0);
$config->setDefault('cache.variables.enabled', true);
$config->setDefault('cache.variables.ttl', 3600);
$config->setDefault('cache.variables.max', 1000);
$config->save();
self::$configuration = $config->getConfiguration();

View file

@ -0,0 +1,11 @@
create table variables
(
name varchar(255) not null comment 'The name of the variable'
primary key comment 'The unique index for the variable name',
value text null comment 'The value of the variable',
`read_only` tinyint(1) default 0 not null comment 'Boolean indicator if the variable is read only',
created timestamp default current_timestamp() not null comment 'The Timestamp for when this record was created',
updated timestamp null comment 'The Timestamp for when this record was last updated',
constraint variables_name_uindex
unique (name) comment 'The unique index for the variable name'
);

View file

@ -0,0 +1,88 @@
<?php
namespace Socialbox\Classes;
use Socialbox\Classes\ServerResolver;
use Socialbox\Enums\StandardHeaders;
use Socialbox\Exceptions\ResolutionException;
use Socialclient\Exceptions\RpcRequestException;
class RpcClient
{
private const string CLIENT_NAME = 'Socialbox PHP';
private const string CLIENT_VERSION = '1.0';
private const string CONTENT_TYPE = 'application/json';
private string $domain;
private string $endpoint;
private string $serverPublicKey;
/**
* @throws ResolutionException
*/
public function __construct(string $domain)
{
$resolved = ServerResolver::resolveDomain($domain);
$this->domain = $domain;
$this->endpoint = $resolved->getEndpoint();
$this->serverPublicKey = $resolved->getPublicKey();
$this->clientPrivateKey = null;
}
public function getDomain(): string
{
return $this->domain;
}
public function getEndpoint(): string
{
return $this->endpoint;
}
public function getServerPublicKey(): string
{
return $this->serverPublicKey;
}
public function sendRequest(array $data)
{
$ch = curl_init($this->endpoint);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, Utilities::jsonEncode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
Utilities::generateHeader(StandardHeaders::CLIENT_NAME, self::CLIENT_NAME),
Utilities::generateHeader(StandardHeaders::CLIENT_VERSION, self::CLIENT_VERSION),
Utilities::generateHeader(StandardHeaders::CONTENT_TYPE, self::CONTENT_TYPE)
]);
curl_setopt($ch, CURLOPT_HEADER, true);
$response = curl_exec($ch);
if (curl_errno($ch))
{
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
// Separate headers and body
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$response_body = substr($response, $header_size);
curl_close($ch);
// Throw exception with response body as message and status code as code
throw new RpcRequestException($response_body, $statusCode);
}
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
// Separate headers and body
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$response_headers = substr($response, 0, $header_size);
$response_body = substr($response, $header_size);
curl_close($ch);
}
}

View file

@ -8,6 +8,7 @@ use Socialbox\Enums\StandardHeaders;
use Socialbox\Exceptions\CryptographyException;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\RpcException;
use Socialbox\Exceptions\StandardException;
use Socialbox\Managers\SessionManager;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\RpcRequest;
@ -85,11 +86,6 @@ class RpcHandler
try
{
if(!SessionManager::sessionExists($clientRequest->getSessionUuid()))
{
throw new RpcException('Session UUID not found', 404);
}
$session = SessionManager::getSession($clientRequest->getSessionUuid());
// Verify the signature of the request
@ -98,6 +94,10 @@ class RpcHandler
throw new RpcException('Request signature check failed', 400);
}
}
catch(StandardException $e)
{
throw new RpcException($e->getMessage(), 400);
}
catch(CryptographyException $e)
{
throw new RpcException('Request signature check failed (Cryptography Error)', 400, $e);

View file

@ -0,0 +1,52 @@
<?php
namespace Socialbox\Classes;
use Socialbox\Exceptions\ResolutionException;
use Socialbox\Objects\ResolvedServer;
class ServerResolver
{
/**
* Resolves a given domain to fetch the RPC endpoint and public key from its DNS TXT records.
*
* @param string $domain The domain to be resolved.
* @return ResolvedServer An instance of ResolvedServer containing the endpoint and public key.
* @throws ResolutionException If the DNS TXT records cannot be resolved or if required information is missing.
*/
public static function resolveDomain(string $domain): ResolvedServer
{
$txtRecords = dns_get_record($domain, DNS_TXT);
if ($txtRecords === false)
{
throw new ResolutionException(sprintf("Failed to resolve DNS TXT records for %s", $domain));
}
$endpoint = null;
$publicKey = null;
foreach ($txtRecords as $txt)
{
if (isset($txt['txt']) && str_starts_with($txt['txt'], 'socialbox='))
{
$endpoint = substr($txt['txt'], strlen('socialbox='));
}
elseif (isset($txt['txt']) && str_starts_with($txt['txt'], 'socialbox-key='))
{
$publicKey = substr($txt['txt'], strlen('socialbox-key='));
}
}
if ($endpoint === null)
{
throw new ResolutionException(sprintf("Failed to resolve RPC endpoint for %s", $domain));
}
if ($publicKey === null)
{
throw new ResolutionException(sprintf("Failed to resolve public key for %s", $domain));
}
return new ResolvedServer($endpoint, $publicKey);
}
}

View file

@ -6,7 +6,6 @@ use InvalidArgumentException;
use Socialbox\Abstracts\Method;
use Socialbox\Enums\StandardError;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\StandardException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Managers\SessionManager;
use Socialbox\Objects\ClientRequest;

View file

@ -4,6 +4,7 @@ namespace Socialbox\Classes;
use InvalidArgumentException;
use RuntimeException;
use Socialbox\Enums\StandardHeaders;
class Utilities
{
@ -34,6 +35,18 @@ class Utilities
return $decoded;
}
public static function jsonEncode(mixed $data): string
{
try
{
return json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
}
catch(\JsonException $e)
{
throw new \RuntimeException("Failed to encode json input", $e);
}
}
/**
* Encodes the given data in Base64.
*
@ -117,4 +130,9 @@ class Utilities
$e->getTraceAsString()
);
}
public static function generateHeader(StandardHeaders $header, string $value): string
{
return $header->value . ': ' . $value;
}
}