From 0f5c8b40e224781361528d9e74f10bae0cc15801 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 24 Oct 2024 13:55:21 -0400 Subject: [PATCH 001/420] Refactor cryptography handling and improve test coverage --- .idea/php.xml | 2 +- .idea/sqldialects.xml | 1 - src/Socialbox/Abstracts/Method.php | 2 +- src/Socialbox/Classes/Cryptography.php | 20 +++++++++++-- src/Socialbox/Classes/RpcHandler.php | 28 +++++++++++-------- .../Classes/StandardMethods/CreateSession.php | 4 +-- src/Socialbox/Classes/Utilities.php | 2 +- src/Socialbox/Managers/SessionManager.php | 4 +-- .../Objects/{ => Database}/SessionRecord.php | 2 +- src/Socialbox/Socialbox.php | 1 - tests/Socialbox/Classes/CryptographyTest.php | 22 +++++++++++++-- tests/Socialbox/Classes/client_private.der | 1 + tests/Socialbox/Classes/client_public.der | 1 + tests/Socialbox/Classes/content.txt | 4 +++ .../{private.der => server_private.der} | 0 .../Classes/{public.der => server_public.der} | 0 .../Classes/{secret.txt => server_secret.txt} | 0 .../Socialbox/Managers/SessionManagerTest.php | 2 +- 18 files changed, 66 insertions(+), 30 deletions(-) rename src/Socialbox/Objects/{ => Database}/SessionRecord.php (98%) create mode 100644 tests/Socialbox/Classes/client_private.der create mode 100644 tests/Socialbox/Classes/client_public.der create mode 100644 tests/Socialbox/Classes/content.txt rename tests/Socialbox/Classes/{private.der => server_private.der} (100%) rename tests/Socialbox/Classes/{public.der => server_public.der} (100%) rename tests/Socialbox/Classes/{secret.txt => server_secret.txt} (100%) diff --git a/.idea/php.xml b/.idea/php.xml index 6e91387..cee48e7 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -123,7 +123,7 @@ - + diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml index 12d6855..4609cff 100644 --- a/.idea/sqldialects.xml +++ b/.idea/sqldialects.xml @@ -7,6 +7,5 @@ - \ No newline at end of file diff --git a/src/Socialbox/Abstracts/Method.php b/src/Socialbox/Abstracts/Method.php index 3dbb855..d3f5971 100644 --- a/src/Socialbox/Abstracts/Method.php +++ b/src/Socialbox/Abstracts/Method.php @@ -8,8 +8,8 @@ use Socialbox\Exceptions\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\SessionManager; use Socialbox\Objects\ClientRequest; +use Socialbox\Objects\Database\SessionRecord; use Socialbox\Objects\RpcRequest; -use Socialbox\Objects\SessionRecord; abstract class Method { diff --git a/src/Socialbox/Classes/Cryptography.php b/src/Socialbox/Classes/Cryptography.php index 20d242f..5de4846 100644 --- a/src/Socialbox/Classes/Cryptography.php +++ b/src/Socialbox/Classes/Cryptography.php @@ -108,14 +108,28 @@ class Cryptography */ public static function verifyContent(string $content, string $signature, string $publicKey): bool { - $publicKey = openssl_pkey_get_public(self::derToPem(Utilities::base64decode($publicKey), self::PEM_PUBLIC_HEADER)); - + try + { + $publicKey = openssl_pkey_get_public(self::derToPem(Utilities::base64decode($publicKey), self::PEM_PUBLIC_HEADER)); + } + catch(InvalidArgumentException $e) + { + throw new CryptographyException('Failed to decode public key: ' . $e->getMessage()); + } + if (!$publicKey) { throw new CryptographyException('Invalid public key: ' . openssl_error_string()); } - return openssl_verify($content, base64_decode($signature), $publicKey, self::HASH_ALGORITHM) === 1; + try + { + return openssl_verify($content, Utilities::base64decode($signature), $publicKey, self::HASH_ALGORITHM) === 1; + } + catch(InvalidArgumentException $e) + { + throw new CryptographyException('Failed to verify content: ' . $e->getMessage()); + } } /** diff --git a/src/Socialbox/Classes/RpcHandler.php b/src/Socialbox/Classes/RpcHandler.php index df24724..458a57c 100644 --- a/src/Socialbox/Classes/RpcHandler.php +++ b/src/Socialbox/Classes/RpcHandler.php @@ -2,10 +2,10 @@ namespace Socialbox\Classes; +use Exception; use InvalidArgumentException; use RuntimeException; use Socialbox\Enums\StandardHeaders; -use Socialbox\Exceptions\CryptographyException; use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\RpcException; use Socialbox\Exceptions\StandardException; @@ -87,25 +87,31 @@ class RpcHandler try { $session = SessionManager::getSession($clientRequest->getSessionUuid()); - - // Verify the signature of the request - if(!Cryptography::verifyContent($clientRequest->getHash(), $clientRequest->getSignature(), $session->getPublicKey())) - { - 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); - } catch(DatabaseOperationException $e) { throw new RpcException('Failed to verify session', 500, $e); } + + try + { + if(!Cryptography::verifyContent($clientRequest->getHash(), $clientRequest->getSignature(), $session->getPublicKey())) + { + throw new RpcException('Request signature check failed', 400); + } + } + catch(RpcException $e) + { + throw $e; + } + catch(Exception $e) + { + throw new RpcException('Request signature check failed (Cryptography Error): ' . $e->getMessage(), 400, $e); + } } return $clientRequest; diff --git a/src/Socialbox/Classes/StandardMethods/CreateSession.php b/src/Socialbox/Classes/StandardMethods/CreateSession.php index 9d9dcfe..9b11360 100644 --- a/src/Socialbox/Classes/StandardMethods/CreateSession.php +++ b/src/Socialbox/Classes/StandardMethods/CreateSession.php @@ -40,8 +40,6 @@ class CreateSession extends Method return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, $e->getMessage()); } - return $rpcRequest->produceResponse([ - 'uuid' => $uuid - ]); + return $rpcRequest->produceResponse($uuid); } } \ No newline at end of file diff --git a/src/Socialbox/Classes/Utilities.php b/src/Socialbox/Classes/Utilities.php index 5f4b44b..a7c1fa2 100644 --- a/src/Socialbox/Classes/Utilities.php +++ b/src/Socialbox/Classes/Utilities.php @@ -43,7 +43,7 @@ class Utilities } catch(\JsonException $e) { - throw new \RuntimeException("Failed to encode json input", $e); + throw new InvalidArgumentException("Failed to encode json input", $e); } } diff --git a/src/Socialbox/Managers/SessionManager.php b/src/Socialbox/Managers/SessionManager.php index 4a9ab6b..d14d511 100644 --- a/src/Socialbox/Managers/SessionManager.php +++ b/src/Socialbox/Managers/SessionManager.php @@ -9,12 +9,11 @@ use PDOException; use Socialbox\Classes\Cryptography; use Socialbox\Classes\Database; - use Socialbox\Classes\Utilities; use Socialbox\Enums\SessionState; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\StandardException; - use Socialbox\Objects\SessionRecord; + use Socialbox\Objects\Database\SessionRecord; use Symfony\Component\Uid\Uuid; class SessionManager @@ -41,7 +40,6 @@ throw new InvalidArgumentException('The given public key is invalid', 400); } - $publicKey = Utilities::base64decode($publicKey); $uuid = Uuid::v4()->toRfc4122(); try diff --git a/src/Socialbox/Objects/SessionRecord.php b/src/Socialbox/Objects/Database/SessionRecord.php similarity index 98% rename from src/Socialbox/Objects/SessionRecord.php rename to src/Socialbox/Objects/Database/SessionRecord.php index 5b9ee23..19c1b6d 100644 --- a/src/Socialbox/Objects/SessionRecord.php +++ b/src/Socialbox/Objects/Database/SessionRecord.php @@ -1,6 +1,6 @@ produceError(StandardError::INTERNAL_SERVER_ERROR, Utilities::throwableToString($e)); diff --git a/tests/Socialbox/Classes/CryptographyTest.php b/tests/Socialbox/Classes/CryptographyTest.php index 483ba13..fa26a49 100644 --- a/tests/Socialbox/Classes/CryptographyTest.php +++ b/tests/Socialbox/Classes/CryptographyTest.php @@ -126,7 +126,7 @@ class CryptographyTest extends TestCase public function testEncryptFromFile() { - $file_path = __DIR__ . DIRECTORY_SEPARATOR . 'public.der'; + $file_path = __DIR__ . DIRECTORY_SEPARATOR . 'server_public.der'; $content = "Test Content"; $encryptedContent = Cryptography::encryptContent($content, file_get_contents($file_path)); @@ -139,8 +139,8 @@ class CryptographyTest extends TestCase public function testDecryptFromFile() { - $private_key_file = __DIR__ . DIRECTORY_SEPARATOR . 'private.der'; - $content = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'secret.txt'); + $private_key_file = __DIR__ . DIRECTORY_SEPARATOR . 'server_private.der'; + $content = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'server_secret.txt'); try { @@ -200,4 +200,20 @@ class CryptographyTest extends TestCase $this->assertFalse($result); } + public function testRequestSigning() + { + $client_private_der = __DIR__ . DIRECTORY_SEPARATOR . 'client_private.der'; + $client_public_der = __DIR__ . DIRECTORY_SEPARATOR . 'client_public.der'; + $content_file = __DIR__ . DIRECTORY_SEPARATOR . 'content.txt'; + + $hash = hash('sha1', file_get_contents($content_file)); + $this->assertEquals('fa2415f0735a8aa151195688852178e8fd6e77c5', $hash); + + $signature = Cryptography::signContent($hash, file_get_contents($client_private_der)); + $this->assertEquals("Gcnijq7V8AYXgdk/eP9IswXN7831FevlBNDTKN60Ku7xesPDuPX8e55+38WFGCQ87DbeiIr+61XIDoN4+bTM4Wl0YSUe7oHV9BBnBqGhyZTntDPedUYUomrF3IRcpVRK0SbQSRaYucIp/ZsSHdbQgQBtDCvH5pK1+5g+VK9ZFT16Isvk0PhMjZiLkUYxUklFuzak7agWiS3wllFPqYSM6ri0RF+5I5JbnR9fUAOfhOceax//5H7d2WsdLj6DwtuY+eL5WyHxSmGA04YeQF3JgOGJ3WX2DSH8L0zA7pkGOjz5y1Nu6+0U6KRUXcezU/iM4zy5OJOnD5eJH4pYZizkiA==", $signature); + + $result = Cryptography::verifyContent($hash, $signature, file_get_contents($client_public_der)); + $this->assertTrue($result); + } + } diff --git a/tests/Socialbox/Classes/client_private.der b/tests/Socialbox/Classes/client_private.der new file mode 100644 index 0000000..249edf1 --- /dev/null +++ b/tests/Socialbox/Classes/client_private.der @@ -0,0 +1 @@ +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDZb4K+28kKYe1CvHPWHJALJFS396HOFmBv+anpAVWMDGBUyAWbWEqxmTAV17cBHiICjDDCNFBpOZLWzIiUpdWKA0Jo+Vu9zgWSPUyGe/Lik4GFNZ38gfolfdKGnLNFnn4nFR/fsZQ7hg4wWDarJmhJ+ZSLShOz2uIb4LaKk2qy12c6Zepufgrbk9TwWZQiXkzqBWbrZDpw0pp50CzoIwEnYJ+a7vhb98jpeS+Jjnp5zWlFjv9RgzOQUOwwOK4We2gNAVeFC5BP9trklpTh1bJlit4CECH68fCGjgoTOU92UbgucgyA4O5FVPGQYPAMuiZMGFaqXE2E7z1XwYIMAL4VAgMBAAECggEAAKiJz3CYuO+gGnL+F7qjaSXCUE8VvPfoCwuNYHNEFXo9DJBmnL7EU2WrYG+wARCP7O7qd0dEidx9u36ytjyCcKT4nYni8lM1zU7rVvbnLbsuRZS/4RO/RaYfPxig94fDfSeJ2ma0i7G56onj+MBbyTZarZ7Bf8hpcmKg9pkNEcEVcklNIwwbXKBOGq75Vka/+W56JZKJD3G9YmfrAO5RGF1prh93MRXlxlN/91k/m2pqkN9xYofepn0ePmI8Ci18jrMpJbmeu8BkypzgvC/5EfHipn7y/yJ215o/EtB575muz2zngRXe+GVO5lB5d5PuEwmXoaV5o3BqkIcb3aiz4QKBgQD7P1AE2/3oATNUF1FwlXzvdCS7M2BB28jQWjzJvHus1d1+qA2StWPgCPG2D/YTtHPI3xefBnAmeSIFCFEub0YLONbRvtQAZdTt5SAaZuUyMprqD1sCUHCizyVO0wHxo3DS0sIFmo/Lpc+jnYHn3KcuRPRJk3ncZNCQhy9a/rrnxQKBgQDdjHY82YdkWQWj/xM1EuVtkVVeCJWJ6tSDn+Uq8d+hXILFAQ47GOUbzj4Ty4qGgsAgsaAGqja5t6CE+fYs8Q34FsxTsYgIRm0VXqtPm4aYTQ4PwKbmMPEOgEsXBywe5Y+QB0u/WuNyhgwgYP5cy1IS3HA1HmbTisi0zLEfkVWSEQKBgCuP36zoA88NHjwvStSNZrsR1SiMEN16YQgXDUEhKARglGXYd3n/b1Cx3E7n14+1Evo6DBtrf1h8WjSrK4A0lN1vPnfhcVqcTV3uAzHwsz6P3aJFhU8SaWUhK2POXCDsaKx1FGTqVpJFrom8zoBIFsiD9iMnqdJXvH3CoqhRUFDNAoGAEJdwU2ZHCXDRR1LW8WaU3/u+VOh3qnh3qdPTqb+ra74t3OsTUcGvhsGPTJQ1r5UjJk+nGFiu+IGT9+FwWjVDQo0SiEIHWfdMPAl28uNG1SkQIIXg+eQ4aUmaVgMnfrjaY4LoXVBFMFJxngslgXWIk/kGPjQkpzsBhOi/awnLSsECgYEAkSEb3CXfq1r/8qXMTzI+A9CGPr++aC2H6ytFNGJq4J+P40u1tcsfkwNGcQ0Hp+Qz3FHBYFuMxtjXDq0QSvVKEhdV9bjlZhTqN3lqWcCukU3ESqRbxsIj9izuncpxSP7G19WEU0anGD9ev+QWYdHPTBY9nn1+H0tkJjqh4XkRBuY= \ No newline at end of file diff --git a/tests/Socialbox/Classes/client_public.der b/tests/Socialbox/Classes/client_public.der new file mode 100644 index 0000000..58b132e --- /dev/null +++ b/tests/Socialbox/Classes/client_public.der @@ -0,0 +1 @@ +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2W+CvtvJCmHtQrxz1hyQCyRUt/ehzhZgb/mp6QFVjAxgVMgFm1hKsZkwFde3AR4iAowwwjRQaTmS1syIlKXVigNCaPlbvc4Fkj1Mhnvy4pOBhTWd/IH6JX3ShpyzRZ5+JxUf37GUO4YOMFg2qyZoSfmUi0oTs9riG+C2ipNqstdnOmXqbn4K25PU8FmUIl5M6gVm62Q6cNKaedAs6CMBJ2Cfmu74W/fI6XkviY56ec1pRY7/UYMzkFDsMDiuFntoDQFXhQuQT/ba5JaU4dWyZYreAhAh+vHwho4KEzlPdlG4LnIMgODuRVTxkGDwDLomTBhWqlxNhO89V8GCDAC+FQIDAQAB \ No newline at end of file diff --git a/tests/Socialbox/Classes/content.txt b/tests/Socialbox/Classes/content.txt new file mode 100644 index 0000000..f2afb1a --- /dev/null +++ b/tests/Socialbox/Classes/content.txt @@ -0,0 +1,4 @@ +{ + "method" : "ping", + "id" : "daa31852" +} \ No newline at end of file diff --git a/tests/Socialbox/Classes/private.der b/tests/Socialbox/Classes/server_private.der similarity index 100% rename from tests/Socialbox/Classes/private.der rename to tests/Socialbox/Classes/server_private.der diff --git a/tests/Socialbox/Classes/public.der b/tests/Socialbox/Classes/server_public.der similarity index 100% rename from tests/Socialbox/Classes/public.der rename to tests/Socialbox/Classes/server_public.der diff --git a/tests/Socialbox/Classes/secret.txt b/tests/Socialbox/Classes/server_secret.txt similarity index 100% rename from tests/Socialbox/Classes/secret.txt rename to tests/Socialbox/Classes/server_secret.txt diff --git a/tests/Socialbox/Managers/SessionManagerTest.php b/tests/Socialbox/Managers/SessionManagerTest.php index 1da708c..b376361 100644 --- a/tests/Socialbox/Managers/SessionManagerTest.php +++ b/tests/Socialbox/Managers/SessionManagerTest.php @@ -6,7 +6,7 @@ use InvalidArgumentException; use PHPUnit\Framework\TestCase; use Socialbox\Classes\Cryptography; use Socialbox\Classes\Utilities; -use Socialbox\Objects\SessionRecord; +use Socialbox\Objects\Database\SessionRecord; class SessionManagerTest extends TestCase { From 3b3271b94fda5647fc7633b6d72312b42dfab6ae Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 24 Oct 2024 13:55:29 -0400 Subject: [PATCH 002/420] Add initial PHPUnit configuration for PhpStorm This commit adds a new file `.idea/php-test-framework.xml` to configure PHPUnit for PhpStorm. It specifies the local path and version for PHP testing. --- .idea/php-test-framework.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .idea/php-test-framework.xml diff --git a/.idea/php-test-framework.xml b/.idea/php-test-framework.xml new file mode 100644 index 0000000..ea6eea8 --- /dev/null +++ b/.idea/php-test-framework.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file From 5eeb06805a7c9d8408dd5b903a8c1955b3ddbe43 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 24 Oct 2024 14:06:06 -0400 Subject: [PATCH 003/420] Add optional SHA1 hashing for sign and verify functions base64 encoding and decoding. --- src/Socialbox/Classes/Cryptography.php | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Socialbox/Classes/Cryptography.php b/src/Socialbox/Classes/Cryptography.php index 5de4846..f69605d 100644 --- a/src/Socialbox/Classes/Cryptography.php +++ b/src/Socialbox/Classes/Cryptography.php @@ -54,7 +54,7 @@ class Cryptography private static function pemToDer(string $pemKey): string { $pemKey = preg_replace('/-----(BEGIN|END) [A-Z ]+-----/', '', $pemKey); - return base64_decode(str_replace(["\n", "\r", " "], '', $pemKey)); + return Utilities::base64decode(str_replace(["\n", "\r", " "], '', $pemKey)); } /** @@ -66,7 +66,7 @@ class Cryptography */ private static function derToPem(string $derKey, string $type): string { - $formattedKey = chunk_split(base64_encode($derKey), 64); + $formattedKey = chunk_split(Utilities::base64encode($derKey), 64); $headerFooter = strtoupper($type) === self::PEM_PUBLIC_HEADER ? "PUBLIC KEY" : "PRIVATE KEY"; @@ -78,10 +78,11 @@ class Cryptography * * @param string $content The content to be signed. * @param string $privateKey The private key used to sign the content. + * @param bool $hashContent Whether to hash the content using SHA1 before signing it. Default is false. * @return string The Base64 encoded signature of the content. * @throws CryptographyException If the private key is invalid or if the content signing fails. */ - public static function signContent(string $content, string $privateKey): string + public static function signContent(string $content, string $privateKey, bool $hashContent=false): string { $privateKey = openssl_pkey_get_private(self::derToPem(Utilities::base64decode($privateKey), self::PEM_PRIVATE_HEADER)); if (!$privateKey) @@ -89,6 +90,11 @@ class Cryptography throw new CryptographyException('Invalid private key: ' . openssl_error_string()); } + if($hashContent) + { + $content = hash('sha1', $content); + } + if (!openssl_sign($content, $signature, $privateKey, self::HASH_ALGORITHM)) { throw new CryptographyException('Failed to sign content: ' . openssl_error_string()); @@ -103,10 +109,11 @@ class Cryptography * @param string $content The content to be verified. * @param string $signature The digital signature to verify against. * @param string $publicKey The public key to use for verification. + * @param bool $hashContent Whether to hash the content using SHA1 before verifying it. Default is false. * @return bool Returns true if the content verification is successful, false otherwise. * @throws CryptographyException If the public key is invalid or if the signature verification fails. */ - public static function verifyContent(string $content, string $signature, string $publicKey): bool + public static function verifyContent(string $content, string $signature, string $publicKey, bool $hashContent=false): bool { try { @@ -122,6 +129,11 @@ class Cryptography throw new CryptographyException('Invalid public key: ' . openssl_error_string()); } + if($hashContent) + { + $content = hash('sha1', $content); + } + try { return openssl_verify($content, Utilities::base64decode($signature), $publicKey, self::HASH_ALGORITHM) === 1; From 5555e793277d3e4fb6e73489f34afc5bd5102932 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 24 Oct 2024 15:15:14 -0400 Subject: [PATCH 004/420] Refactored Configuration --- .idea/php.xml | 15 ++-- src/Socialbox/Abstracts/CacheLayer.php | 2 +- .../CacheLayer/MemcachedCacheLayer.php | 6 +- .../Classes/CacheLayer/RedisCacheLayer.php | 10 +-- .../Classes/CliCommands/InitializeCommand.php | 2 +- src/Socialbox/Classes/Configuration.php | 86 +++++++++++++------ .../Configuration/CacheConfiguration.php | 83 ++++++++++++++++++ .../Configuration/DatabaseConfiguration.php | 46 ++++++++++ .../Resources/database/registered_peers.sql | 2 +- src/Socialbox/Enums/Flags/PeerFlags.php | 34 ++++++++ src/Socialbox/Managers/SessionManager.php | 7 +- 11 files changed, 247 insertions(+), 46 deletions(-) create mode 100644 src/Socialbox/Classes/Configuration/CacheConfiguration.php create mode 100644 src/Socialbox/Classes/Configuration/DatabaseConfiguration.php create mode 100644 src/Socialbox/Enums/Flags/PeerFlags.php diff --git a/.idea/php.xml b/.idea/php.xml index cee48e7..6f8f81e 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -12,16 +12,17 @@ - - - + - - + + + + - - + + + diff --git a/src/Socialbox/Abstracts/CacheLayer.php b/src/Socialbox/Abstracts/CacheLayer.php index e825def..4e07a5c 100644 --- a/src/Socialbox/Abstracts/CacheLayer.php +++ b/src/Socialbox/Abstracts/CacheLayer.php @@ -69,7 +69,7 @@ abstract class CacheLayer { if (self::$instance === null) { - $engine = Configuration::getConfiguration()['cache']['engine']; + $engine = Configuration::getCacheConfiguration()->getEngine(); if ($engine === 'redis') { diff --git a/src/Socialbox/Classes/CacheLayer/MemcachedCacheLayer.php b/src/Socialbox/Classes/CacheLayer/MemcachedCacheLayer.php index 8095ae1..d1edcad 100644 --- a/src/Socialbox/Classes/CacheLayer/MemcachedCacheLayer.php +++ b/src/Socialbox/Classes/CacheLayer/MemcachedCacheLayer.php @@ -22,10 +22,10 @@ class MemcachedCacheLayer extends CacheLayer } $this->memcached = new Memcached(); - $this->memcached->addServer(Configuration::getConfiguration()['cache']['host'], (int)Configuration::getConfiguration()['cache']['port']); - if(Configuration::getConfiguration()['cache']['username'] !== null || Configuration::getConfiguration()['cache']['password'] !== null) + $this->memcached->addServer(Configuration::getCacheConfiguration()->getHost(), Configuration::getCacheConfiguration()->getPort()); + if(Configuration::getCacheConfiguration()->getUsername() !== null || Configuration::getCacheConfiguration()->getPassword() !== null) { - $this->memcached->setSaslAuthData(Configuration::getConfiguration()['cache']['username'], Configuration::getConfiguration()['cache']['password']); + $this->memcached->setSaslAuthData(Configuration::getCacheConfiguration()->getUsername(), Configuration::getCacheConfiguration()->getPassword()); } } diff --git a/src/Socialbox/Classes/CacheLayer/RedisCacheLayer.php b/src/Socialbox/Classes/CacheLayer/RedisCacheLayer.php index 1715e51..f836d92 100644 --- a/src/Socialbox/Classes/CacheLayer/RedisCacheLayer.php +++ b/src/Socialbox/Classes/CacheLayer/RedisCacheLayer.php @@ -26,15 +26,15 @@ class RedisCacheLayer extends CacheLayer try { - $this->redis->connect(Configuration::getConfiguration()['cache']['host'], (int)Configuration::getConfiguration()['cache']['port']); - if (Configuration::getConfiguration()['cache']['password'] !== null) + $this->redis->connect(Configuration::getCacheConfiguration()->getHost(), Configuration::getCacheConfiguration()->getPort()); + if (Configuration::getCacheConfiguration()->getPassword() !== null) { - $this->redis->auth(Configuration::getConfiguration()['cache']['password']); + $this->redis->auth(Configuration::getCacheConfiguration()->getPassword()); } - if (Configuration::getConfiguration()['cache']['database'] !== 0) + if (Configuration::getCacheConfiguration()->getDatabase() !== null) { - $this->redis->select((int)Configuration::getConfiguration()['cache']['database']); + $this->redis->select(Configuration::getCacheConfiguration()->getDatabase()); } } catch (RedisException $e) diff --git a/src/Socialbox/Classes/CliCommands/InitializeCommand.php b/src/Socialbox/Classes/CliCommands/InitializeCommand.php index 9cf5b64..07b71e8 100644 --- a/src/Socialbox/Classes/CliCommands/InitializeCommand.php +++ b/src/Socialbox/Classes/CliCommands/InitializeCommand.php @@ -30,7 +30,7 @@ class InitializeCommand implements CliCommandInterface Log::info('net.nosial.socialbox', 'Initializing Socialbox...'); - if(Configuration::getConfiguration()['cache']['enabled']) + if(Configuration::getCacheConfiguration()->isEnabled()) { Log::verbose('net.nosial.socialbox', 'Clearing cache layer...'); CacheLayer::getInstance()->clear(); diff --git a/src/Socialbox/Classes/Configuration.php b/src/Socialbox/Classes/Configuration.php index 648e439..73a6b02 100644 --- a/src/Socialbox/Classes/Configuration.php +++ b/src/Socialbox/Classes/Configuration.php @@ -2,43 +2,75 @@ namespace Socialbox\Classes; +use Socialbox\Classes\Configuration\CacheConfiguration; +use Socialbox\Classes\Configuration\DatabaseConfiguration; + class Configuration { private static ?array $configuration = null; + private static ?DatabaseConfiguration $databaseConfiguration = null; + private static ?CacheConfiguration $cacheConfiguration = null; + + private static function initializeConfiguration(): void + { + $config = new \ConfigLib\Configuration('socialbox'); + + // False by default, requires the user to enable it. + $config->setDefault('instance.enabled', false); + + $config->setDefault('security.display_internal_exceptions', false); + + $config->setDefault('database.host', '127.0.0.1'); + $config->setDefault('database.port', 3306); + $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.sessions.enabled', true); + $config->setDefault('cache.sessions.ttl', 3600); + $config->setDefault('cache.sessions.max', 1000); + + $config->save(); + + self::$configuration = $config->getConfiguration(); + self::$databaseConfiguration = self::$configuration['database']; + self::$cacheConfiguration = self::$configuration['cache']; + } public static function getConfiguration(): array { if(self::$configuration === null) { - $config = new \ConfigLib\Configuration('socialbox'); - - // False by default, requires the user to enable it. - $config->setDefault('instance.enabled', false); - - $config->setDefault('security.display_internal_exceptions', false); - - $config->setDefault('database.host', '127.0.0.1'); - $config->setDefault('database.port', 3306); - $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(); + self::initializeConfiguration(); } return self::$configuration; } + + public static function getDatabaseConfiguration(): DatabaseConfiguration + { + if(self::$databaseConfiguration === null) + { + self::initializeConfiguration(); + } + + return self::$databaseConfiguration; + } + + public static function getCacheConfiguration(): CacheConfiguration + { + if(self::$cacheConfiguration === null) + { + self::initializeConfiguration(); + } + + return self::$cacheConfiguration; + } } \ No newline at end of file diff --git a/src/Socialbox/Classes/Configuration/CacheConfiguration.php b/src/Socialbox/Classes/Configuration/CacheConfiguration.php new file mode 100644 index 0000000..a080f03 --- /dev/null +++ b/src/Socialbox/Classes/Configuration/CacheConfiguration.php @@ -0,0 +1,83 @@ +enabled = (bool)$data['enabled']; + $this->engine = (string)$data['engine']; + $this->host = (string)$data['host']; + $this->port = (int)$data['port']; + $this->username = $data['username'] ? (string)$data['username'] : null; + $this->password = $data['password'] ? (string)$data['password'] : null; + $this->database = $data['database'] ? (int)$data['database'] : null; + + $this->sessionsEnabled = (bool)$data['sessions.enabled']; + $this->sessionsTtl = (int)$data['sessions.ttl']; + $this->sessionsMax = (int)$data['sessions.max']; + } + + public function isEnabled(): bool + { + return $this->enabled; + } + + public function getEngine(): string + { + return $this->engine; + } + + public function getHost(): string + { + return $this->host; + } + + public function getPort(): int + { + return $this->port; + } + + public function getUsername(): ?string + { + return $this->username; + } + + public function getPassword(): ?string + { + return $this->password; + } + + public function getDatabase(): ?int + { + return $this->database; + } + + public function isSessionsEnabled(): bool + { + return $this->sessionsEnabled; + } + + public function getSessionsTtl(): int + { + return $this->sessionsTtl; + } + + public function getSessionsMax(): int + { + return $this->sessionsMax; + } +} \ No newline at end of file diff --git a/src/Socialbox/Classes/Configuration/DatabaseConfiguration.php b/src/Socialbox/Classes/Configuration/DatabaseConfiguration.php new file mode 100644 index 0000000..2eb427c --- /dev/null +++ b/src/Socialbox/Classes/Configuration/DatabaseConfiguration.php @@ -0,0 +1,46 @@ +host = (string)$data['host']; + $this->port = (int)$data['port']; + $this->username = (string)$data['username']; + $this->password = $data['password'] ? (string)$data['password'] : null; + $this->name = (string)$data['name']; + } + + public function getHost(): string + { + return $this->host; + } + + public function getPort(): int + { + return $this->port; + } + + public function getUsername(): string + { + return $this->username; + } + + public function getPassword(): ?string + { + return $this->password; + } + + public function getName(): string + { + return $this->name; + } +} \ No newline at end of file diff --git a/src/Socialbox/Classes/Resources/database/registered_peers.sql b/src/Socialbox/Classes/Resources/database/registered_peers.sql index 59e1914..01f9229 100644 --- a/src/Socialbox/Classes/Resources/database/registered_peers.sql +++ b/src/Socialbox/Classes/Resources/database/registered_peers.sql @@ -15,6 +15,6 @@ create table registered_peers comment 'Table for housing registered peers under this network'; create index registered_peers_registered_index - on registered_peers (registered) + on registered_peers (created) comment 'The Index for the reigstered column of the peer'; diff --git a/src/Socialbox/Enums/Flags/PeerFlags.php b/src/Socialbox/Enums/Flags/PeerFlags.php new file mode 100644 index 0000000..fc26d7c --- /dev/null +++ b/src/Socialbox/Enums/Flags/PeerFlags.php @@ -0,0 +1,34 @@ + false, + default => true, + }; + } +} diff --git a/src/Socialbox/Managers/SessionManager.php b/src/Socialbox/Managers/SessionManager.php index d14d511..2032cfd 100644 --- a/src/Socialbox/Managers/SessionManager.php +++ b/src/Socialbox/Managers/SessionManager.php @@ -7,6 +7,7 @@ use InvalidArgumentException; use PDO; use PDOException; + use Socialbox\Classes\Configuration; use Socialbox\Classes\Cryptography; use Socialbox\Classes\Database; use Socialbox\Enums\SessionState; @@ -107,7 +108,8 @@ $data['created'] = new DateTime($data['created']); $data['last_request'] = new DateTime($data['last_request']); - return SessionRecord::fromArray($data); + $sessionRecord = SessionRecord::fromArray($data); + } catch (PDOException | DateMalformedStringException $e) { @@ -120,6 +122,7 @@ * * @param string $uuid The UUID of the session to update. * @return void + * @throws DatabaseOperationException */ public static function updateAuthenticatedPeer(string $uuid): void { @@ -141,6 +144,7 @@ * * @param string $uuid The UUID of the session to be updated. * @return void + * @throws DatabaseOperationException */ public static function updateLastRequest(string $uuid): void { @@ -164,6 +168,7 @@ * @param string $uuid The unique identifier of the session to update. * @param SessionState $state The new state to be set for the session. * @return void No return value. + * @throws DatabaseOperationException */ public static function updateState(string $uuid, SessionState $state): void { From d2eaa1fe7333b974a16eb41032d77659c043d897 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 24 Oct 2024 15:23:28 -0400 Subject: [PATCH 005/420] Added Captcha library --- .idea/php.xml | 2 ++ project.json | 5 +++++ tests/Socialbox/Other/CaptchaTest.php | 17 +++++++++++++++++ 3 files changed, 24 insertions(+) create mode 100644 tests/Socialbox/Other/CaptchaTest.php diff --git a/.idea/php.xml b/.idea/php.xml index 6f8f81e..d8ba44c 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -12,6 +12,8 @@ + + diff --git a/project.json b/project.json index ac332be..985bc07 100644 --- a/project.json +++ b/project.json @@ -44,6 +44,11 @@ "name": "net.nosial.optslib", "version": "latest", "source": "nosial/libs.opts=latest@n64" + }, + { + "name": "com.gregwar.captcha", + "version": "latest", + "source": "gregwar/captcha=latest@packagist" } ], "configurations": [ diff --git a/tests/Socialbox/Other/CaptchaTest.php b/tests/Socialbox/Other/CaptchaTest.php new file mode 100644 index 0000000..b192f1a --- /dev/null +++ b/tests/Socialbox/Other/CaptchaTest.php @@ -0,0 +1,17 @@ +build(); + + $builder->save(__DIR__ . DIRECTORY_SEPARATOR . 'test.png'); + } +} \ No newline at end of file From 02b5afe4c038575a6f2a66d52a41faf8dc436a74 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 25 Oct 2024 13:37:09 -0400 Subject: [PATCH 006/420] Add registration configuration and remove old test files --- src/Socialbox/Classes/Configuration.php | 56 ++++++++ .../RegistrationConfiguration.php | 122 ++++++++++++++++++ tests/Socialbox/Classes/ConfigurationTest.php | 38 ------ 3 files changed, 178 insertions(+), 38 deletions(-) create mode 100644 src/Socialbox/Classes/Configuration/RegistrationConfiguration.php delete mode 100644 tests/Socialbox/Classes/ConfigurationTest.php diff --git a/src/Socialbox/Classes/Configuration.php b/src/Socialbox/Classes/Configuration.php index 73a6b02..7b5814f 100644 --- a/src/Socialbox/Classes/Configuration.php +++ b/src/Socialbox/Classes/Configuration.php @@ -4,13 +4,21 @@ namespace Socialbox\Classes; use Socialbox\Classes\Configuration\CacheConfiguration; use Socialbox\Classes\Configuration\DatabaseConfiguration; +use Socialbox\Classes\Configuration\RegistrationConfiguration; class Configuration { private static ?array $configuration = null; private static ?DatabaseConfiguration $databaseConfiguration = null; private static ?CacheConfiguration $cacheConfiguration = null; + private static ?RegistrationConfiguration $registrationConfiguration = null; + /** + * Initializes the configuration settings for the application. This includes + * settings for the instance, security, database, cache layer, and registration. + * + * @return void + */ private static function initializeConfiguration(): void { $config = new \ConfigLib\Configuration('socialbox'); @@ -20,12 +28,14 @@ class Configuration $config->setDefault('security.display_internal_exceptions', false); + // Database configuration $config->setDefault('database.host', '127.0.0.1'); $config->setDefault('database.port', 3306); $config->setDefault('database.username', 'root'); $config->setDefault('database.password', 'root'); $config->setDefault('database.name', 'test'); + // Cache layer configuration $config->setDefault('cache.enabled', false); $config->setDefault('cache.engine', 'redis'); $config->setDefault('cache.host', '127.0.0.1'); @@ -37,13 +47,30 @@ class Configuration $config->setDefault('cache.sessions.ttl', 3600); $config->setDefault('cache.sessions.max', 1000); + // Registration configuration + $config->setDefault('registration.enabled', true); + $config->setDefault('registration.password_required', true); + $config->setDefault('registration.otp_required', false); + $config->setDefault('registration.display_name_required', false); + $config->setDefault('registration.email_verification_required', false); + $config->setDefault('registration.sms_verification_required', false); + $config->setDefault('registration.phone_call_verification_required', false); + $config->setDefault('registration.image_captcha_verification_required', true); + $config->save(); self::$configuration = $config->getConfiguration(); self::$databaseConfiguration = self::$configuration['database']; self::$cacheConfiguration = self::$configuration['cache']; + self::$registrationConfiguration = self::$configuration['registration']; } + /** + * Retrieves the current configuration array. If the configuration is not initialized, + * it triggers the initialization process. + * + * @return array The current configuration array. + */ public static function getConfiguration(): array { if(self::$configuration === null) @@ -54,6 +81,11 @@ class Configuration return self::$configuration; } + /** + * Retrieves the current database configuration. + * + * @return DatabaseConfiguration The configuration settings for the database. + */ public static function getDatabaseConfiguration(): DatabaseConfiguration { if(self::$databaseConfiguration === null) @@ -64,6 +96,12 @@ class Configuration return self::$databaseConfiguration; } + /** + * Retrieves the current cache configuration. If the cache configuration + * has not been initialized, it will initialize it first. + * + * @return CacheConfiguration The current cache configuration instance. + */ public static function getCacheConfiguration(): CacheConfiguration { if(self::$cacheConfiguration === null) @@ -73,4 +111,22 @@ class Configuration return self::$cacheConfiguration; } + + /** + * Retrieves the registration configuration. + * + * This method returns the current RegistrationConfiguration instance. + * If the configuration has not been initialized yet, it initializes it first. + * + * @return RegistrationConfiguration The registration configuration instance. + */ + public static function getRegistrationConfiguration(): RegistrationConfiguration + { + if(self::$registrationConfiguration === null) + { + self::initializeConfiguration(); + } + + return self::$registrationConfiguration; + } } \ No newline at end of file diff --git a/src/Socialbox/Classes/Configuration/RegistrationConfiguration.php b/src/Socialbox/Classes/Configuration/RegistrationConfiguration.php new file mode 100644 index 0000000..03b0ed6 --- /dev/null +++ b/src/Socialbox/Classes/Configuration/RegistrationConfiguration.php @@ -0,0 +1,122 @@ +registrationEnabled = (bool)$data['registration_enabled']; + $this->passwordRequired = (bool)$data['password_required']; + $this->otpRequired = (bool)$data['otp_required']; + $this->displayNameRequired = (bool)$data['display_name_required']; + $this->emailVerificationRequired = (bool)$data['email_verification_required']; + $this->smsVerificationRequired = (bool)$data['sms_verification_required']; + $this->phoneCallVerificationRequired = (bool)$data['phone_call_verification_required']; + $this->imageCaptchaVerificationRequired = (bool)$data['image_captcha_verification_required']; + } + + /** + * Checks if the registration is enabled. + * + * @return bool True if registration is enabled, false otherwise. + */ + public function isRegistrationEnabled(): bool + { + return $this->registrationEnabled; + } + + /** + * Determines if a password is required. + * + * @return bool True if a password is required, false otherwise. + */ + public function isPasswordRequired(): bool + { + return $this->passwordRequired; + } + + /** + * Determines if OTP (One-Time Password) is required. + * + * @return bool True if OTP is required, false otherwise. + */ + public function isOtpRequired(): bool + { + return $this->otpRequired; + } + + /** + * Checks if a display name is required. + * + * @return bool Returns true if a display name is required, false otherwise. + */ + public function isDisplayNameRequired(): bool + { + return $this->displayNameRequired; + } + + /** + * Checks if email verification is required. + * + * @return bool Returns true if email verification is required, false otherwise. + */ + public function isEmailVerificationRequired(): bool + { + return $this->emailVerificationRequired; + } + + /** + * Checks if SMS verification is required. + * + * @return bool Returns true if SMS verification is required, false otherwise. + */ + public function isSmsVerificationRequired(): bool + { + return $this->smsVerificationRequired; + } + + /** + * Checks if phone call verification is required. + * + * @return bool Returns true if phone call verification is required, false otherwise. + */ + public function isPhoneCallVerificationRequired(): bool + { + return $this->phoneCallVerificationRequired; + } + + /** + * Determines if image CAPTCHA verification is required. + * + * @return bool Returns true if image CAPTCHA verification is required, false otherwise. + */ + public function isImageCaptchaVerificationRequired(): bool + { + return $this->imageCaptchaVerificationRequired; + } +} \ No newline at end of file diff --git a/tests/Socialbox/Classes/ConfigurationTest.php b/tests/Socialbox/Classes/ConfigurationTest.php deleted file mode 100644 index 028e3e3..0000000 --- a/tests/Socialbox/Classes/ConfigurationTest.php +++ /dev/null @@ -1,38 +0,0 @@ -assertIsArray($config, "Configuration should be an array."); - - //Assert that all the default configuration exists - $this->assertArrayHasKey('host', $config['database']); - $this->assertIsString($config['database']['host']); - - $this->assertArrayHasKey('port', $config['database']); - $this->assertIsInt($config['database']['port'], 3306); - - $this->assertArrayHasKey('username', $config['database']); - $this->assertIsString($config['database']['username']); - - $this->assertArrayHasKey('password', $config['database']); - $this->assertIsString($config['database']['password']); - - $this->assertArrayHasKey('name', $config['database']); - $this->assertIsString($config['database']['name']); - } -} \ No newline at end of file From deaa6b1d205b944ba077cb77feed47e804167a2a Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 25 Oct 2024 13:37:21 -0400 Subject: [PATCH 007/420] Add Captcha Management System --- src/Socialbox/Enums/Status/CaptchaStatus.php | 9 + src/Socialbox/Managers/CaptchaManager.php | 188 ++++++++++++++++++ .../Objects/Database/CaptchaRecord.php | 80 ++++++++ 3 files changed, 277 insertions(+) create mode 100644 src/Socialbox/Enums/Status/CaptchaStatus.php create mode 100644 src/Socialbox/Managers/CaptchaManager.php create mode 100644 src/Socialbox/Objects/Database/CaptchaRecord.php diff --git a/src/Socialbox/Enums/Status/CaptchaStatus.php b/src/Socialbox/Enums/Status/CaptchaStatus.php new file mode 100644 index 0000000..4400a07 --- /dev/null +++ b/src/Socialbox/Enums/Status/CaptchaStatus.php @@ -0,0 +1,9 @@ +getUuid(); + } + + $answer = Utilities::randomString(6, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'); + + if(!self::captchaExists($peer_uuid)) + { + $statement = Database::getConnection()->prepare("INSERT INTO captcha_images (peer_uuid, answer) VALUES (?, ?)"); + $statement->bindParam(1, $peer_uuid); + $statement->bindParam(2, $answer); + + try + { + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to create a captcha in the database', $e); + } + + return $answer; + } + + $statement = Database::getConnection()->prepare("UPDATE captcha_images SET answer=?, status='UNSOLVED', created=NOW() WHERE peer_uuid=?"); + $statement->bindParam(1, $answer); + $statement->bindParam(2, $peer_uuid); + + try + { + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to update a captcha in the database', $e); + } + + return $answer; + } + + /** + * Answers a captcha for the given peer UUID. + * + * @param string|RegisteredPeerRecord $peer_uuid The UUID of the peer to answer the captcha for. + * @param string $answer The answer to the captcha. + * @return bool True if the answer is correct, false otherwise. + * @throws DatabaseOperationException If the operation fails. + */ + public static function answerCaptcha(string|RegisteredPeerRecord $peer_uuid, string $answer): bool + { + if($peer_uuid instanceof RegisteredPeerRecord) + { + $peer_uuid = $peer_uuid->getUuid(); + } + + // Return false if the captcha does not exist + if(!self::captchaExists($peer_uuid)) + { + return false; + } + + $captcha = self::getCaptcha($peer_uuid); + + // Return false if the captcha has already been solved + if($captcha->getStatus() === CaptchaStatus::SOLVED) + { + return false; + } + + // Return false if the captcha is older than 5 minutes + if ($captcha->getCreated() instanceof DateTimeInterface && $captcha->getCreated()->diff(new DateTime())->i > 5) + { + return false; + } + + // Verify the answer + if($captcha->getAnswer() !== $answer) + { + return false; + } + + $statement = Database::getConnection()->prepare("UPDATE captcha_images SET status='SOLVED', answered=NOW() WHERE peer_uuid=?"); + $statement->bindParam(1, $peer_uuid); + + try + { + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to update a captcha in the database', $e); + } + + return true; + } + + /** + * Retrieves the captcha record for the given peer UUID. + * + * @param string|RegisteredPeerRecord $peer_uuid The UUID of the peer to retrieve the captcha for. + * @return CaptchaRecord The captcha record. + * @throws DatabaseOperationException If the operation fails. + */ + public static function getCaptcha(string|RegisteredPeerRecord $peer_uuid): CaptchaRecord + { + // If the peer_uuid is a RegisteredPeerRecord, get the UUID + if($peer_uuid instanceof RegisteredPeerRecord) + { + $peer_uuid = $peer_uuid->getUuid(); + } + + try + { + $statement = Database::getConnection()->prepare("SELECT * FROM captcha_images WHERE peer_uuid=? LIMIT 1"); + $statement->bindParam(1, $peer_uuid); + $statement->execute(); + $result = $statement->fetch(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to get a captcha from the database', $e); + } + + if($result === false) + { + throw new DatabaseOperationException('The requested captcha does not exist'); + } + + return CaptchaRecord::fromArray($result); + } + + /** + * Checks if a captcha exists for the given peer UUID. + * + * @param string|RegisteredPeerRecord $peer_uuid The UUID of the peer to check for a captcha. + * @return bool True if a captcha exists, false otherwise. + * @throws DatabaseOperationException If the operation fails. + */ + private static function captchaExists(string|RegisteredPeerRecord $peer_uuid): bool + { + // If the peer_uuid is a RegisteredPeerRecord, get the UUID + if($peer_uuid instanceof RegisteredPeerRecord) + { + $peer_uuid = $peer_uuid->getUuid(); + } + + try + { + $statement = Database::getConnection()->prepare("SELECT COUNT(*) FROM captcha_images WHERE peer_uuid=?"); + $statement->bindParam(1, $peer_uuid); + $statement->execute(); + $result = $statement->fetchColumn(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to check if a captcha exists in the database', $e); + } + + return $result > 0; + } +} \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/CaptchaRecord.php b/src/Socialbox/Objects/Database/CaptchaRecord.php new file mode 100644 index 0000000..4427310 --- /dev/null +++ b/src/Socialbox/Objects/Database/CaptchaRecord.php @@ -0,0 +1,80 @@ +uuid = (string)$data['uuid']; + $this->peerUuid = (string)$data['peer_uuid']; + $this->status = CaptchaStatus::tryFrom((string)$data['status']); + $this->answer = isset($data['answer']) ? (string)$data['answer'] : null; + $this->answered = isset($data['answered']) ? new DateTime((string)$data['answered']) : null; + $this->created = new DateTime((string)$data['created']); + } + + public function getUuid(): string + { + return $this->uuid; + } + + public function getPeerUuid(): string + { + return $this->peerUuid; + } + + public function getStatus(): CaptchaStatus + { + return $this->status; + } + + public function getAnswer(): ?string + { + return $this->answer; + } + + public function getAnswered(): ?DateTime + { + return $this->answered; + } + + public function getCreated(): DateTime + { + return $this->created; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): object + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'uuid' => $this->uuid, + 'peer_uuid' => $this->peerUuid, + 'status' => $this->status->value, + 'answer' => $this->answer, + 'answered' => $this->answered?->format('Y-m-d H:i:s'), + 'created' => $this->created->format('Y-m-d H:i:s') + ]; + } +} \ No newline at end of file From d73a3e5457557be0fdb884cb067cf95f469bd2f9 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 25 Oct 2024 13:37:35 -0400 Subject: [PATCH 008/420] Add RegisteredPeerManager and related classes/tests --- src/Socialbox/Enums/Flags/PeerFlags.php | 4 + .../Managers/RegisteredPeerManager.php | 365 ++++++++++++++++++ .../Objects/Database/RegisteredPeerRecord.php | 144 +++++++ .../Managers/RegisteredPeerManagerTest.php | 78 ++++ 4 files changed, 591 insertions(+) create mode 100644 src/Socialbox/Managers/RegisteredPeerManager.php create mode 100644 src/Socialbox/Objects/Database/RegisteredPeerRecord.php create mode 100644 tests/Socialbox/Managers/RegisteredPeerManagerTest.php diff --git a/src/Socialbox/Enums/Flags/PeerFlags.php b/src/Socialbox/Enums/Flags/PeerFlags.php index fc26d7c..ce7b085 100644 --- a/src/Socialbox/Enums/Flags/PeerFlags.php +++ b/src/Socialbox/Enums/Flags/PeerFlags.php @@ -14,6 +14,10 @@ enum PeerFlags : string // Verification Flags case VER_SET_PASSWORD = 'VER_SET_PASSWORD'; case VER_SET_OTP = 'VER_SET_OTP'; + case VER_SET_DISPLAY_NAME = 'VER_SET_DISPLAY_NAME'; + case VER_EMAIL = 'VER_EMAIL'; + case VER_SMS = 'VER_SMS'; + case VER_PHONE_CALL = 'VER_PHONE_CALL'; case VER_SOLVE_IMAGE_CAPTCHA = 'VER_SOLVE_IMAGE_CAPTCHA'; /** diff --git a/src/Socialbox/Managers/RegisteredPeerManager.php b/src/Socialbox/Managers/RegisteredPeerManager.php new file mode 100644 index 0000000..f5ddfef --- /dev/null +++ b/src/Socialbox/Managers/RegisteredPeerManager.php @@ -0,0 +1,365 @@ +prepare('SELECT COUNT(*) FROM `registered_peers` WHERE username=?'); + $statement->bindParam(1, $username); + $statement->execute(); + + $result = $statement->fetchColumn(); + return $result > 0; + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to check if the username exists', $e); + } + } + + /** + * Creates a new peer with the given username. + * + * @param string $username The username to associate with the new peer. + * @param bool $enabled True if the peer should be enabled, false otherwise. + * @return string The UUID of the newly created peer. + * @throws DatabaseOperationException If the operation fails. + */ + public static function createPeer(string $username, bool $enabled=false): string + { + if(self::usernameExists($username)) + { + throw new DatabaseOperationException('The username already exists'); + } + + $uuid = Uuid::v4()->toRfc4122(); + + // If `enabled` is True, we insert the peer into the database as an activated account. + if($enabled) + { + try + { + $statement = Database::getConnection()->prepare('INSERT INTO `registered_peers` (uuid, username, enabled) VALUES (?, ?, ?)'); + $statement->bindParam(1, $uuid); + $statement->bindParam(2, $username); + $statement->bindParam(3, $enabled, PDO::PARAM_BOOL); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to create the peer in the database', $e); + } + + return $uuid; + } + + // Otherwise, we insert the peer into the database as a disabled account & the required verification flags. + $flags = []; + + if(Configuration::getRegistrationConfiguration()->isRegistrationEnabled()) + { + $flags[] = PeerFlags::VER_EMAIL; + } + + if(Configuration::getRegistrationConfiguration()->isPasswordRequired()) + { + $flags[] = PeerFlags::VER_SET_PASSWORD; + } + + if(Configuration::getRegistrationConfiguration()->isOtpRequired()) + { + $flags[] = PeerFlags::VER_SET_OTP; + } + + if(Configuration::getRegistrationConfiguration()->isDisplayNameRequired()) + { + $flags[] = PeerFlags::VER_SET_DISPLAY_NAME; + } + + if(Configuration::getRegistrationConfiguration()->isEmailVerificationRequired()) + { + $flags[] = PeerFlags::VER_EMAIL; + } + + if(Configuration::getRegistrationConfiguration()->isSmsVerificationRequired()) + { + $flags[] = PeerFlags::VER_SMS; + } + + if(Configuration::getRegistrationConfiguration()->isPhoneCallVerificationRequired()) + { + $flags[] = PeerFlags::VER_PHONE_CALL; + } + + if(Configuration::getRegistrationConfiguration()->isImageCaptchaVerificationRequired()) + { + $flags[] = PeerFlags::VER_SOLVE_IMAGE_CAPTCHA; + } + + try + { + $implodedFlags = implode(',', $flags); + $statement = Database::getConnection()->prepare('INSERT INTO `registered_peers` (uuid, username, enabled, flags) VALUES (?, ?, ?, ?)'); + $statement->bindParam(1, $uuid); + $statement->bindParam(2, $username); + $statement->bindParam(3, $enabled, PDO::PARAM_BOOL); + $statement->bindParam(4, $implodedFlags); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to create the peer in the database', $e); + } + + return $uuid; + } + + /** + * Deletes a peer from the database based on the given UUID or RegisteredPeerRecord. + * WARNING: This operation is cascading and will delete all associated data. + * + * @param string|RegisteredPeerRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer to be deleted. + * @return void + * @throws DatabaseOperationException If the operation fails. + */ + public static function deletePeer(string|RegisteredPeerRecord $uuid): void + { + if($uuid instanceof RegisteredPeerRecord) + { + $uuid = $uuid->getUuid(); + } + + try + { + $statement = Database::getConnection()->prepare('DELETE FROM `registered_peers` WHERE uuid=?'); + $statement->bindParam(1, $uuid); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to delete the peer from the database', $e); + } + } + + /** + * Retrieves a registered peer record based on the given unique identifier or RegisteredPeerRecord object. + * + * @param string|RegisteredPeerRecord $uuid The unique identifier of the registered peer, or an instance of RegisteredPeerRecord. + * @return RegisteredPeerRecord Returns a RegisteredPeerRecord object containing the peer's information. + * @throws StandardException If the requested peer does not exist. + * @throws DatabaseOperationException If there is an error during the database operation. + */ + public static function getPeer(string|RegisteredPeerRecord $uuid): RegisteredPeerRecord + { + if($uuid instanceof RegisteredPeerRecord) + { + $uuid = $uuid->getUuid(); + } + + try + { + $statement = Database::getConnection()->prepare('SELECT * FROM `registered_peers` WHERE uuid=?'); + $statement->bindParam(1, $uuid); + $statement->execute(); + + $result = $statement->fetch(PDO::FETCH_ASSOC); + + if($result === false) + { + throw new StandardException(sprintf("The requested peer '%s' does not exist", $uuid), StandardError::PEER_NOT_FOUND); + } + + return new RegisteredPeerRecord($result); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to get the peer from the database', $e); + } + } + + /** + * Retrieves a peer record by the given username. + * + * @param string $username The username of the peer to be retrieved. + * @return RegisteredPeerRecord The record of the peer associated with the given username. + * @throws DatabaseOperationException If there is an error while querying the database. + * @throws StandardException If the peer does not exist. + */ + public static function getPeerByUsername(string $username): RegisteredPeerRecord + { + try + { + $statement = Database::getConnection()->prepare('SELECT * FROM `registered_peers` WHERE username=?'); + $statement->bindParam(1, $username); + $statement->execute(); + + $result = $statement->fetch(PDO::FETCH_ASSOC); + + if($result === false) + { + throw new StandardException(sprintf("The requested peer '%s' does not exist", $username), StandardError::PEER_NOT_FOUND); + } + + return new RegisteredPeerRecord($result); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to get the peer from the database', $e); + } + } + + /** + * Enables a peer identified by the given UUID or RegisteredPeerRecord. + * + * @param string|RegisteredPeerRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer to be enabled. + * @return void + * @throws DatabaseOperationException If there is an error while updating the database. + */ + public static function enablePeer(string|RegisteredPeerRecord $uuid): void + { + if($uuid instanceof RegisteredPeerRecord) + { + $uuid = $uuid->getUuid(); + } + + try + { + $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET enabled=1 WHERE uuid=?'); + $statement->bindParam(1, $uuid); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to enable the peer in the database', $e); + } + } + + /** + * Disables the peer identified by the given UUID or RegisteredPeerRecord. + * + * @param string|RegisteredPeerRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer. + * @return void + * @throws DatabaseOperationException If there is an error while updating the peer's status in the database. + */ + public static function disablePeer(string|RegisteredPeerRecord $uuid): void + { + if($uuid instanceof RegisteredPeerRecord) + { + $uuid = $uuid->getUuid(); + } + + try + { + $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET enabled=0 WHERE uuid=?'); + $statement->bindParam(1, $uuid); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to disable the peer in the database', $e); + } + } + + /** + * Adds a specific flag to the peer identified by the given UUID or RegisteredPeerRecord. + * + * @param string|RegisteredPeerRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer. + * @param PeerFlags|array $flags The flag or array of flags to be added to the peer. + * @return void + * @throws DatabaseOperationException If there is an error while updating the database. + * @throws StandardException If the peer does not exist. + */ + public static function addFlag(string|RegisteredPeerRecord $uuid, PeerFlags|array $flags): void + { + if($uuid instanceof RegisteredPeerRecord) + { + $uuid = $uuid->getUuid(); + } + + $peer = self::getPeer($uuid); + $existingFlags = $peer->getFlags(); + $flags = is_array($flags) ? $flags : [$flags]; + + foreach($flags as $flag) + { + if(!in_array($flag, $existingFlags)) + { + $existingFlags[] = $flag; + } + } + + try + { + $implodedFlags = implode(',', array_map(fn($flag) => $flag->name, $existingFlags)); + $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET flags=? WHERE uuid=?'); + $statement->bindParam(1, $implodedFlags); + $statement->bindParam(2, $uuid); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to add the flag to the peer in the database', $e); + } + } + + /** + * Removes a specific flag from the peer identified by the given UUID or RegisteredPeerRecord. + * + * @param string|RegisteredPeerRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer. + * @param PeerFlags $flag The flag to be removed from the peer. + * @return void + * @throws DatabaseOperationException If there is an error while updating the database. + * @throws StandardException If the peer does not exist. + */ + public static function removeFlag(string|RegisteredPeerRecord $uuid, PeerFlags $flag): void + { + if($uuid instanceof RegisteredPeerRecord) + { + $uuid = $uuid->getUuid(); + } + + $peer = self::getPeer($uuid); + if(!$peer->flagExists($flag)) + { + return; + } + + $flags = $peer->getFlags(); + unset($flags[array_search($flag, $flags)]); + + try + { + $implodedFlags = implode(',', $flags); + $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET flags=? WHERE uuid=?'); + $statement->bindParam(1, $implodedFlags); + $statement->bindParam(2, $uuid); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to remove the flag from the peer in the database', $e); + } + } +} \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/RegisteredPeerRecord.php b/src/Socialbox/Objects/Database/RegisteredPeerRecord.php new file mode 100644 index 0000000..a9b5e20 --- /dev/null +++ b/src/Socialbox/Objects/Database/RegisteredPeerRecord.php @@ -0,0 +1,144 @@ +uuid = $data['uuid']; + $this->username = $data['username']; + $this->displayName = $data['display_name'] ?? null; + + if($data['flags']) + { + if(is_array($data['flags'])) + { + $this->flags = array_map(fn($flag) => PeerFlags::from($flag), $data['flags']); + } + elseif(is_string($data['flags'])) + { + $flags = explode(',', $data['flags']); + $this->flags = array_map(fn($flag) => PeerFlags::from($flag), $flags); + } + } + else + { + $this->flags = []; + } + + $this->enabled = $data['enabled']; + + if (is_string($data['created'])) + { + $this->created = new DateTime($data['created']); + } + else + { + $this->created = $data['created']; + } + } + + /** + * Retrieves the UUID of the current instance. + * + * @return string The UUID. + */ + public function getUuid(): string + { + return $this->uuid; + } + + /** + * Retrieves the username. + * + * @return string The username. + */ + public function getUsername(): string + { + return $this->username; + } + + /** + * Retrieves the display name. + * + * @return string|null The display name if set, or null otherwise. + */ + public function getDisplayName(): ?string + { + return $this->displayName; + } + + public function getFlags(): array + { + return $this->flags; + } + + public function flagExists(PeerFlags $flag): bool + { + return in_array($flag, $this->flags, true); + } + + /** + * Checks if the current instance is enabled. + * + * @return bool True if enabled, false otherwise. + */ + public function isEnabled(): bool + { + return $this->enabled; + } + + /** + * Retrieves the creation date and time. + * + * @return DateTime The creation date and time. + */ + public function getCreated(): DateTime + { + return $this->created; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): object + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'uuid' => $this->uuid, + 'username' => $this->username, + 'display_name' => $this->displayName, + 'flags' => implode(',', array_map(fn($flag) => $flag->name, $this->flags)), + 'enabled' => $this->enabled, + 'created' => $this->created + ]; + } +} \ No newline at end of file diff --git a/tests/Socialbox/Managers/RegisteredPeerManagerTest.php b/tests/Socialbox/Managers/RegisteredPeerManagerTest.php new file mode 100644 index 0000000..74d4ad0 --- /dev/null +++ b/tests/Socialbox/Managers/RegisteredPeerManagerTest.php @@ -0,0 +1,78 @@ +getUuid()); + } + + self::$peerUuid = RegisteredPeerManager::createPeer('test_peer', true); + } + + public static function tearDownAfterClass(): void + { + if(RegisteredPeerManager::usernameExists('test_peer')) + { + RegisteredPeerManager::deletePeer(RegisteredPeerManager::getPeerByUsername('test_peer')->getUuid()); + } + } + + public function testEnablePeer() + { + RegisteredPeerManager::enablePeer(self::$peerUuid); + $peer = RegisteredPeerManager::getPeer(self::$peerUuid); + + $this->assertTrue($peer->isEnabled()); + } + + + public function testGetPeer() + { + $peer = RegisteredPeerManager::getPeer(self::$peerUuid); + + $this->assertEquals('test_peer', $peer->getUsername()); + } + + public function testGetPeerByUsername() + { + $peer = RegisteredPeerManager::getPeerByUsername('test_peer'); + + $this->assertEquals(self::$peerUuid, $peer->getUuid()); + } + + public function testUsernameExists() + { + $this->assertTrue(RegisteredPeerManager::usernameExists('test_peer')); + } + + public function testDisablePeer() + { + RegisteredPeerManager::disablePeer(self::$peerUuid); + $peer = RegisteredPeerManager::getPeer(self::$peerUuid); + + $this->assertFalse($peer->isEnabled()); + } + + public function testRemoveFlag() + { + RegisteredPeerManager::addFlag(self::$peerUuid, PeerFlags::ADMIN); + $peer = RegisteredPeerManager::getPeer(self::$peerUuid); + + $this->assertTrue($peer->flagExists(PeerFlags::ADMIN)); + + RegisteredPeerManager::removeFlag(self::$peerUuid, PeerFlags::ADMIN); + $peer = RegisteredPeerManager::getPeer(self::$peerUuid); + + $this->assertFalse($peer->flagExists(PeerFlags::ADMIN)); + } +} From 877297d94512664bb91ae5f1469091c22e57b6b6 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 25 Oct 2024 13:38:08 -0400 Subject: [PATCH 009/420] Updated sqldialects.xml --- .idea/sqldialects.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml index 4609cff..2f0c570 100644 --- a/.idea/sqldialects.xml +++ b/.idea/sqldialects.xml @@ -5,6 +5,8 @@ + + From 7073af234f3e592e789685fdf8b151395b6e1a6a Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 25 Oct 2024 13:38:38 -0400 Subject: [PATCH 010/420] Add PEER_NOT_FOUND case to StandardError enum --- src/Socialbox/Enums/StandardError.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Socialbox/Enums/StandardError.php b/src/Socialbox/Enums/StandardError.php index f8e6174..6485f91 100644 --- a/src/Socialbox/Enums/StandardError.php +++ b/src/Socialbox/Enums/StandardError.php @@ -20,6 +20,9 @@ enum StandardError : int case SESSION_NOT_FOUND = -3001; case UNSUPPORTED_AUTHENTICATION_TYPE = -3002; + // General Error Messages + case PEER_NOT_FOUND = -4000; + /** * Returns the default generic message for the error * From 7a9dd62d755b0a65181c4f444d1451073c8e465f Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 25 Oct 2024 13:38:47 -0400 Subject: [PATCH 011/420] Add ResolvedServerRecord class to the database module --- .../Objects/Database/ResolvedServerRecord.php | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/Socialbox/Objects/Database/ResolvedServerRecord.php diff --git a/src/Socialbox/Objects/Database/ResolvedServerRecord.php b/src/Socialbox/Objects/Database/ResolvedServerRecord.php new file mode 100644 index 0000000..262a8b1 --- /dev/null +++ b/src/Socialbox/Objects/Database/ResolvedServerRecord.php @@ -0,0 +1,25 @@ + Date: Fri, 25 Oct 2024 13:39:33 -0400 Subject: [PATCH 012/420] Refactor SessionManager to return SessionRecord directly --- src/Socialbox/Managers/SessionManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Socialbox/Managers/SessionManager.php b/src/Socialbox/Managers/SessionManager.php index 2032cfd..88520b1 100644 --- a/src/Socialbox/Managers/SessionManager.php +++ b/src/Socialbox/Managers/SessionManager.php @@ -108,7 +108,7 @@ $data['created'] = new DateTime($data['created']); $data['last_request'] = new DateTime($data['last_request']); - $sessionRecord = SessionRecord::fromArray($data); + return SessionRecord::fromArray($data); } catch (PDOException | DateMalformedStringException $e) From d2dbafe72884f5289cf4a112f8462700276a0275 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 25 Oct 2024 13:39:57 -0400 Subject: [PATCH 013/420] Add random string generation method to Utilities class Added a new method `randomString` to generate a random string using a specified character set. Enhanced code documentation and fixed a minor namespace typo in exception handling. --- src/Socialbox/Classes/Utilities.php | 39 +++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/src/Socialbox/Classes/Utilities.php b/src/Socialbox/Classes/Utilities.php index a7c1fa2..5f9a513 100644 --- a/src/Socialbox/Classes/Utilities.php +++ b/src/Socialbox/Classes/Utilities.php @@ -3,8 +3,10 @@ namespace Socialbox\Classes; use InvalidArgumentException; +use JsonException; use RuntimeException; use Socialbox\Enums\StandardHeaders; +use Throwable; class Utilities { @@ -41,7 +43,7 @@ class Utilities { return json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR); } - catch(\JsonException $e) + catch(JsonException $e) { throw new InvalidArgumentException("Failed to encode json input", $e); } @@ -119,7 +121,13 @@ class Utilities return $headers; } - public static function throwableToString(\Throwable $e): string + /** + * Converts a Throwable object into a formatted string. + * + * @param Throwable $e The throwable to be converted into a string. + * @return string The formatted string representation of the throwable, including the exception class, message, file, line, and stack trace. + */ + public static function throwableToString(Throwable $e): string { return sprintf( "%s: %s in %s:%d\nStack trace:\n%s", @@ -131,8 +139,35 @@ class Utilities ); } + /** + * Generates a formatted header string. + * + * @param StandardHeaders $header The standard header object. + * @param string $value The header value to be associated with the standard header. + * @return string The formatted header string. + */ public static function generateHeader(StandardHeaders $header, string $value): string { return $header->value . ': ' . $value; } + + /** + * Generates a random string of specified length using the provided character set. + * + * @param int $int The length of the random string to be generated. + * @param string $string The character set to use for generating the random string. + * @return string The generated random string. + */ + public static function randomString(int $int, string $string): string + { + $characters = str_split($string); + $randomString = ''; + + for ($i = 0; $i < $int; $i++) + { + $randomString .= $characters[array_rand($characters)]; + } + + return $randomString; + } } \ No newline at end of file From ce64643d7371375e0e3c2db827792fd80cd1b42f Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 25 Oct 2024 14:23:43 -0400 Subject: [PATCH 014/420] Add ResolvedServersManager and integrate with ServerResolver --- .idea/sqldialects.xml | 1 + src/Socialbox/Classes/Configuration.php | 6 +- src/Socialbox/Classes/ServerResolver.php | 85 +++++++--- .../Managers/ResolvedServersManager.php | 145 ++++++++++++++++++ .../Objects/Database/ResolvedServerRecord.php | 89 ++++++++++- .../Managers/ResolvedServersManagerTest.php | 43 ++++++ .../Socialbox/Managers/SessionManagerTest.php | 2 +- 7 files changed, 345 insertions(+), 26 deletions(-) create mode 100644 src/Socialbox/Managers/ResolvedServersManager.php create mode 100644 tests/Socialbox/Managers/ResolvedServersManagerTest.php diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml index 2f0c570..016146b 100644 --- a/.idea/sqldialects.xml +++ b/.idea/sqldialects.xml @@ -7,6 +7,7 @@ + diff --git a/src/Socialbox/Classes/Configuration.php b/src/Socialbox/Classes/Configuration.php index 7b5814f..fe86406 100644 --- a/src/Socialbox/Classes/Configuration.php +++ b/src/Socialbox/Classes/Configuration.php @@ -60,9 +60,9 @@ class Configuration $config->save(); self::$configuration = $config->getConfiguration(); - self::$databaseConfiguration = self::$configuration['database']; - self::$cacheConfiguration = self::$configuration['cache']; - self::$registrationConfiguration = self::$configuration['registration']; + self::$databaseConfiguration = new DatabaseConfiguration(self::$configuration['database']); + self::$cacheConfiguration = new CacheConfiguration(self::$configuration['cache']); + self::$registrationConfiguration = new RegistrationConfiguration(self::$configuration['registration']); } /** diff --git a/src/Socialbox/Classes/ServerResolver.php b/src/Socialbox/Classes/ServerResolver.php index 58bf1ff..29d25a1 100644 --- a/src/Socialbox/Classes/ServerResolver.php +++ b/src/Socialbox/Classes/ServerResolver.php @@ -2,51 +2,96 @@ namespace Socialbox\Classes; +use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\ResolutionException; +use Socialbox\Managers\ResolvedServersManager; use Socialbox\Objects\ResolvedServer; class ServerResolver { + private const string PATTERN = '/v=socialbox;sb-rpc=(https?:\/\/[^;]+);sb-key=([^;]+)/'; + /** * 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. + * @throws DatabaseOperationException */ public static function resolveDomain(string $domain): ResolvedServer { - $txtRecords = dns_get_record($domain, DNS_TXT); + // First query the database to check if the domain is already resolved + if(ResolvedServersManager::resolvedServerExists($domain)) + { + // If the resolved server was updated in the last 30 minutes, return it + if(ResolvedServersManager::getResolvedServerUpdated($domain) > (time() - 1800)) + { + return ResolvedServersManager::getResolvedServer($domain)->toResolvedServer(); + } + } + + $txtRecords = self::dnsGetTxtRecords($domain); if ($txtRecords === false) { throw new ResolutionException(sprintf("Failed to resolve DNS TXT records for %s", $domain)); } - $endpoint = null; - $publicKey = null; + $fullRecord = self::concatenateTxtRecords($txtRecords); + + if (preg_match(self::PATTERN, $fullRecord, $matches)) + { + $endpoint = trim($matches[1]); + $publicKey = trim(str_replace(' ', '', $matches[2])); + + if (empty($endpoint)) + { + throw new ResolutionException(sprintf("Failed to resolve RPC endpoint for %s", $domain)); + } + + if (empty($publicKey)) + { + throw new ResolutionException(sprintf("Failed to resolve public key for %s", $domain)); + } + + return new ResolvedServer($endpoint, $publicKey); + } + else + { + throw new ResolutionException(sprintf("Failed to find valid SocialBox record for %s", $domain)); + } + } + + /** + * Retrieves the TXT records for a given domain using the dns_get_record function. + * + * @param string $domain The domain name to fetch TXT records for. + * @return array|false An array of DNS TXT records on success, or false on failure. + */ + private static function dnsGetTxtRecords(string $domain) + { + return dns_get_record($domain, DNS_TXT); + } + + /** + * Concatenates an array of TXT records into a single string. + * + * @param array $txtRecords An array of TXT records, where each record is expected to have a 'txt' key. + * @return string A concatenated string of all TXT records. + */ + private static function concatenateTxtRecords(array $txtRecords): string + { + $fullRecordBuilder = ''; + foreach ($txtRecords as $txt) { - if (isset($txt['txt']) && str_starts_with($txt['txt'], 'socialbox=')) + if (isset($txt['txt'])) { - $endpoint = substr($txt['txt'], strlen('socialbox=')); - } - elseif (isset($txt['txt']) && str_starts_with($txt['txt'], 'socialbox-key=')) - { - $publicKey = substr($txt['txt'], strlen('socialbox-key=')); + $fullRecordBuilder .= trim($txt['txt'], '" '); } } - 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); + return $fullRecordBuilder; } } \ No newline at end of file diff --git a/src/Socialbox/Managers/ResolvedServersManager.php b/src/Socialbox/Managers/ResolvedServersManager.php new file mode 100644 index 0000000..69ae2d1 --- /dev/null +++ b/src/Socialbox/Managers/ResolvedServersManager.php @@ -0,0 +1,145 @@ +prepare("SELECT COUNT(*) FROM resolved_servers WHERE domain=?"); + $statement->bindParam(1, $domain); + $statement->execute(); + return $statement->fetchColumn() > 0; + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to check if a resolved server exists in the database', $e); + } + } + + /** + * Deletes a resolved server from the database. + * + * @param string $domain The domain name of the server to be deleted. + * @return void + * @throws DatabaseOperationException If the deletion operation fails. + */ + public static function deleteResolvedServer(string $domain): void + { + try + { + $statement = Database::getConnection()->prepare("DELETE FROM resolved_servers WHERE domain=?"); + $statement->bindParam(1, $domain); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to delete a resolved server from the database', $e); + } + } + + /** + * Retrieves the last updated date of a resolved server based on its domain. + * + * @param string $domain The domain of the resolved server. + * @return DateTime The last updated date and time of the resolved server. + */ + public static function getResolvedServerUpdated(string $domain): DateTime + { + try + { + $statement = Database::getConnection()->prepare("SELECT updated FROM resolved_servers WHERE domain=?"); + $statement->bindParam(1, $domain); + $statement->execute(); + $result = $statement->fetchColumn(); + return new DateTime($result); + } + catch(Exception $e) + { + throw new DatabaseOperationException('Failed to get the updated date of a resolved server from the database', $e); + } + } + + /** + * Retrieves the resolved server record from the database for a given domain. + * + * @param string $domain The domain name for which to retrieve the resolved server record. + * @return ResolvedServerRecord The resolved server record associated with the given domain. + * @throws DatabaseOperationException If there is an error retrieving the resolved server record from the database. + */ + public static function getResolvedServer(string $domain): ResolvedServerRecord + { + try + { + $statement = Database::getConnection()->prepare("SELECT * FROM resolved_servers WHERE domain=?"); + $statement->bindParam(1, $domain); + $statement->execute(); + $result = $statement->fetch(); + return new ResolvedServerRecord($result); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to get a resolved server from the database', $e); + } + } + + /** + * Adds or updates a resolved server in the database. + * + * @param string $domain The domain name of the resolved server. + * @param ResolvedServer $resolvedServer The resolved server object containing endpoint and public key. + * @return void + * @throws DatabaseOperationException If a database operation fails. + */ + public static function addResolvedServer(string $domain, ResolvedServer $resolvedServer): void + { + $endpoint = $resolvedServer->getEndpoint(); + $publicKey = $resolvedServer->getPublicKey(); + + if(self::resolvedServerExists($domain)) + { + try + { + $statement = Database::getConnection()->prepare("UPDATE resolved_servers SET endpoint=?, public_key=?, updated=NOW() WHERE domain=?"); + $statement->bindParam(1, $endpoint); + $statement->bindParam(2, $publicKey); + $statement->bindParam(3, $domain); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to update a resolved server in the database', $e); + } + } + + try + { + $statement = Database::getConnection()->prepare("INSERT INTO resolved_servers (domain, endpoint, public_key) VALUES (?, ?, ?)"); + $statement->bindParam(1, $domain); + $statement->bindParam(2, $endpoint); + $statement->bindParam(3, $publicKey); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to add a resolved server to the database', $e); + } + } +} \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/ResolvedServerRecord.php b/src/Socialbox/Objects/Database/ResolvedServerRecord.php index 262a8b1..12a31fb 100644 --- a/src/Socialbox/Objects/Database/ResolvedServerRecord.php +++ b/src/Socialbox/Objects/Database/ResolvedServerRecord.php @@ -2,17 +2,97 @@ namespace Socialbox\Objects\Database; +use DateTime; use Socialbox\Interfaces\SerializableInterface; +use Socialbox\Objects\ResolvedServer; class ResolvedServerRecord implements SerializableInterface { + private string $domain; + private string $endpoint; + private string $publicKey; + private DateTime $updated; + + /** + * Constructs a new instance of the class. + * + * @param array $data An associative array containing the domain, endpoint, public_key, and updated values. + * @throws \DateMalformedStringException + */ + public function __construct(array $data) + { + $this->domain = (string)$data['domain']; + $this->endpoint = (string)$data['endpoint']; + $this->publicKey = (string)$data['public_key']; + + if(is_null($data['updated'])) + { + $this->updated = new DateTime(); + } + elseif (is_string($data['updated'])) + { + $this->updated = new DateTime($data['updated']); + } + else + { + $this->updated = $data['updated']; + } + } + + /** + * + * @return string The domain value. + */ + public function getDomain(): string + { + return $this->domain; + } + + /** + * + * @return string The endpoint value. + */ + public function getEndpoint(): string + { + return $this->endpoint; + } + + /** + * + * @return string The public key. + */ + public function getPublicKey(): string + { + return $this->publicKey; + } + + /** + * Retrieves the timestamp of the last update. + * + * @return DateTime The DateTime object representing the last update time. + */ + public function getUpdated(): DateTime + { + return $this->updated; + } + + /** + * Converts the record to a ResolvedServer object. + * + * @return ResolvedServer The ResolvedServer object. + */ + public function toResolvedServer(): ResolvedServer + { + return new ResolvedServer($this->endpoint, $this->publicKey); + } /** * @inheritDoc + * @throws \DateMalformedStringException */ public static function fromArray(array $data): object { - // TODO: Implement fromArray() method. + return new self($data); } /** @@ -20,6 +100,11 @@ class ResolvedServerRecord implements SerializableInterface */ public function toArray(): array { - // TODO: Implement toArray() method. + return [ + 'domain' => $this->domain, + 'endpoint' => $this->endpoint, + 'public_key' => $this->publicKey, + 'updated' => $this->updated->format('Y-m-d H:i:s') + ]; } } \ No newline at end of file diff --git a/tests/Socialbox/Managers/ResolvedServersManagerTest.php b/tests/Socialbox/Managers/ResolvedServersManagerTest.php new file mode 100644 index 0000000..0813345 --- /dev/null +++ b/tests/Socialbox/Managers/ResolvedServersManagerTest.php @@ -0,0 +1,43 @@ +assertInstanceOf(DateTime::class, ResolvedServersManager::getResolvedServerUpdated('n64.cc')); + } + + public function testResolvedServerExists() + { + ResolvedServersManager::addResolvedServer('n64.cc', ServerResolver::resolveDomain('n64.cc')); + $this->assertTrue(ResolvedServersManager::resolvedServerExists('n64.cc')); + } + + public function testGetResolvedServer() + { + ResolvedServersManager::addResolvedServer('n64.cc', ServerResolver::resolveDomain('n64.cc')); + $resolvedServer = ResolvedServersManager::getResolvedServer('n64.cc'); + + $this->assertEquals('n64.cc', $resolvedServer->getDomain()); + $this->assertIsString($resolvedServer->getEndpoint()); + $this->assertIsString($resolvedServer->getPublicKey()); + $this->assertInstanceOf(DateTime::class, $resolvedServer->getUpdated()); + } +} diff --git a/tests/Socialbox/Managers/SessionManagerTest.php b/tests/Socialbox/Managers/SessionManagerTest.php index b376361..8fea5cb 100644 --- a/tests/Socialbox/Managers/SessionManagerTest.php +++ b/tests/Socialbox/Managers/SessionManagerTest.php @@ -35,6 +35,6 @@ class SessionManagerTest extends TestCase $this->assertInstanceOf(SessionRecord::class, $session); $this->assertEquals($uuid, $session->getUuid()); - $this->assertEquals($keyPair->getPublicKey(), Utilities::base64encode($session->getPublicKey())); + $this->assertEquals($keyPair->getPublicKey(), $session->getPublicKey()); } } From f874cc0e411a6fe27a3bec889df0664ee3f4d99e Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 25 Oct 2024 14:24:52 -0400 Subject: [PATCH 015/420] Refactor GitHub Actions workflow --- .github/workflows/ncc_workflow.yml | 306 +++++++++++++++++++++++++++-- Makefile | 26 ++- 2 files changed, 303 insertions(+), 29 deletions(-) diff --git a/.github/workflows/ncc_workflow.yml b/.github/workflows/ncc_workflow.yml index afa2f41..1b5e4e4 100644 --- a/.github/workflows/ncc_workflow.yml +++ b/.github/workflows/ncc_workflow.yml @@ -9,7 +9,7 @@ on: workflow_dispatch: jobs: - build: + release: runs-on: ubuntu-latest container: image: php:8.3 @@ -51,14 +51,163 @@ jobs: - name: Build project run: | - ncc build --config release --log-level debug + ncc build --config release --build-source --log-level debug - - name: Upload build artifacts + - name: Upload build artifact uses: actions/upload-artifact@v4 with: - name: Socialbox_build + name: release path: build/release/net.nosial.socialbox.ncc + debug: + runs-on: ubuntu-latest + container: + image: php:8.3 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + apt update -yqq + apt install git libpq-dev libzip-dev zip make wget gnupg -yqq + + - name: Install phive + run: | + wget -O phive.phar https://phar.io/releases/phive.phar + wget -O phive.phar.asc https://phar.io/releases/phive.phar.asc + gpg --keyserver hkps://keys.openpgp.org --recv-keys 0x9D8A98B29B2D5D79 + gpg --verify phive.phar.asc phive.phar + chmod +x phive.phar + mv phive.phar /usr/local/bin/phive + + - name: Install phab + run: | + phive install phpab --global --trust-gpg-keys 0x2A8299CE842DD38C + + - name: Install latest version of NCC + run: | + git clone https://git.n64.cc/nosial/ncc.git + cd ncc + make redist + NCC_DIR=$(find build/ -type d -name "ncc_*" | head -n 1) + if [ -z "$NCC_DIR" ]; then + echo "NCC build directory not found" + exit 1 + fi + php "$NCC_DIR/INSTALL" --auto + cd .. && rm -rf ncc + + - name: Build project + run: | + ncc build --config debug --build-source --log-level debug + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: debug + path: build/debug/net.nosial.socialbox.ncc + release_executable: + runs-on: ubuntu-latest + container: + image: php:8.3 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + apt update -yqq + apt install git libpq-dev libzip-dev zip make wget gnupg -yqq + + - name: Install phive + run: | + wget -O phive.phar https://phar.io/releases/phive.phar + wget -O phive.phar.asc https://phar.io/releases/phive.phar.asc + gpg --keyserver hkps://keys.openpgp.org --recv-keys 0x9D8A98B29B2D5D79 + gpg --verify phive.phar.asc phive.phar + chmod +x phive.phar + mv phive.phar /usr/local/bin/phive + + - name: Install phab + run: | + phive install phpab --global --trust-gpg-keys 0x2A8299CE842DD38C + + - name: Install latest version of NCC + run: | + git clone https://git.n64.cc/nosial/ncc.git + cd ncc + make redist + NCC_DIR=$(find build/ -type d -name "ncc_*" | head -n 1) + if [ -z "$NCC_DIR" ]; then + echo "NCC build directory not found" + exit 1 + fi + php "$NCC_DIR/INSTALL" --auto + cd .. && rm -rf ncc + + - name: Build project + run: | + ncc build --config release_executable --build-source --log-level debug + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: release_executable + path: build/release/Socialbox + debug_executable: + runs-on: ubuntu-latest + container: + image: php:8.3 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + apt update -yqq + apt install git libpq-dev libzip-dev zip make wget gnupg -yqq + + - name: Install phive + run: | + wget -O phive.phar https://phar.io/releases/phive.phar + wget -O phive.phar.asc https://phar.io/releases/phive.phar.asc + gpg --keyserver hkps://keys.openpgp.org --recv-keys 0x9D8A98B29B2D5D79 + gpg --verify phive.phar.asc phive.phar + chmod +x phive.phar + mv phive.phar /usr/local/bin/phive + + - name: Install phab + run: | + phive install phpab --global --trust-gpg-keys 0x2A8299CE842DD38C + + - name: Install latest version of NCC + run: | + git clone https://git.n64.cc/nosial/ncc.git + cd ncc + make redist + NCC_DIR=$(find build/ -type d -name "ncc_*" | head -n 1) + if [ -z "$NCC_DIR" ]; then + echo "NCC build directory not found" + exit 1 + fi + php "$NCC_DIR/INSTALL" --auto + cd .. && rm -rf ncc + + - name: Build project + run: | + ncc build --config debug_executable --build-source --log-level debug + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: debug_executable + path: build/debug/Socialbox + + + # Checking for phpunit.xml check-phpunit: runs-on: ubuntu-latest outputs: @@ -74,9 +223,59 @@ jobs: else echo "phpunit-exists=false" >> $GITHUB_OUTPUT fi + # Checking for phpdoc.dist.xml + check-phpdoc: + runs-on: ubuntu-latest + outputs: + phpdoc-exists: ${{ steps.check.outputs.phpdoc-exists }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Check for phpdoc.dist.xml + id: check + run: | + if [ -f phpdoc.dist.xml ]; then + echo "phpdoc-exists=true" >> $GITHUB_OUTPUT + else + echo "phpdoc-exists=false" >> $GITHUB_OUTPUT + fi + generate-phpdoc: + needs: [release, check-phpdoc] + runs-on: ubuntu-latest + container: + image: php:8.3 + if: needs.check-phpdoc.outputs.phpdoc-exists == 'true' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + apt update -yqq + apt install git libpq-dev libzip-dev zip make wget gnupg -yqq + + - name: Download PHPDocumentor + run: | + wget https://phpdoc.org/phpDocumentor.phar + chmod +x phpDocumentor.phar + + - name: Generate PHPDoc + run: | + php phpDocumentor.phar -d src -t docs + + - name: Archive PHPDoc + run: | + zip -r docs.zip docs + + - name: Upload PHPDoc + uses: actions/upload-artifact@v4 + with: + name: documentation + path: docs.zip test: - needs: [build, check-phpunit] + needs: [release, debug, release_executable, debug_executable, check-phpunit] runs-on: ubuntu-latest container: image: php:8.3 @@ -89,8 +288,8 @@ jobs: - name: Download build artifacts uses: actions/download-artifact@v4 with: - name: Socialbox_build - path: Socialbox_build # Adjust this to download the artifact directly under 'Socialbox_build' + name: release + path: release - name: Install dependencies run: | @@ -98,7 +297,7 @@ jobs: apt install git libpq-dev libzip-dev zip make wget gnupg -yqq curl -sSLf -o /usr/local/bin/install-php-extensions https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions chmod +x /usr/local/bin/install-php-extensions - install-php-extensions zip pdo_mysql mysqli mcrypt + install-php-extensions zip - name: Install phive run: | @@ -128,15 +327,22 @@ jobs: - name: Install NCC packages run: | - ncc package install --package="Socialbox_build/net.nosial.socialbox.ncc" --build-source --reinstall -y --log-level debug + ncc package install --package="release/net.nosial.socialbox.ncc" --build-source --reinstall -y --log-level debug - name: Run PHPUnit tests run: | wget https://phar.phpunit.de/phpunit-11.3.phar - php phpunit-11.3.phar --configuration phpunit.xml + php phpunit-11.3.phar --configuration phpunit.xml --log-junit reports/junit.xml --log-teamcity reports/teamcity --testdox-html reports/testdox.html --testdox-text reports/testdox.txt - release: - needs: [build, test] + - name: Upload test reports + uses: actions/upload-artifact@v4 + with: + name: reports + path: reports + + + release-documentation: + needs: generate-phpdoc permissions: write-all runs-on: ubuntu-latest container: @@ -147,16 +353,78 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Download build artifacts + - name: Download documentation artifact uses: actions/download-artifact@v4 with: - name: Socialbox_build - path: Socialbox_build + name: documentation + path: documentation - - name: Upload to GitHub Release + - name: Upload documentation artifact uses: softprops/action-gh-release@v1 with: - files: | - Socialbox_build/net.nosial.socialbox.ncc + files: | + documentation/* env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + + release-artifacts: + needs: [release, debug, release_executable, debug_executable] + permissions: write-all + runs-on: ubuntu-latest + container: + image: php:8.3 + if: github.event_name == 'release' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download release artifact + uses: actions/download-artifact@v4 + with: + name: release + path: release + - name: Upload release artifact to release + uses: softprops/action-gh-release@v1 + with: + files: | + release/* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Download debug artifact + uses: actions/download-artifact@v4 + with: + name: debug + path: debug + - name: Upload debug artifact to release + uses: softprops/action-gh-release@v1 + with: + files: | + debug/* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Download release_executable artifact + uses: actions/download-artifact@v4 + with: + name: release_executable + path: release_executable + - name: Upload release_executable artifact to release + uses: softprops/action-gh-release@v1 + with: + files: | + release_executable/* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Download debug_executable artifact + uses: actions/download-artifact@v4 + with: + name: debug_executable + path: debug_executable + - name: Upload debug_executable artifact to release + uses: softprops/action-gh-release@v1 + with: + files: | + debug_executable/* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/Makefile b/Makefile index 424ebce..ce5da5d 100644 --- a/Makefile +++ b/Makefile @@ -1,23 +1,29 @@ # Variables -CONFIG ?= release +DEFAULT_CONFIGURATION ?= release LOG_LEVEL = debug -OUTDIR = build/$(CONFIG) -PACKAGE = $(OUTDIR)/net.nosial.socialbox.ncc # Default Target -all: build +all: release debug release_executable debug_executable # Build Steps -build: - ncc build --config=$(CONFIG) --log-level $(LOG_LEVEL) +release: + ncc build --config=release --log-level $(LOG_LEVEL) +debug: + ncc build --config=debug --log-level $(LOG_LEVEL) +release_executable: + ncc build --config=release_executable --log-level $(LOG_LEVEL) +debug_executable: + ncc build --config=debug_executable --log-level $(LOG_LEVEL) -install: build - ncc package install --package=$(PACKAGE) --skip-dependencies --build-source --reinstall -y --log-level $(LOG_LEVEL) -test: build +install: release + ncc package install --package=build/release/net.nosial.socialbox.ncc --skip-dependencies --build-source --reinstall -y --log-level $(LOG_LEVEL) + +test: release + [ -f phpunit.xml ] || { echo "phpunit.xml not found"; exit 1; } phpunit clean: rm -rf build -.PHONY: all build install test clean \ No newline at end of file +.PHONY: all install test clean release debug release_executable debug_executable \ No newline at end of file From 425fa862a89f1133d9a519c97fc83c3718a2f615 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 25 Oct 2024 14:28:04 -0400 Subject: [PATCH 016/420] Updated .gitlab-ci.yml --- .gitlab-ci.yml | 188 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 156 insertions(+), 32 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 52ab115..185dca3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,46 +1,170 @@ -image: php:8.1 +# Global config +default: + image: php:8.3 -before_script: - # Install some stuff that the image doesn't come with - - apt update -yqq - - apt install git libpq-dev libzip-dev zip make wget gnupg -yqq +variables: + GIT_STRATEGY: clone - # Install phive - - wget -O phive.phar https://phar.io/releases/phive.phar - - wget -O phive.phar.asc https://phar.io/releases/phive.phar.asc - - gpg --keyserver hkps://keys.openpgp.org --recv-keys 0x9D8A98B29B2D5D79 - - gpg --verify phive.phar.asc phive.phar - - chmod +x phive.phar - - mv phive.phar /usr/local/bin/phive +workflow: + rules: + - if: $CI_PIPELINE_SOURCE == "push" + - if: $CI_PIPELINE_SOURCE == "web" + - if: $CI_COMMIT_TAG - # Install phab - - phive install phpab --global --trust-gpg-keys 0x2A8299CE842DD38C +# Reusable template for installing common dependencies +.setup_template: &setup_definition + before_script: + - apt update -yqq + - apt install git libpq-dev libzip-dev zip make wget gnupg -yqq + # Install phive + - wget -O phive.phar https://phar.io/releases/phive.phar + - wget -O phive.phar.asc https://phar.io/releases/phive.phar.asc + - gpg --keyserver hkps://keys.openpgp.org --recv-keys 0x9D8A98B29B2D5D79 + - gpg --verify phive.phar.asc phive.phar + - chmod +x phive.phar + - mv phive.phar /usr/local/bin/phive + # Install phab + - phive install phpab --global --trust-gpg-keys 0x2A8299CE842DD38C + # Install NCC + - git clone https://git.n64.cc/nosial/ncc.git + - cd ncc + - make redist + - NCC_DIR=$(find build/ -type d -name "ncc_*" | head -n 1) + - if [ -z "$NCC_DIR" ]; then echo "NCC build directory not found"; exit 1; fi + - php "$NCC_DIR/INSTALL" --auto + - cd .. + - rm -rf ncc - # Install the latest version of ncc (Nosial Code Compiler) - - git clone https://git.n64.cc/nosial/ncc.git - - cd ncc - - make redist - - php build/src/INSTALL --auto --install-composer - - cd .. && rm -rf ncc - -build: +# Build jobs +release: + <<: *setup_definition stage: build script: - - ncc build --config release --log-level debug + - ncc build --config release --build-source --log-level debug artifacts: paths: - - build/ - rules: - - if: $CI_COMMIT_BRANCH + - build/release/net.nosial.socialbox.ncc + expire_in: 1 week -release: - stage: deploy +debug: + <<: *setup_definition + stage: build script: - - ncc build --config release --log-level debug - - > - curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file build/release/net.nosial.socialbox.ncc "$CI_API_V4_URL/projects/$CI_PROJECT_ID/packages/generic/net.nosial.socialbox/$CI_COMMIT_REF_NAME/net.nosial.socialbox.ncc" + - ncc build --config debug --build-source --log-level debug artifacts: paths: - - build/ + - build/debug/net.nosial.socialbox.ncc + expire_in: 1 week + +release_executable: + <<: *setup_definition + stage: build + script: + - ncc build --config release_executable --build-source --log-level debug + artifacts: + paths: + - build/release/Socialbox + expire_in: 1 week + +debug_executable: + <<: *setup_definition + stage: build + script: + - ncc build --config debug_executable --build-source --log-level debug + artifacts: + paths: + - build/debug/Socialbox + expire_in: 1 week + +# Check for configuration files +check_configs: + stage: .pre + script: + - | + if [ -f phpunit.xml ]; then + echo "PHPUNIT_EXISTS=true" >> build.env + else + echo "PHPUNIT_EXISTS=false" >> build.env + fi + if [ -f phpdoc.dist.xml ]; then + echo "PHPDOC_EXISTS=true" >> build.env + else + echo "PHPDOC_EXISTS=false" >> build.env + fi + artifacts: + reports: + dotenv: build.env + +# Documentation generation +generate-phpdoc: + <<: *setup_definition + stage: build + needs: ["check_configs"] + rules: + - if: $PHPDOC_EXISTS == "true" + script: + - wget https://phpdoc.org/phpDocumentor.phar + - chmod +x phpDocumentor.phar + - php phpDocumentor.phar -d src -t docs + - zip -r docs.zip docs + artifacts: + paths: + - docs.zip + expire_in: 1 week + +# Testing +test: + <<: *setup_definition + stage: test + needs: + - job: release + - job: check_configs + rules: + - if: $PHPUNIT_EXISTS == "true" + script: + - curl -sSLf -o /usr/local/bin/install-php-extensions https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions + - chmod +x /usr/local/bin/install-php-extensions + - install-php-extensions zip + - ncc package install --package="build/release/net.nosial.socialbox.ncc" --build-source --reinstall -y --log-level debug + - wget https://phar.phpunit.de/phpunit-11.3.phar + - php phpunit-11.3.phar --configuration phpunit.xml --log-junit reports/junit.xml --log-teamcity reports/teamcity --testdox-html reports/testdox.html --testdox-text reports/testdox.txt + artifacts: + reports: + junit: reports/junit.xml + paths: + - reports/ + expire_in: 1 week + +# Release jobs +release_upload: + stage: deploy rules: - if: $CI_COMMIT_TAG + needs: + - job: release + - job: debug + - job: release_executable + - job: debug_executable + - job: generate-phpdoc + optional: true + script: + - | + if [ -f "docs.zip" ]; then + echo "Releasing documentation..." + curl --request POST \ + --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \ + --form "file=@docs.zip" \ + "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/uploads" + fi + + # Upload release artifacts + for artifact in build/release/net.nosial.socialbox.ncc build/debug/net.nosial.socialbox.ncc build/release/Socialbox build/debug/Socialbox; do + if [ -f "$artifact" ]; then + echo "Uploading $artifact..." + curl --request POST \ + --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \ + --form "file=@${artifact}" \ + "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/uploads" + fi + done + environment: production \ No newline at end of file From 8334816d35e1d3df7210c84811d78fbbe8cac33a Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 30 Oct 2024 15:12:10 -0400 Subject: [PATCH 017/420] Refactor initialization logging and configuration checks --- .../Classes/CliCommands/InitializeCommand.php | 88 +++++++++++++------ 1 file changed, 62 insertions(+), 26 deletions(-) diff --git a/src/Socialbox/Classes/CliCommands/InitializeCommand.php b/src/Socialbox/Classes/CliCommands/InitializeCommand.php index 07b71e8..c45e09d 100644 --- a/src/Socialbox/Classes/CliCommands/InitializeCommand.php +++ b/src/Socialbox/Classes/CliCommands/InitializeCommand.php @@ -9,11 +9,11 @@ use Socialbox\Abstracts\CacheLayer; use Socialbox\Classes\Configuration; use Socialbox\Classes\Cryptography; use Socialbox\Classes\Database; +use Socialbox\Classes\Logger; use Socialbox\Classes\Resources; use Socialbox\Enums\DatabaseObjects; -use Socialbox\Exceptions\DatabaseOperationException; +use Socialbox\Exceptions\CryptographyException; use Socialbox\Interfaces\CliCommandInterface; -use Socialbox\Managers\VariableManager; class InitializeCommand implements CliCommandInterface { @@ -22,23 +22,55 @@ class InitializeCommand implements CliCommandInterface */ public static function execute(array $args): int { - if(Configuration::getConfiguration()['instance']['enabled'] === false && !isset($args['force'])) + if(Configuration::getInstanceConfiguration()->isEnabled() === false && !isset($args['force'])) { - Log::info('net.nosial.socialbox', 'Socialbox is disabled. Use --force to initialize the instance or set `instance.enabled` to True in the configuration'); + $required_configurations = [ + 'database.host', 'database.port', 'database.username', 'database.password', 'database.name', + 'instance.enabled', 'instance.domain', 'registration.*' + ]; + + Logger::getLogger()->error('Socialbox is disabled. Use --force to initialize the instance or set `instance.enabled` to True in the configuration'); + Logger::getLogger()->info('The reason you are required to do this is to allow you to configure the instance before enabling it'); + Logger::getLogger()->info('The following configurations are required to be set before enabling the instance:'); + foreach($required_configurations as $config) + { + Logger::getLogger()->info(sprintf(' - %s', $config)); + } + + Logger::getLogger()->info('instance.private_key & instance.public_key are automatically generated if not set'); + Logger::getLogger()->info('instance.domain is required to be set to the domain name of the instance'); + Logger::getLogger()->info('instance.rpc_endpoint is required to be set to the publicly accessible http rpc endpoint of this server'); + Logger::getLogger()->info('registration.* are required to be set to allow users to register to the instance'); + Logger::getLogger()->info('You will be given a DNS TXT record to set for the public key after the initialization process'); + Logger::getLogger()->info('The configuration file can be edited using ConfigLib:'); + Logger::getLogger()->info(' configlib --conf socialbox -e nano'); + Logger::getLogger()->info('Or manually at:'); + Logger::getLogger()->info(sprintf(' %s', Configuration::getConfigurationLib()->getPath())); return 1; } - Log::info('net.nosial.socialbox', 'Initializing Socialbox...'); + if(Configuration::getInstanceConfiguration()->getDomain() === null) + { + Logger::getLogger()->error('instance.domain is required but was not set'); + return 1; + } + if(Configuration::getInstanceConfiguration()->getRpcEndpoint() === null) + { + Logger::getLogger()->error('instance.rpc_endpoint is required but was not set'); + return 1; + } + + Logger::getLogger()->info('Initializing Socialbox...'); if(Configuration::getCacheConfiguration()->isEnabled()) { - Log::verbose('net.nosial.socialbox', 'Clearing cache layer...'); + Logger::getLogger()->verbose('Clearing cache layer...'); CacheLayer::getInstance()->clear(); } foreach(DatabaseObjects::casesOrdered() as $object) { - Log::verbose('net.nosial.socialbox', "Initializing database object {$object->value}"); + Logger::getLogger()->verbose("Initializing database object {$object->value}"); try { @@ -49,44 +81,48 @@ class InitializeCommand implements CliCommandInterface // Check if the error code is for "table already exists" if ($e->getCode() === '42S01') { - Log::warning('net.nosial.socialbox', "Database object {$object->value} already exists, skipping..."); + Logger::getLogger()->warning("Database object {$object->value} already exists, skipping..."); continue; } else { - Log::error('net.nosial.socialbox', "Failed to initialize database object {$object->value}: {$e->getMessage()}", $e); + Logger::getLogger()->error("Failed to initialize database object {$object->value}: {$e->getMessage()}", $e); return 1; } } catch(Exception $e) { - Log::error('net.nosial.socialbox', "Failed to initialize database object {$object->value}: {$e->getMessage()}", $e); + Logger::getLogger()->error("Failed to initialize database object {$object->value}: {$e->getMessage()}", $e); return 1; } } - try + if(!Configuration::getInstanceConfiguration()->getPublicKey() || !Configuration::getInstanceConfiguration()->getPrivateKey()) { - - if(!VariableManager::variableExists('PUBLIC_KEY') || !VariableManager::variableExists('PRIVATE_KEY')) + try { - Log::info('net.nosial.socialbox', 'Generating new key pair...'); - + Logger::getLogger()->info('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; + catch (CryptographyException $e) + { + Logger::getLogger()->error('Failed to generate keypair', $e); + return 1; + } + + Logger::getLogger()->info('Updating configuration...'); + Configuration::getConfigurationLib()->set('instance.private_key', $keyPair->getPrivateKey()); + Configuration::getConfigurationLib()->set('instance.public_key', $keyPair->getPublicKey()); + Configuration::getConfigurationLib()->save(); + + Logger::getLogger()->info(sprintf('Set the DNS TXT record for the domain %s to the following value:', Configuration::getInstanceConfiguration()->getDomain())); + Logger::getLogger()->info(sprintf("v=socialbox;sb-rpc=%s;sb-key=%s;", + Configuration::getInstanceConfiguration()->getRpcEndpoint(), $keyPair->getPublicKey() + )); } - Log::info('net.nosial.socialbox', 'Socialbox has been initialized successfully'); + // TODO: Create a host peer here? + Logger::getLogger()->info('Socialbox has been initialized successfully'); return 0; } From 99a4f20ecee397df9693731736c814c59425fd92 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 30 Oct 2024 15:12:55 -0400 Subject: [PATCH 018/420] Add logging and instance configurations --- src/Socialbox/Classes/Configuration.php | 74 +++++++++-- .../Configuration/CacheConfiguration.php | 6 +- .../Configuration/InstanceConfiguration.php | 76 ++++++++++++ .../Configuration/LoggingConfiguration.php | 115 ++++++++++++++++++ .../RegistrationConfiguration.php | 2 +- 5 files changed, 261 insertions(+), 12 deletions(-) create mode 100644 src/Socialbox/Classes/Configuration/InstanceConfiguration.php create mode 100644 src/Socialbox/Classes/Configuration/LoggingConfiguration.php diff --git a/src/Socialbox/Classes/Configuration.php b/src/Socialbox/Classes/Configuration.php index fe86406..4e409d9 100644 --- a/src/Socialbox/Classes/Configuration.php +++ b/src/Socialbox/Classes/Configuration.php @@ -2,14 +2,19 @@ namespace Socialbox\Classes; +use League\Flysystem\Config; use Socialbox\Classes\Configuration\CacheConfiguration; use Socialbox\Classes\Configuration\DatabaseConfiguration; +use Socialbox\Classes\Configuration\InstanceConfiguration; +use Socialbox\Classes\Configuration\LoggingConfiguration; use Socialbox\Classes\Configuration\RegistrationConfiguration; class Configuration { - private static ?array $configuration = null; + private static ?\ConfigLib\Configuration $configuration = null; private static ?DatabaseConfiguration $databaseConfiguration = null; + private static ?InstanceConfiguration $instanceConfiguration = null; + private static ?LoggingConfiguration $loggingConfiguration = null; private static ?CacheConfiguration $cacheConfiguration = null; private static ?RegistrationConfiguration $registrationConfiguration = null; @@ -23,10 +28,16 @@ class Configuration { $config = new \ConfigLib\Configuration('socialbox'); - // False by default, requires the user to enable it. - $config->setDefault('instance.enabled', false); + // Instance configuration + $config->setDefault('instance.enabled', false); // False by default, requires the user to enable it. + $config->setDefault('instance.domain', null); + $config->setDefault('instance.rpc_endpoint', null); + $config->setDefault('instance.private_key', null); + $config->setDefault('instance.public_key', null); + // Security Configuration $config->setDefault('security.display_internal_exceptions', false); + $config->setDefault('security.resolved_servers_ttl', 600); // Database configuration $config->setDefault('database.host', '127.0.0.1'); @@ -35,6 +46,12 @@ class Configuration $config->setDefault('database.password', 'root'); $config->setDefault('database.name', 'test'); + // Logging configuration + $config->setDefault('logging.console_logging_enabled', true); + $config->setDefault('logging.console_logging_level', 'info'); + $config->setDefault('logging.file_logging_enabled', true); + $config->setDefault('logging.file_logging_level', 'error'); + // Cache layer configuration $config->setDefault('cache.enabled', false); $config->setDefault('cache.engine', 'redis'); @@ -56,13 +73,14 @@ class Configuration $config->setDefault('registration.sms_verification_required', false); $config->setDefault('registration.phone_call_verification_required', false); $config->setDefault('registration.image_captcha_verification_required', true); - $config->save(); - self::$configuration = $config->getConfiguration(); - self::$databaseConfiguration = new DatabaseConfiguration(self::$configuration['database']); - self::$cacheConfiguration = new CacheConfiguration(self::$configuration['cache']); - self::$registrationConfiguration = new RegistrationConfiguration(self::$configuration['registration']); + self::$configuration = $config; + self::$databaseConfiguration = new DatabaseConfiguration(self::$configuration->getConfiguration()['database']); + self::$instanceConfiguration = new InstanceConfiguration(self::$configuration->getConfiguration()['instance']); + self::$loggingConfiguration = new LoggingConfiguration(self::$configuration->getConfiguration()['logging']); + self::$cacheConfiguration = new CacheConfiguration(self::$configuration->getConfiguration()['cache']); + self::$registrationConfiguration = new RegistrationConfiguration(self::$configuration->getConfiguration()['registration']); } /** @@ -78,6 +96,16 @@ class Configuration self::initializeConfiguration(); } + return self::$configuration->getConfiguration(); + } + + public static function getConfigurationLib(): \ConfigLib\Configuration + { + if(self::$configuration === null) + { + self::initializeConfiguration(); + } + return self::$configuration; } @@ -96,6 +124,36 @@ class Configuration return self::$databaseConfiguration; } + /** + * Retrieves the current instance configuration. + * + * @return InstanceConfiguration The current instance configuration instance. + */ + public static function getInstanceConfiguration(): InstanceConfiguration + { + if(self::$instanceConfiguration === null) + { + self::initializeConfiguration(); + } + + return self::$instanceConfiguration; + } + + /** + * Retrieves the current logging configuration. + * + * @return LoggingConfiguration The current logging configuration instance. + */ + public static function getLoggingConfiguration(): LoggingConfiguration + { + if(self::$loggingConfiguration === null) + { + self::initializeConfiguration(); + } + + return self::$loggingConfiguration; + } + /** * Retrieves the current cache configuration. If the cache configuration * has not been initialized, it will initialize it first. diff --git a/src/Socialbox/Classes/Configuration/CacheConfiguration.php b/src/Socialbox/Classes/Configuration/CacheConfiguration.php index a080f03..5f5d8f4 100644 --- a/src/Socialbox/Classes/Configuration/CacheConfiguration.php +++ b/src/Socialbox/Classes/Configuration/CacheConfiguration.php @@ -26,9 +26,9 @@ class CacheConfiguration $this->password = $data['password'] ? (string)$data['password'] : null; $this->database = $data['database'] ? (int)$data['database'] : null; - $this->sessionsEnabled = (bool)$data['sessions.enabled']; - $this->sessionsTtl = (int)$data['sessions.ttl']; - $this->sessionsMax = (int)$data['sessions.max']; + $this->sessionsEnabled = (bool)$data['sessions']['enabled']; + $this->sessionsTtl = (int)$data['sessions']['ttl']; + $this->sessionsMax = (int)$data['sessions']['max']; } public function isEnabled(): bool diff --git a/src/Socialbox/Classes/Configuration/InstanceConfiguration.php b/src/Socialbox/Classes/Configuration/InstanceConfiguration.php new file mode 100644 index 0000000..26ea951 --- /dev/null +++ b/src/Socialbox/Classes/Configuration/InstanceConfiguration.php @@ -0,0 +1,76 @@ +enabled = (bool)$data['enabled']; + $this->domain = $data['domain']; + $this->rpcEndpoint = $data['rpc_endpoint']; + $this->privateKey = $data['private_key']; + $this->publicKey = $data['public_key']; + } + + /** + * Checks if the current object is enabled. + * + * @return bool True if the object is enabled, false otherwise. + */ + public function isEnabled(): bool + { + return $this->enabled; + } + + /** + * Retrieves the domain. + * + * @return string|null The domain. + */ + public function getDomain(): ?string + { + return $this->domain; + } + + /** + * @return string|null + */ + public function getRpcEndpoint(): ?string + { + return $this->rpcEndpoint; + } + + /** + * Retrieves the private key. + * + * @return string|null The private key. + */ + public function getPrivateKey(): ?string + { + return $this->privateKey; + } + + /** + * Retrieves the public key. + * + * @return string|null The public key. + */ + public function getPublicKey(): ?string + { + return $this->publicKey; + } +} \ No newline at end of file diff --git a/src/Socialbox/Classes/Configuration/LoggingConfiguration.php b/src/Socialbox/Classes/Configuration/LoggingConfiguration.php new file mode 100644 index 0000000..a499d53 --- /dev/null +++ b/src/Socialbox/Classes/Configuration/LoggingConfiguration.php @@ -0,0 +1,115 @@ +consoleLoggingEnabled = (bool) $data['console_logging_enabled']; + $this->consoleLoggingLevel = $data['console_logging_level']; + $this->fileLoggingEnabled = (bool) $data['file_logging_enabled']; + $this->fileLoggingLevel = $data['file_logging_level']; + } + + /** + * Checks if console logging is enabled. + * + * @return bool True if console logging is enabled, otherwise false. + */ + public function isConsoleLoggingEnabled(): bool + { + return $this->consoleLoggingEnabled; + } + + /** + * Retrieves the logging level for console output. + * + * @return LogLevel The logging level configured for console output. + */ + public function getConsoleLoggingLevel(): LogLevel + { + return $this->parseLogLevel($this->consoleLoggingLevel); + } + + /** + * Checks if file logging is enabled. + * + * @return bool True if file logging is enabled, false otherwise. + */ + public function isFileLoggingEnabled(): bool + { + return $this->fileLoggingEnabled; + } + + /** + * Retrieves the logging level for file logging. + * + * @return LogLevel The logging level set for file logging. + */ + public function getFileLoggingLevel(): LogLevel + { + return $this->parseLogLevel($this->fileLoggingLevel); + } + + /** + * Parses the given log level from string format to a LogLevel enumeration. + * + * @param string $logLevel The log level as a string. + * @return LogLevel The corresponding LogLevel enumeration. + */ + private function parseLogLevel(string $logLevel): LogLevel + { + switch (strtolower($logLevel)) { + case LogLevel::DEBUG: + case 'debug': + case '6': + case 'dbg': + return LogLevel::DEBUG; + case LogLevel::VERBOSE: + case 'verbose': + case '5': + case 'vrb': + return LogLevel::VERBOSE; + default: + case LogLevel::INFO: + case 'info': + case '4': + case 'inf': + return LogLevel::INFO; + case LogLevel::WARNING: + case 'warning': + case '3': + case 'wrn': + return LogLevel::WARNING; + case LogLevel::ERROR: + case 'error': + case '2': + case 'err': + return LogLevel::ERROR; + case LogLevel::FATAL: + case 'fatal': + case '1': + case 'crt': + return LogLevel::FATAL; + case LogLevel::SILENT: + case 'silent': + case '0': + case 'sil': + return LogLevel::SILENT; + } + } +} \ No newline at end of file diff --git a/src/Socialbox/Classes/Configuration/RegistrationConfiguration.php b/src/Socialbox/Classes/Configuration/RegistrationConfiguration.php index 03b0ed6..269124c 100644 --- a/src/Socialbox/Classes/Configuration/RegistrationConfiguration.php +++ b/src/Socialbox/Classes/Configuration/RegistrationConfiguration.php @@ -30,7 +30,7 @@ class RegistrationConfiguration */ public function __construct(array $data) { - $this->registrationEnabled = (bool)$data['registration_enabled']; + $this->registrationEnabled = (bool)$data['enabled']; $this->passwordRequired = (bool)$data['password_required']; $this->otpRequired = (bool)$data['otp_required']; $this->displayNameRequired = (bool)$data['display_name_required']; From 7a6d78ac9dd06f8542e2ab1afa030d7e8bf6059d Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 30 Oct 2024 15:13:04 -0400 Subject: [PATCH 019/420] Add Logger class for managing application logging --- src/Socialbox/Classes/Logger.php | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/Socialbox/Classes/Logger.php diff --git a/src/Socialbox/Classes/Logger.php b/src/Socialbox/Classes/Logger.php new file mode 100644 index 0000000..c6c5d75 --- /dev/null +++ b/src/Socialbox/Classes/Logger.php @@ -0,0 +1,31 @@ +setConsoleLoggingEnabled(Configuration::getLoggingConfiguration()->isConsoleLoggingEnabled()); + self::$logger->setConsoleLoggingLevel(Configuration::getLoggingConfiguration()->getConsoleLoggingLevel()); + self::$logger->setFileLoggingEnabled(Configuration::getLoggingConfiguration()->isFileLoggingEnabled()); + self::$logger->setFileLoggingLevel(Configuration::getLoggingConfiguration()->getFileLoggingLevel()); + + Log::registerExceptionHandler(); + Log::register(self::$logger); + } + + return self::$logger; + } +} \ No newline at end of file From 0579d35b6d466be2e7d7bafb8e86f113e7d9fb5b Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 30 Oct 2024 15:13:19 -0400 Subject: [PATCH 020/420] Updated .idea files --- .idea/php.xml | 2 +- .idea/runConfigurations/{Build.xml => Build_Release.xml} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename .idea/runConfigurations/{Build.xml => Build_Release.xml} (57%) diff --git a/.idea/php.xml b/.idea/php.xml index d8ba44c..2f19bb6 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -12,6 +12,7 @@ + @@ -22,7 +23,6 @@ - diff --git a/.idea/runConfigurations/Build.xml b/.idea/runConfigurations/Build_Release.xml similarity index 57% rename from .idea/runConfigurations/Build.xml rename to .idea/runConfigurations/Build_Release.xml index a601571..cc2ede7 100644 --- a/.idea/runConfigurations/Build.xml +++ b/.idea/runConfigurations/Build_Release.xml @@ -1,6 +1,6 @@ - - + + From b09cdde9e726c04eaeacabba85520c6c25a15e8b Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 30 Oct 2024 15:13:21 -0400 Subject: [PATCH 021/420] Updated .idea files --- .idea/runConfigurations/Install__sudo_.xml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.idea/runConfigurations/Install__sudo_.xml b/.idea/runConfigurations/Install__sudo_.xml index 5063fe6..a07b24a 100644 --- a/.idea/runConfigurations/Install__sudo_.xml +++ b/.idea/runConfigurations/Install__sudo_.xml @@ -12,8 +12,6 @@ \ No newline at end of file From 9b7b7178c841b0c22437c0bb67c1d5b227d0820d Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 30 Oct 2024 15:25:09 -0400 Subject: [PATCH 022/420] Add logging for peer management operations --- .../Managers/RegisteredPeerManager.php | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/Socialbox/Managers/RegisteredPeerManager.php b/src/Socialbox/Managers/RegisteredPeerManager.php index f5ddfef..b37bc58 100644 --- a/src/Socialbox/Managers/RegisteredPeerManager.php +++ b/src/Socialbox/Managers/RegisteredPeerManager.php @@ -6,6 +6,7 @@ use PDO; use PDOException; use Socialbox\Classes\Configuration; use Socialbox\Classes\Database; +use Socialbox\Classes\Logger; use Socialbox\Enums\Flags\PeerFlags; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; @@ -24,6 +25,8 @@ class RegisteredPeerManager */ public static function usernameExists(string $username): bool { + Logger::getLogger()->verbose(sprintf("Checking if username %s already exists", $username)); + try { $statement = Database::getConnection()->prepare('SELECT COUNT(*) FROM `registered_peers` WHERE username=?'); @@ -49,11 +52,7 @@ class RegisteredPeerManager */ public static function createPeer(string $username, bool $enabled=false): string { - if(self::usernameExists($username)) - { - throw new DatabaseOperationException('The username already exists'); - } - + Logger::getLogger()->verbose(sprintf("Creating a new peer with username %s", $username)); $uuid = Uuid::v4()->toRfc4122(); // If `enabled` is True, we insert the peer into the database as an activated account. @@ -78,11 +77,6 @@ class RegisteredPeerManager // Otherwise, we insert the peer into the database as a disabled account & the required verification flags. $flags = []; - if(Configuration::getRegistrationConfiguration()->isRegistrationEnabled()) - { - $flags[] = PeerFlags::VER_EMAIL; - } - if(Configuration::getRegistrationConfiguration()->isPasswordRequired()) { $flags[] = PeerFlags::VER_SET_PASSWORD; @@ -120,7 +114,7 @@ class RegisteredPeerManager try { - $implodedFlags = implode(',', $flags); + $implodedFlags = implode(',', array_map(fn($flag) => $flag->name, $flags)); $statement = Database::getConnection()->prepare('INSERT INTO `registered_peers` (uuid, username, enabled, flags) VALUES (?, ?, ?, ?)'); $statement->bindParam(1, $uuid); $statement->bindParam(2, $username); @@ -151,6 +145,8 @@ class RegisteredPeerManager $uuid = $uuid->getUuid(); } + Logger::getLogger()->verbose(sprintf("Deleting peer %s", $uuid)); + try { $statement = Database::getConnection()->prepare('DELETE FROM `registered_peers` WHERE uuid=?'); @@ -168,7 +164,6 @@ class RegisteredPeerManager * * @param string|RegisteredPeerRecord $uuid The unique identifier of the registered peer, or an instance of RegisteredPeerRecord. * @return RegisteredPeerRecord Returns a RegisteredPeerRecord object containing the peer's information. - * @throws StandardException If the requested peer does not exist. * @throws DatabaseOperationException If there is an error during the database operation. */ public static function getPeer(string|RegisteredPeerRecord $uuid): RegisteredPeerRecord @@ -178,6 +173,8 @@ class RegisteredPeerManager $uuid = $uuid->getUuid(); } + Logger::getLogger()->verbose(sprintf("Retrieving peer %s from the database", $uuid)); + try { $statement = Database::getConnection()->prepare('SELECT * FROM `registered_peers` WHERE uuid=?'); @@ -188,12 +185,12 @@ class RegisteredPeerManager if($result === false) { - throw new StandardException(sprintf("The requested peer '%s' does not exist", $uuid), StandardError::PEER_NOT_FOUND); + throw new DatabaseOperationException(sprintf("The requested peer '%s' does not exist", $uuid)); } return new RegisteredPeerRecord($result); } - catch(PDOException $e) + catch(PDOException | \DateMalformedStringException $e) { throw new DatabaseOperationException('Failed to get the peer from the database', $e); } @@ -209,6 +206,8 @@ class RegisteredPeerManager */ public static function getPeerByUsername(string $username): RegisteredPeerRecord { + Logger::getLogger()->verbose(sprintf("Retrieving peer %s from the database", $username)); + try { $statement = Database::getConnection()->prepare('SELECT * FROM `registered_peers` WHERE username=?'); @@ -224,7 +223,7 @@ class RegisteredPeerManager return new RegisteredPeerRecord($result); } - catch(PDOException $e) + catch(PDOException | \DateMalformedStringException $e) { throw new DatabaseOperationException('Failed to get the peer from the database', $e); } @@ -244,6 +243,8 @@ class RegisteredPeerManager $uuid = $uuid->getUuid(); } + Logger::getLogger()->verbose(sprintf("Enabling peer %s", $uuid)); + try { $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET enabled=1 WHERE uuid=?'); @@ -270,6 +271,8 @@ class RegisteredPeerManager $uuid = $uuid->getUuid(); } + Logger::getLogger()->verbose(sprintf("Disabling peer %s", $uuid)); + try { $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET enabled=0 WHERE uuid=?'); @@ -289,7 +292,6 @@ class RegisteredPeerManager * @param PeerFlags|array $flags The flag or array of flags to be added to the peer. * @return void * @throws DatabaseOperationException If there is an error while updating the database. - * @throws StandardException If the peer does not exist. */ public static function addFlag(string|RegisteredPeerRecord $uuid, PeerFlags|array $flags): void { @@ -298,6 +300,8 @@ class RegisteredPeerManager $uuid = $uuid->getUuid(); } + Logger::getLogger()->verbose(sprintf("Adding flag(s) %s to peer %s", implode(',', $flags), $uuid)); + $peer = self::getPeer($uuid); $existingFlags = $peer->getFlags(); $flags = is_array($flags) ? $flags : [$flags]; @@ -331,7 +335,6 @@ class RegisteredPeerManager * @param PeerFlags $flag The flag to be removed from the peer. * @return void * @throws DatabaseOperationException If there is an error while updating the database. - * @throws StandardException If the peer does not exist. */ public static function removeFlag(string|RegisteredPeerRecord $uuid, PeerFlags $flag): void { @@ -340,6 +343,8 @@ class RegisteredPeerManager $uuid = $uuid->getUuid(); } + Logger::getLogger()->verbose(sprintf("Removing flag %s from peer %s", $flag->value, $uuid)); + $peer = self::getPeer($uuid); if(!$peer->flagExists($flag)) { From 6a4283ccc606de1a2e92a97debfe1a28c036f847 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 30 Oct 2024 15:27:15 -0400 Subject: [PATCH 023/420] Refactor flag handling and add peer utility methods --- .../Objects/Database/RegisteredPeerRecord.php | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/src/Socialbox/Objects/Database/RegisteredPeerRecord.php b/src/Socialbox/Objects/Database/RegisteredPeerRecord.php index a9b5e20..417c3e4 100644 --- a/src/Socialbox/Objects/Database/RegisteredPeerRecord.php +++ b/src/Socialbox/Objects/Database/RegisteredPeerRecord.php @@ -3,8 +3,11 @@ namespace Socialbox\Objects\Database; use DateTime; +use Socialbox\Classes\Configuration; +use Socialbox\Classes\Logger; use Socialbox\Enums\Flags\PeerFlags; use Socialbox\Interfaces\SerializableInterface; +use Socialbox\Objects\Standard\SelfUser; class RegisteredPeerRecord implements SerializableInterface { @@ -22,7 +25,7 @@ class RegisteredPeerRecord implements SerializableInterface * Constructor for initializing class properties from provided data. * * @param array $data Array containing initialization data. - * @return void + * @throws \DateMalformedStringException */ public function __construct(array $data) { @@ -32,15 +35,7 @@ class RegisteredPeerRecord implements SerializableInterface if($data['flags']) { - if(is_array($data['flags'])) - { - $this->flags = array_map(fn($flag) => PeerFlags::from($flag), $data['flags']); - } - elseif(is_string($data['flags'])) - { - $flags = explode(',', $data['flags']); - $this->flags = array_map(fn($flag) => PeerFlags::from($flag), $flags); - } + $this->flags = PeerFlags::fromString($data['flags']); } else { @@ -79,6 +74,16 @@ class RegisteredPeerRecord implements SerializableInterface return $this->username; } + /** + * Constructs and retrieves the peer address using the current instance's username and the domain from the configuration. + * + * @return string The constructed peer address. + */ + public function getAddress(): string + { + return sprintf("%s@%s", $this->username, Configuration::getInstanceConfiguration()->getDomain()); + } + /** * Retrieves the display name. * @@ -119,6 +124,16 @@ class RegisteredPeerRecord implements SerializableInterface return $this->created; } + /** + * Converts the current instance to a SelfUser object. + * + * @return SelfUser The SelfUser object. + */ + public function toSelfUser(): SelfUser + { + return new SelfUser($this); + } + /** * @inheritDoc */ @@ -136,7 +151,7 @@ class RegisteredPeerRecord implements SerializableInterface 'uuid' => $this->uuid, 'username' => $this->username, 'display_name' => $this->displayName, - 'flags' => implode(',', array_map(fn($flag) => $flag->name, $this->flags)), + 'flags' => PeerFlags::toString($this->flags), 'enabled' => $this->enabled, 'created' => $this->created ]; From ac09674fdda16799a5f51e142436b5d258fe74d5 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 30 Oct 2024 15:27:36 -0400 Subject: [PATCH 024/420] Add username validation to Validator class --- src/Socialbox/Classes/Validator.php | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Socialbox/Classes/Validator.php b/src/Socialbox/Classes/Validator.php index 06e9de3..9fe24a0 100644 --- a/src/Socialbox/Classes/Validator.php +++ b/src/Socialbox/Classes/Validator.php @@ -4,7 +4,8 @@ namespace Socialbox\Classes; class Validator { - private const PEER_ADDRESS_PATTERN = "/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/"; + private const string PEER_ADDRESS_PATTERN = "/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/"; + private const string USERNAME_PATTERN = "/^[a-zA-Z0-9_]+$/"; /** * Validates a peer address @@ -16,4 +17,21 @@ class Validator { return preg_match(self::PEER_ADDRESS_PATTERN, $address) === 1; } + + /** + * Validates a username + * + * @param string $username The username to validate. + * @return bool True if the username is valid, false otherwise. + */ + public static function validateUsername(string $username): bool + { + if(strlen($username) < 3 || strlen($username) > 255) + { + return false; + } + + return preg_match(self::USERNAME_PATTERN, $username) === 1; + } + } \ No newline at end of file From ab03c135e0a9a8dce252c0e62495f63b170bd2cd Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 30 Oct 2024 15:27:46 -0400 Subject: [PATCH 025/420] Add new methods to StandardMethods enum --- src/Socialbox/Enums/StandardMethods.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index 7200d9b..3d683e1 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -3,7 +3,9 @@ namespace Socialbox\Enums; use Socialbox\Classes\StandardMethods\CreateSession; +use Socialbox\Classes\StandardMethods\GetMe; use Socialbox\Classes\StandardMethods\Ping; +use Socialbox\Classes\StandardMethods\Register; use Socialbox\Exceptions\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\ClientRequest; @@ -12,7 +14,9 @@ use Socialbox\Objects\RpcRequest; enum StandardMethods : string { case PING = 'ping'; - case CREATE_SESSION = 'create_session'; + case CREATE_SESSION = 'createSession'; + case REGISTER = 'register'; + case GET_ME = 'getMe'; /** * @param ClientRequest $request @@ -26,6 +30,8 @@ enum StandardMethods : string { self::PING => Ping::execute($request, $rpcRequest), self::CREATE_SESSION => CreateSession::execute($request, $rpcRequest), + self::REGISTER => Register::execute($request, $rpcRequest), + self::GET_ME => GetMe::execute($request, $rpcRequest), }; } } \ No newline at end of file From 5c551c966d225b4bf6d503902ba1d4d3a89225c9 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 30 Oct 2024 15:27:54 -0400 Subject: [PATCH 026/420] Update StandardError enum with additional error cases --- src/Socialbox/Enums/StandardError.php | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Socialbox/Enums/StandardError.php b/src/Socialbox/Enums/StandardError.php index 6485f91..22f3599 100644 --- a/src/Socialbox/Enums/StandardError.php +++ b/src/Socialbox/Enums/StandardError.php @@ -17,11 +17,17 @@ enum StandardError : int // Authentication/Cryptography Errors case INVALID_PUBLIC_KEY = -3000; - case SESSION_NOT_FOUND = -3001; - case UNSUPPORTED_AUTHENTICATION_TYPE = -3002; + case UNSUPPORTED_AUTHENTICATION_TYPE = -3001; + case ALREADY_AUTHENTICATED = -3002; + case AUTHENTICATION_REQUIRED = -3003; + case SESSION_NOT_FOUND = -3004; + case SESSION_REQUIRED = -3005; + case REGISTRATION_DISABLED = -3006; // General Error Messages case PEER_NOT_FOUND = -4000; + case INVALID_USERNAME = -4001; + case USERNAME_ALREADY_EXISTS = -4002; /** * Returns the default generic message for the error @@ -41,8 +47,15 @@ enum StandardError : int self::SERVER_UNAVAILABLE => 'Server temporarily unavailable', self::INVALID_PUBLIC_KEY => 'The given public key is not valid', + self::UNSUPPORTED_AUTHENTICATION_TYPE => 'The requested authentication type is not supported by the server', + self::ALREADY_AUTHENTICATED => 'The action cannot be preformed while authenticated', + self::AUTHENTICATION_REQUIRED => 'Authentication is required to preform this action', self::SESSION_NOT_FOUND => 'The requested session UUID was not found', - self::UNSUPPORTED_AUTHENTICATION_TYPE => 'The requested authentication type is not supported by the server' + self::SESSION_REQUIRED => 'A session is required to preform this action', + + self::PEER_NOT_FOUND => 'The requested peer was not found', + self::INVALID_USERNAME => 'The given username is invalid, it must be Alphanumeric with a minimum of 3 character but no greater than 255 characters', + self::USERNAME_ALREADY_EXISTS => 'The given username already exists on the network' }; } From 413d04a02d666f42a3fa6b31139a81976b32162f Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 30 Oct 2024 15:28:13 -0400 Subject: [PATCH 027/420] Add logging for RPC request processing --- src/Socialbox/Socialbox.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index c4da0d5..be5b683 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -3,7 +3,9 @@ namespace Socialbox; use Exception; + use LogLib\Log; use Socialbox\Classes\Configuration; + use Socialbox\Classes\Logger; use Socialbox\Classes\RpcHandler; use Socialbox\Classes\Utilities; use Socialbox\Enums\StandardError; @@ -21,11 +23,14 @@ } catch(RpcException $e) { + Logger::getLogger()->error('Failed to parse the client request', $e); http_response_code($e->getCode()); print($e->getMessage()); return; } + Logger::getLogger()->verbose(sprintf('Received %d RPC request(s) from %s', count($clientRequest->getRequests()), $_SERVER['REMOTE_ADDR'])); + $results = []; foreach($clientRequest->getRequests() as $rpcRequest) { @@ -33,20 +38,24 @@ if($method === false) { + Logger::getLogger()->warning('The requested method does not exist'); $response = $rpcRequest->produceError(StandardError::RPC_METHOD_NOT_FOUND, 'The requested method does not exist'); } else { try { + Logger::getLogger()->debug(sprintf('Processing RPC request for method %s', $rpcRequest->getMethod())); $response = $method->execute($clientRequest, $rpcRequest); } catch(StandardException $e) { + Logger::getLogger()->error('An error occurred while processing the RPC request', $e); $response = $e->produceError($rpcRequest); } catch(Exception $e) { + Logger::getLogger()->error('An internal error occurred while processing the RPC request', $e); if(Configuration::getConfiguration()['security']['display_internal_exceptions']) { $response = $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, Utilities::throwableToString($e)); @@ -60,22 +69,26 @@ if($response !== null) { + Logger::getLogger()->debug(sprintf('Producing response for method %s', $rpcRequest->getMethod())); $results[] = $response->toArray(); } } if(count($results) == 0) { + Logger::getLogger()->verbose('No results to return'); http_response_code(204); return; } if(count($results) == 1) { + Logger::getLogger()->verbose('Returning single result'); print(json_encode($results[0])); return; } + Logger::getLogger()->verbose('Returning multiple results'); print(json_encode($results)); } } \ No newline at end of file From 1bdfc310e58a401b531108a882cf7cf8dabea9cb Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 30 Oct 2024 15:28:23 -0400 Subject: [PATCH 028/420] Refactor SessionRecord date properties --- src/Socialbox/Objects/Database/SessionRecord.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Socialbox/Objects/Database/SessionRecord.php b/src/Socialbox/Objects/Database/SessionRecord.php index 19c1b6d..ab149ef 100644 --- a/src/Socialbox/Objects/Database/SessionRecord.php +++ b/src/Socialbox/Objects/Database/SessionRecord.php @@ -13,7 +13,7 @@ class SessionRecord implements SerializableInterface private string $publicKey; private SessionState $state; private DateTime $created; - private DateTime $lastRequest; + private ?DateTime $lastRequest; public function __construct(array $data) { @@ -53,12 +53,12 @@ class SessionRecord implements SerializableInterface return $this->state; } - public function getCreated(): int + public function getCreated(): DateTime { return $this->created; } - public function getLastRequest(): int + public function getLastRequest(): ?DateTime { return $this->lastRequest; } From daa5010fffacaa31411abaeb2d87b2c22c37e240 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 30 Oct 2024 15:28:36 -0400 Subject: [PATCH 029/420] Add logging to SessionManager methods --- src/Socialbox/Managers/SessionManager.php | 31 ++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/Socialbox/Managers/SessionManager.php b/src/Socialbox/Managers/SessionManager.php index 88520b1..811e855 100644 --- a/src/Socialbox/Managers/SessionManager.php +++ b/src/Socialbox/Managers/SessionManager.php @@ -5,15 +5,18 @@ use DateMalformedStringException; use DateTime; use InvalidArgumentException; + use LogLib\Log; use PDO; use PDOException; use Socialbox\Classes\Configuration; use Socialbox\Classes\Cryptography; use Socialbox\Classes\Database; + use Socialbox\Classes\Logger; use Socialbox\Enums\SessionState; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\StandardException; + use Socialbox\Objects\Database\RegisteredPeerRecord; use Socialbox\Objects\Database\SessionRecord; use Symfony\Component\Uid\Uuid; @@ -92,6 +95,8 @@ */ public static function getSession(string $uuid): SessionRecord { + Logger::getLogger()->verbose(sprintf("Retrieving session %s from the database", $uuid)); + try { $statement = Database::getConnection()->prepare("SELECT * FROM sessions WHERE uuid=?"); @@ -106,7 +111,15 @@ // Convert the timestamp fields to DateTime objects $data['created'] = new DateTime($data['created']); - $data['last_request'] = new DateTime($data['last_request']); + + if(isset($data['last_request']) && $data['last_request'] !== null) + { + $data['last_request'] = new DateTime($data['last_request']); + } + else + { + $data['last_request'] = null; + } return SessionRecord::fromArray($data); @@ -121,15 +134,23 @@ * Update the authenticated peer associated with the given session UUID. * * @param string $uuid The UUID of the session to update. + * @param RegisteredPeerRecord|string $registeredPeerUuid * @return void * @throws DatabaseOperationException */ - public static function updateAuthenticatedPeer(string $uuid): void + public static function updateAuthenticatedPeer(string $uuid, RegisteredPeerRecord|string $registeredPeerUuid): void { + if($registeredPeerUuid instanceof RegisteredPeerRecord) + { + $registeredPeerUuid = $registeredPeerUuid->getUuid(); + } + + Logger::getLogger()->verbose(sprintf("Assigning peer %s to session %s", $registeredPeerUuid, $uuid)); + try { $statement = Database::getConnection()->prepare("UPDATE sessions SET authenticated_peer_uuid=? WHERE uuid=?"); - $statement->bindParam(1, $uuid); + $statement->bindParam(1, $registeredPeerUuid); $statement->bindParam(2, $uuid); $statement->execute(); } @@ -148,6 +169,8 @@ */ public static function updateLastRequest(string $uuid): void { + Logger::getLogger()->verbose(sprintf("Updating last request timestamp for session %s", $uuid)); + try { $formattedTime = (new DateTime('@' . time()))->format('Y-m-d H:i:s'); @@ -172,6 +195,8 @@ */ public static function updateState(string $uuid, SessionState $state): void { + Logger::getLogger()->verbose(sprintf("Updating state of session %s to %s", $uuid, $state->value)); + try { $state_value = $state->value; From 436022dcbd622fe1b06bb4aea31f342600d33dfd Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 30 Oct 2024 15:28:44 -0400 Subject: [PATCH 030/420] Add SelfUser class to handle user-related operations --- src/Socialbox/Objects/Standard/SelfUser.php | 152 ++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 src/Socialbox/Objects/Standard/SelfUser.php diff --git a/src/Socialbox/Objects/Standard/SelfUser.php b/src/Socialbox/Objects/Standard/SelfUser.php new file mode 100644 index 0000000..109c21e --- /dev/null +++ b/src/Socialbox/Objects/Standard/SelfUser.php @@ -0,0 +1,152 @@ +uuid = $data->getUuid(); + $this->username = $data->getUsername(); + $this->address = + $this->displayName = $data->getDisplayName(); + $this->flags = $data->getFlags(); + $this->created = $data->getCreated()->getTimestamp(); + + return; + } + + $this->uuid = $data['uuid']; + $this->username = $data['username']; + $this->displayName = $data['display_name'] ?? null; + + if(is_string($data['flags'])) + { + $this->flags = PeerFlags::fromString($data['flags']); + } + elseif(is_array($data['flags'])) + { + $this->flags = $data['flags']; + } + else + { + $this->flags = []; + } + + if($data['created'] instanceof DateTime) + { + $this->created = $data['created']->getTimestamp(); + } + else + { + $this->created = $data['created']; + } + + return; + } + + /** + * Retrieves the UUID of the object. + * + * @return string The UUID of the object. + */ + public function getUuid(): string + { + return $this->uuid; + } + + /** + * + * @return string The username of the user. + */ + public function getUsername(): string + { + return $this->username; + } + + /** + * + * @return string|null The display name. + */ + public function getDisplayName(): ?string + { + return $this->displayName; + } + + /** + * + * @return array + */ + public function getFlags(): array + { + return $this->flags; + } + + /** + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->enabled; + } + + /** + * + * @return int The timestamp when the object was created. + */ + public function getCreated(): int + { + return $this->created; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): SelfUser + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + $flags = []; + foreach($this->flags as $flag) + { + $flags[] = $flag->value; + } + + return [ + 'uuid' => $this->uuid, + 'username' => $this->username, + 'display_name' => $this->displayName, + 'flags' => $flags, + 'created' => $this->created + ]; + } +} \ No newline at end of file From 3b55e550c5c95eea7e01d643898f3eda934e4646 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 30 Oct 2024 15:28:52 -0400 Subject: [PATCH 031/420] Add logging for RpcRequest response production --- src/Socialbox/Objects/RpcRequest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Socialbox/Objects/RpcRequest.php b/src/Socialbox/Objects/RpcRequest.php index 366700f..db85a8f 100644 --- a/src/Socialbox/Objects/RpcRequest.php +++ b/src/Socialbox/Objects/RpcRequest.php @@ -4,6 +4,7 @@ namespace Socialbox\Objects; use InvalidArgumentException; use ncc\ThirdParty\nikic\PhpParser\Node\Expr\BinaryOp\BooleanOr; +use Socialbox\Classes\Logger; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\RpcException; use Socialbox\Exceptions\StandardException; @@ -130,6 +131,7 @@ class RpcRequest implements SerializableInterface throw new InvalidArgumentException('The \'$result\' property must either be string, boolean, integer, array, null or SerializableInterface'); } + Logger::getLogger()->verbose(sprintf('Producing response for request %s', $this->id)); return new RpcResponse($this->id, $result); } From 44143cb8cdfec44ea4634c5cb7a9617e299ae62d Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 30 Oct 2024 15:29:00 -0400 Subject: [PATCH 032/420] Add user registration method --- .../Classes/StandardMethods/Register.php | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/Socialbox/Classes/StandardMethods/Register.php diff --git a/src/Socialbox/Classes/StandardMethods/Register.php b/src/Socialbox/Classes/StandardMethods/Register.php new file mode 100644 index 0000000..40c1904 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/Register.php @@ -0,0 +1,80 @@ +isRegistrationEnabled()) + { + return $rpcRequest->produceError(StandardError::REGISTRATION_DISABLED, StandardError::REGISTRATION_DISABLED->getMessage()); + } + + // Check if the username parameter exists + if(!$rpcRequest->containsParameter('username')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing parameter \'username\''); + } + + // Check if the username is valid + if(!Validator::validateUsername($rpcRequest->getParameter('username'))) + { + return $rpcRequest->produceError(StandardError::INVALID_USERNAME, StandardError::INVALID_USERNAME->getMessage()); + } + + // Check if the username exists already + try + { + if (RegisteredPeerManager::usernameExists($rpcRequest->getParameter('username'))) + { + return $rpcRequest->produceError(StandardError::USERNAME_ALREADY_EXISTS, StandardError::USERNAME_ALREADY_EXISTS->getMessage()); + } + } + catch (DatabaseOperationException $e) + { + throw new StandardException("There was an unexpected error while trying to check the username existence", StandardError::INTERNAL_SERVER_ERROR, $e); + } + + // Check if the request has a Session UUID + if($request->getSessionUuid() === null) + { + return $rpcRequest->produceError(StandardError::SESSION_REQUIRED); + } + + try + { + // Get the session and check if it's already authenticated + $session = SessionManager::getSession($request->getSessionUuid()); + if($session->getAuthenticatedPeerUuid() !== null) + { + return $rpcRequest->produceError(StandardError::ALREADY_AUTHENTICATED); + } + + // Create the peer & set the current's session authenticated peer as the newly created peer + SessionManager::updateAuthenticatedPeer($session->getUuid(), RegisteredPeerManager::createPeer($rpcRequest->getParameter('username'))); + } + catch(DatabaseOperationException $e) + { + throw new StandardException("There was an unexpected error while trying to register", StandardError::INTERNAL_SERVER_ERROR, $e); + } + + // Return true to indicate the operation was a success + return $rpcRequest->produceResponse(true); + } +} \ No newline at end of file From 9a9cb3957bb1e238bb72258131d511fc7c6433d4 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 30 Oct 2024 15:29:21 -0400 Subject: [PATCH 033/420] Add serialization methods to PeerFlags enum --- src/Socialbox/Enums/Flags/PeerFlags.php | 29 +++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/Socialbox/Enums/Flags/PeerFlags.php b/src/Socialbox/Enums/Flags/PeerFlags.php index ce7b085..86b69bb 100644 --- a/src/Socialbox/Enums/Flags/PeerFlags.php +++ b/src/Socialbox/Enums/Flags/PeerFlags.php @@ -2,6 +2,8 @@ namespace Socialbox\Enums\Flags; +use Socialbox\Classes\Logger; + enum PeerFlags : string { // Administrative Flags @@ -20,6 +22,33 @@ enum PeerFlags : string case VER_PHONE_CALL = 'VER_PHONE_CALL'; case VER_SOLVE_IMAGE_CAPTCHA = 'VER_SOLVE_IMAGE_CAPTCHA'; + /** + * Converts an array of PeerFlags enums to a string representation + * + * @param PeerFlags[] $flags Array of PeerFlags enums + * @return string Comma-separated string of flag values + */ + public static function toString(array $flags): string + { + return implode(',', array_map(fn(PeerFlags $flag) => $flag->value, $flags)); + } + + /** + * Converts a string representation back to an array of PeerFlags enums + * + * @param string $flagString Comma-separated string of flag values + * @return PeerFlags[] Array of PeerFlags enums + */ + public static function fromString(string $flagString): array + { + if (empty($flagString)) + { + return []; + } + + return array_map(fn(string $value) => PeerFlags::from(trim($value)), explode(',', $flagString)); + } + /** * Returns whether the flag is public. Public flags can be seen by other peers. * From a9a13f186c742e56865b2823e586a49354cf1dea Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 30 Oct 2024 15:29:31 -0400 Subject: [PATCH 034/420] Add GetMe standard method implementation --- .../Classes/StandardMethods/GetMe.php | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/Socialbox/Classes/StandardMethods/GetMe.php diff --git a/src/Socialbox/Classes/StandardMethods/GetMe.php b/src/Socialbox/Classes/StandardMethods/GetMe.php new file mode 100644 index 0000000..9621320 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/GetMe.php @@ -0,0 +1,47 @@ +getSessionUuid() === null) + { + return $rpcRequest->produceError(StandardError::SESSION_REQUIRED); + } + + try + { + // Get the session and check if it's already authenticated + $session = SessionManager::getSession($request->getSessionUuid()); + if($session->getAuthenticatedPeerUuid() === null) + { + return $rpcRequest->produceError(StandardError::AUTHENTICATION_REQUIRED); + } + + // Get the peer and return it + return $rpcRequest->produceResponse(RegisteredPeerManager::getPeer($session->getAuthenticatedPeerUuid())->toSelfUser()); + } + catch(DatabaseOperationException $e) + { + throw new StandardException("There was an unexpected error while trying to register", StandardError::INTERNAL_SERVER_ERROR, $e); + } + } +} \ No newline at end of file From cd7be1c3b2a08c3c8ba69adc0b10c99fa7439292 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 30 Oct 2024 18:33:38 -0400 Subject: [PATCH 035/420] Rename and add session update methods --- .../Classes/StandardMethods/GetMe.php | 4 +-- .../Classes/StandardMethods/Register.php | 4 +-- src/Socialbox/Managers/SessionManager.php | 21 +++++++++++++-- .../Objects/Database/SessionRecord.php | 23 ++++++++++++---- src/Socialbox/Objects/Standard/SelfUser.php | 27 ++++++++++++------- 5 files changed, 58 insertions(+), 21 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/GetMe.php b/src/Socialbox/Classes/StandardMethods/GetMe.php index 9621320..fbd9826 100644 --- a/src/Socialbox/Classes/StandardMethods/GetMe.php +++ b/src/Socialbox/Classes/StandardMethods/GetMe.php @@ -31,13 +31,13 @@ class GetMe extends Method { // Get the session and check if it's already authenticated $session = SessionManager::getSession($request->getSessionUuid()); - if($session->getAuthenticatedPeerUuid() === null) + if($session->getPeerUuid() === null) { return $rpcRequest->produceError(StandardError::AUTHENTICATION_REQUIRED); } // Get the peer and return it - return $rpcRequest->produceResponse(RegisteredPeerManager::getPeer($session->getAuthenticatedPeerUuid())->toSelfUser()); + return $rpcRequest->produceResponse(RegisteredPeerManager::getPeer($session->getPeerUuid())->toSelfUser()); } catch(DatabaseOperationException $e) { diff --git a/src/Socialbox/Classes/StandardMethods/Register.php b/src/Socialbox/Classes/StandardMethods/Register.php index 40c1904..6bedc2d 100644 --- a/src/Socialbox/Classes/StandardMethods/Register.php +++ b/src/Socialbox/Classes/StandardMethods/Register.php @@ -61,13 +61,13 @@ class Register extends Method { // Get the session and check if it's already authenticated $session = SessionManager::getSession($request->getSessionUuid()); - if($session->getAuthenticatedPeerUuid() !== null) + if($session->getPeerUuid() !== null) { return $rpcRequest->produceError(StandardError::ALREADY_AUTHENTICATED); } // Create the peer & set the current's session authenticated peer as the newly created peer - SessionManager::updateAuthenticatedPeer($session->getUuid(), RegisteredPeerManager::createPeer($rpcRequest->getParameter('username'))); + SessionManager::updatePeer($session->getUuid(), RegisteredPeerManager::createPeer($rpcRequest->getParameter('username'))); } catch(DatabaseOperationException $e) { diff --git a/src/Socialbox/Managers/SessionManager.php b/src/Socialbox/Managers/SessionManager.php index 811e855..bb3eb6a 100644 --- a/src/Socialbox/Managers/SessionManager.php +++ b/src/Socialbox/Managers/SessionManager.php @@ -138,7 +138,7 @@ * @return void * @throws DatabaseOperationException */ - public static function updateAuthenticatedPeer(string $uuid, RegisteredPeerRecord|string $registeredPeerUuid): void + public static function updatePeer(string $uuid, RegisteredPeerRecord|string $registeredPeerUuid): void { if($registeredPeerUuid instanceof RegisteredPeerRecord) { @@ -149,7 +149,7 @@ try { - $statement = Database::getConnection()->prepare("UPDATE sessions SET authenticated_peer_uuid=? WHERE uuid=?"); + $statement = Database::getConnection()->prepare("UPDATE sessions SET peer_uuid=? WHERE uuid=?"); $statement->bindParam(1, $registeredPeerUuid); $statement->bindParam(2, $uuid); $statement->execute(); @@ -160,6 +160,23 @@ } } + public static function updateAuthentication(string $uuid, bool $authenticated): void + { + Logger::getLogger()->verbose(sprintf("Marking session %s as authenticated: %s", $uuid, $authenticated ? 'true' : 'false')); + + try + { + $statement = Database::getConnection()->prepare("UPDATE sessions SET authenticated=? WHERE uuid=?"); + $statement->bindParam(1, $authenticated); + $statement->bindParam(2, $uuid); + $statement->execute(); + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to update authenticated peer', $e); + } + } + /** * Updates the last request timestamp for a given session by its UUID. * diff --git a/src/Socialbox/Objects/Database/SessionRecord.php b/src/Socialbox/Objects/Database/SessionRecord.php index ab149ef..fa0ce42 100644 --- a/src/Socialbox/Objects/Database/SessionRecord.php +++ b/src/Socialbox/Objects/Database/SessionRecord.php @@ -9,7 +9,8 @@ use Socialbox\Interfaces\SerializableInterface; class SessionRecord implements SerializableInterface { private string $uuid; - private ?string $authenticatedPeerUuid; + private ?string $peerUuid; + private bool $authenticated; private string $publicKey; private SessionState $state; private DateTime $created; @@ -18,7 +19,8 @@ class SessionRecord implements SerializableInterface public function __construct(array $data) { $this->uuid = $data['uuid']; - $this->authenticatedPeerUuid = $data['authenticated_peer_uuid'] ?? null; + $this->peerUuid = $data['peer_uuid'] ?? null; + $this->authenticated = $data['authenticated'] ?? false; $this->publicKey = $data['public_key']; $this->created = $data['created']; $this->lastRequest = $data['last_request']; @@ -38,9 +40,19 @@ class SessionRecord implements SerializableInterface return $this->uuid; } - public function getAuthenticatedPeerUuid(): ?string + public function getPeerUuid(): ?string { - return $this->authenticatedPeerUuid; + return $this->peerUuid; + } + + public function isAuthenticated(): bool + { + if($this->peerUuid === null) + { + return false; + } + + return $this->authenticated; } public function getPublicKey(): string @@ -72,7 +84,8 @@ class SessionRecord implements SerializableInterface { return [ 'uuid' => $this->uuid, - 'authenticated_peer_uuid' => $this->authenticatedPeerUuid, + 'peer_uuid' => $this->peerUuid, + 'authenticated' => $this->authenticated, 'public_key' => $this->publicKey, 'state' => $this->state->value, 'created' => $this->created, diff --git a/src/Socialbox/Objects/Standard/SelfUser.php b/src/Socialbox/Objects/Standard/SelfUser.php index 109c21e..443f8dc 100644 --- a/src/Socialbox/Objects/Standard/SelfUser.php +++ b/src/Socialbox/Objects/Standard/SelfUser.php @@ -10,6 +10,7 @@ use Socialbox\Objects\Database\RegisteredPeerRecord; class SelfUser implements SerializableInterface { private string $uuid; + private bool $enabled; private string $address; private string $username; private ?string $displayName; @@ -29,8 +30,9 @@ class SelfUser implements SerializableInterface if($data instanceof RegisteredPeerRecord) { $this->uuid = $data->getUuid(); + $this->enabled = $data->isEnabled(); $this->username = $data->getUsername(); - $this->address = + $this->address = $data->getAddress(); $this->displayName = $data->getDisplayName(); $this->flags = $data->getFlags(); $this->created = $data->getCreated()->getTimestamp(); @@ -39,7 +41,9 @@ class SelfUser implements SerializableInterface } $this->uuid = $data['uuid']; + $this->enabled = $data['enabled']; $this->username = $data['username']; + $this->address = $data['address']; $this->displayName = $data['display_name'] ?? null; if(is_string($data['flags'])) @@ -77,6 +81,11 @@ class SelfUser implements SerializableInterface return $this->uuid; } + public function isEnabled(): bool + { + return $this->enabled; + } + /** * * @return string The username of the user. @@ -86,6 +95,11 @@ class SelfUser implements SerializableInterface return $this->username; } + public function getAddress(): string + { + return $this->address; + } + /** * * @return string|null The display name. @@ -104,15 +118,6 @@ class SelfUser implements SerializableInterface return $this->flags; } - /** - * - * @return bool - */ - public function isEnabled(): bool - { - return $this->enabled; - } - /** * * @return int The timestamp when the object was created. @@ -143,7 +148,9 @@ class SelfUser implements SerializableInterface return [ 'uuid' => $this->uuid, + 'enabled' => $this->enabled, 'username' => $this->username, + 'address' => $this->address, 'display_name' => $this->displayName, 'flags' => $flags, 'created' => $this->created From beadf6d1817e7a086cfaa85d77dd72d6093a47b9 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 30 Oct 2024 21:36:20 -0400 Subject: [PATCH 036/420] Add Security Configuration Class and Integration --- src/Socialbox/Classes/Configuration.php | 54 ++++++++++++------ .../Configuration/SecurityConfiguration.php | 55 +++++++++++++++++++ 2 files changed, 92 insertions(+), 17 deletions(-) create mode 100644 src/Socialbox/Classes/Configuration/SecurityConfiguration.php diff --git a/src/Socialbox/Classes/Configuration.php b/src/Socialbox/Classes/Configuration.php index 4e409d9..d667097 100644 --- a/src/Socialbox/Classes/Configuration.php +++ b/src/Socialbox/Classes/Configuration.php @@ -8,12 +8,14 @@ use Socialbox\Classes\Configuration\DatabaseConfiguration; use Socialbox\Classes\Configuration\InstanceConfiguration; use Socialbox\Classes\Configuration\LoggingConfiguration; use Socialbox\Classes\Configuration\RegistrationConfiguration; +use Socialbox\Classes\Configuration\SecurityConfiguration; class Configuration { private static ?\ConfigLib\Configuration $configuration = null; - private static ?DatabaseConfiguration $databaseConfiguration = null; private static ?InstanceConfiguration $instanceConfiguration = null; + private static ?SecurityConfiguration $securityConfiguration = null; + private static ?DatabaseConfiguration $databaseConfiguration = null; private static ?LoggingConfiguration $loggingConfiguration = null; private static ?CacheConfiguration $cacheConfiguration = null; private static ?RegistrationConfiguration $registrationConfiguration = null; @@ -38,6 +40,7 @@ class Configuration // Security Configuration $config->setDefault('security.display_internal_exceptions', false); $config->setDefault('security.resolved_servers_ttl', 600); + $config->setDefault('security.captcha_ttl', 200); // Database configuration $config->setDefault('database.host', '127.0.0.1'); @@ -73,11 +76,13 @@ class Configuration $config->setDefault('registration.sms_verification_required', false); $config->setDefault('registration.phone_call_verification_required', false); $config->setDefault('registration.image_captcha_verification_required', true); + $config->save(); self::$configuration = $config; - self::$databaseConfiguration = new DatabaseConfiguration(self::$configuration->getConfiguration()['database']); self::$instanceConfiguration = new InstanceConfiguration(self::$configuration->getConfiguration()['instance']); + self::$securityConfiguration = new SecurityConfiguration(self::$configuration->getConfiguration()['security']); + self::$databaseConfiguration = new DatabaseConfiguration(self::$configuration->getConfiguration()['database']); self::$loggingConfiguration = new LoggingConfiguration(self::$configuration->getConfiguration()['logging']); self::$cacheConfiguration = new CacheConfiguration(self::$configuration->getConfiguration()['cache']); self::$registrationConfiguration = new RegistrationConfiguration(self::$configuration->getConfiguration()['registration']); @@ -109,21 +114,6 @@ class Configuration return self::$configuration; } - /** - * Retrieves the current database configuration. - * - * @return DatabaseConfiguration The configuration settings for the database. - */ - public static function getDatabaseConfiguration(): DatabaseConfiguration - { - if(self::$databaseConfiguration === null) - { - self::initializeConfiguration(); - } - - return self::$databaseConfiguration; - } - /** * Retrieves the current instance configuration. * @@ -139,6 +129,36 @@ class Configuration return self::$instanceConfiguration; } + /** + * Retrieves the current security configuration. + * + * @return SecurityConfiguration The current security configuration instance. + */ + public static function getSecurityConfiguration(): SecurityConfiguration + { + if(self::$securityConfiguration === null) + { + self::initializeConfiguration(); + } + + return self::$securityConfiguration; + } + + /** + * Retrieves the current database configuration. + * + * @return DatabaseConfiguration The configuration settings for the database. + */ + public static function getDatabaseConfiguration(): DatabaseConfiguration + { + if(self::$databaseConfiguration === null) + { + self::initializeConfiguration(); + } + + return self::$databaseConfiguration; + } + /** * Retrieves the current logging configuration. * diff --git a/src/Socialbox/Classes/Configuration/SecurityConfiguration.php b/src/Socialbox/Classes/Configuration/SecurityConfiguration.php new file mode 100644 index 0000000..70aa930 --- /dev/null +++ b/src/Socialbox/Classes/Configuration/SecurityConfiguration.php @@ -0,0 +1,55 @@ +displayInternalErrors = $data['display_internal_errors']; + $this->resolvedServersTtl = $data['resolved_servers_ttl']; + $this->captchaTtl = $data['captcha_ttl']; + } + + /** + * Determines if the display of internal errors is enabled. + * + * @return bool True if the display of internal errors is enabled, false otherwise. + */ + public function isDisplayInternalErrors(): bool + { + return $this->displayInternalErrors; + } + + /** + * Retrieves the time-to-live (TTL) value for resolved servers. + * + * @return int The TTL value for resolved servers. + */ + public function getResolvedServersTtl(): int + { + return $this->resolvedServersTtl; + } + + /** + * Retrieves the time-to-live (TTL) value for captchas. + * + * @return int The TTL value for captchas. + */ + public function getCaptchaTtl(): int + { + return $this->captchaTtl; + } + +} \ No newline at end of file From b003845f39f70b99316f65921ae4f515765e0d1f Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 31 Oct 2024 12:18:44 -0400 Subject: [PATCH 037/420] Add `useDatabase` parameter to resolveDomain method --- src/Socialbox/Classes/ServerResolver.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Socialbox/Classes/ServerResolver.php b/src/Socialbox/Classes/ServerResolver.php index 29d25a1..3e3e8d6 100644 --- a/src/Socialbox/Classes/ServerResolver.php +++ b/src/Socialbox/Classes/ServerResolver.php @@ -19,19 +19,18 @@ class ServerResolver * @throws ResolutionException If the DNS TXT records cannot be resolved or if required information is missing. * @throws DatabaseOperationException */ - public static function resolveDomain(string $domain): ResolvedServer + public static function resolveDomain(string $domain, bool $useDatabase=true): ResolvedServer { // First query the database to check if the domain is already resolved - if(ResolvedServersManager::resolvedServerExists($domain)) + if($useDatabase) { - // If the resolved server was updated in the last 30 minutes, return it - if(ResolvedServersManager::getResolvedServerUpdated($domain) > (time() - 1800)) + $resolvedServer = ResolvedServersManager::getResolvedServer($domain); + if($resolvedServer !== null) { - return ResolvedServersManager::getResolvedServer($domain)->toResolvedServer(); + return $resolvedServer->toResolvedServer(); } } - $txtRecords = self::dnsGetTxtRecords($domain); if ($txtRecords === false) { From ff0fde7156b74859f6ecb5532ed74d4ba3991f4d Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 31 Oct 2024 12:19:13 -0400 Subject: [PATCH 038/420] Delete unused Socialbox.php file --- src/Socialbox/Socialbox/Socialbox.php | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 src/Socialbox/Socialbox/Socialbox.php diff --git a/src/Socialbox/Socialbox/Socialbox.php b/src/Socialbox/Socialbox/Socialbox.php deleted file mode 100644 index f0ca775..0000000 --- a/src/Socialbox/Socialbox/Socialbox.php +++ /dev/null @@ -1,8 +0,0 @@ - Date: Thu, 31 Oct 2024 12:20:16 -0400 Subject: [PATCH 039/420] Remove unused Log import and add docblock for handleRpc method --- src/Socialbox/Socialbox.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index be5b683..713cb7d 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -3,7 +3,6 @@ namespace Socialbox; use Exception; - use LogLib\Log; use Socialbox\Classes\Configuration; use Socialbox\Classes\Logger; use Socialbox\Classes\RpcHandler; @@ -15,6 +14,12 @@ class Socialbox { + /** + * Handles the RPC (Remote Procedure Call) requests by parsing the client request, + * executing the appropriate methods, and returning the responses. + * + * @return void + */ public static function handleRpc(): void { try From 84db54feeecdb74e1cff5dd8a2b8360b74c78221 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 31 Oct 2024 13:20:14 -0400 Subject: [PATCH 040/420] Minor update --- src/Socialbox/Classes/ServerResolver.php | 154 +++++++++++------------ 1 file changed, 77 insertions(+), 77 deletions(-) diff --git a/src/Socialbox/Classes/ServerResolver.php b/src/Socialbox/Classes/ServerResolver.php index 3e3e8d6..9524b19 100644 --- a/src/Socialbox/Classes/ServerResolver.php +++ b/src/Socialbox/Classes/ServerResolver.php @@ -1,96 +1,96 @@ toResolvedServer(); + $resolvedServer = ResolvedServersManager::getResolvedServer($domain); + if($resolvedServer !== null) + { + return $resolvedServer->toResolvedServer(); + } + } + + $txtRecords = self::dnsGetTxtRecords($domain); + if ($txtRecords === false) + { + throw new ResolutionException(sprintf("Failed to resolve DNS TXT records for %s", $domain)); + } + + $fullRecord = self::concatenateTxtRecords($txtRecords); + + if (preg_match(self::PATTERN, $fullRecord, $matches)) + { + $endpoint = trim($matches[1]); + $publicKey = trim(str_replace(' ', '', $matches[2])); + + if (empty($endpoint)) + { + throw new ResolutionException(sprintf("Failed to resolve RPC endpoint for %s", $domain)); + } + + if (empty($publicKey)) + { + throw new ResolutionException(sprintf("Failed to resolve public key for %s", $domain)); + } + + return new ResolvedServer($endpoint, $publicKey); + } + else + { + throw new ResolutionException(sprintf("Failed to find valid SocialBox record for %s", $domain)); } } - $txtRecords = self::dnsGetTxtRecords($domain); - if ($txtRecords === false) + /** + * Retrieves the TXT records for a given domain using the dns_get_record function. + * + * @param string $domain The domain name to fetch TXT records for. + * @return array|false An array of DNS TXT records on success, or false on failure. + */ + private static function dnsGetTxtRecords(string $domain) { - throw new ResolutionException(sprintf("Failed to resolve DNS TXT records for %s", $domain)); + return dns_get_record($domain, DNS_TXT); } - $fullRecord = self::concatenateTxtRecords($txtRecords); - - if (preg_match(self::PATTERN, $fullRecord, $matches)) + /** + * Concatenates an array of TXT records into a single string. + * + * @param array $txtRecords An array of TXT records, where each record is expected to have a 'txt' key. + * @return string A concatenated string of all TXT records. + */ + private static function concatenateTxtRecords(array $txtRecords): string { - $endpoint = trim($matches[1]); - $publicKey = trim(str_replace(' ', '', $matches[2])); + $fullRecordBuilder = ''; - if (empty($endpoint)) + foreach ($txtRecords as $txt) { - throw new ResolutionException(sprintf("Failed to resolve RPC endpoint for %s", $domain)); + if (isset($txt['txt'])) + { + $fullRecordBuilder .= trim($txt['txt'], '" '); + } } - if (empty($publicKey)) - { - throw new ResolutionException(sprintf("Failed to resolve public key for %s", $domain)); - } - - return new ResolvedServer($endpoint, $publicKey); + return $fullRecordBuilder; } - else - { - throw new ResolutionException(sprintf("Failed to find valid SocialBox record for %s", $domain)); - } - } - - /** - * Retrieves the TXT records for a given domain using the dns_get_record function. - * - * @param string $domain The domain name to fetch TXT records for. - * @return array|false An array of DNS TXT records on success, or false on failure. - */ - private static function dnsGetTxtRecords(string $domain) - { - return dns_get_record($domain, DNS_TXT); - } - - /** - * Concatenates an array of TXT records into a single string. - * - * @param array $txtRecords An array of TXT records, where each record is expected to have a 'txt' key. - * @return string A concatenated string of all TXT records. - */ - private static function concatenateTxtRecords(array $txtRecords): string - { - $fullRecordBuilder = ''; - - foreach ($txtRecords as $txt) - { - if (isset($txt['txt'])) - { - $fullRecordBuilder .= trim($txt['txt'], '" '); - } - } - - return $fullRecordBuilder; - } -} \ No newline at end of file + } \ No newline at end of file From 47b8641c445e87352c9d91fef4814196159a83ef Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 31 Oct 2024 13:45:52 -0400 Subject: [PATCH 041/420] Clarify error message parameter in RpcError constructor. --- src/Socialbox/Objects/RpcError.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Socialbox/Objects/RpcError.php b/src/Socialbox/Objects/RpcError.php index 99694d4..9b6453f 100644 --- a/src/Socialbox/Objects/RpcError.php +++ b/src/Socialbox/Objects/RpcError.php @@ -16,7 +16,7 @@ class RpcError implements SerializableInterface * * @param string $id The ID of the RPC request * @param StandardError $code The error code - * @param string $error The error message + * @param string|null $error The error message */ public function __construct(string $id, StandardError $code, ?string $error) { From ce963d2ff6efacfeb317d60182ebd426213e33fb Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 31 Oct 2024 13:46:01 -0400 Subject: [PATCH 042/420] Updated project files --- .idea/socialbox-php.iml | 2 -- composer.json | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.idea/socialbox-php.iml b/.idea/socialbox-php.iml index 47ac8d8..9ac0b9d 100644 --- a/.idea/socialbox-php.iml +++ b/.idea/socialbox-php.iml @@ -2,8 +2,6 @@ - - diff --git a/composer.json b/composer.json index c76565e..05de501 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ "ext-pdo": "*", "ext-openssl": "*", "ext-redis": "*", - "ext-memcached": "*" + "ext-memcached": "*", + "ext-curl": "*" } } \ No newline at end of file From 706666af09512b7b87c66d56a390bc058a98759c Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 31 Oct 2024 14:54:34 -0400 Subject: [PATCH 043/420] Refactor RpcClient and add session management. --- src/Socialbox/Classes/RpcClient.php | 262 ++++++++++++++++++++++++---- 1 file changed, 229 insertions(+), 33 deletions(-) diff --git a/src/Socialbox/Classes/RpcClient.php b/src/Socialbox/Classes/RpcClient.php index 31dbcc0..0b8963e 100644 --- a/src/Socialbox/Classes/RpcClient.php +++ b/src/Socialbox/Classes/RpcClient.php @@ -2,23 +2,32 @@ namespace Socialbox\Classes; - use Socialbox\Classes\ServerResolver; use Socialbox\Enums\StandardHeaders; + use Socialbox\Exceptions\CryptographyException; + use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\ResolutionException; - use Socialclient\Exceptions\RpcRequestException; + use Socialbox\Exceptions\RpcException; + use Socialbox\Objects\RpcError; + use Socialbox\Objects\RpcRequest; + use Socialbox\Objects\RpcResponse; class RpcClient { private const string CLIENT_NAME = 'Socialbox PHP'; private const string CLIENT_VERSION = '1.0'; - private const string CONTENT_TYPE = 'application/json'; + private const string CONTENT_TYPE = 'application/json; charset=utf-8'; private string $domain; private string $endpoint; private string $serverPublicKey; - + private ?string $sessionUuid; + private ?string $privateKey; /** + * Constructor for initializing the server connection with a given domain. + * + * @param string $domain The domain used to resolve the server's endpoint and public key. + * @throws DatabaseOperationException * @throws ResolutionException */ public function __construct(string $domain) @@ -28,61 +37,248 @@ $this->domain = $domain; $this->endpoint = $resolved->getEndpoint(); $this->serverPublicKey = $resolved->getPublicKey(); - $this->clientPrivateKey = null; + $this->sessionUuid = null; + $this->privateKey = null; } + /** + * Retrieves the domain. + * + * @return string The domain. + */ public function getDomain(): string { return $this->domain; } + /** + * Retrieves the endpoint URL. + * + * @return string The endpoint URL. + */ public function getEndpoint(): string { return $this->endpoint; } + /** + * Retrieves the server's public key. + * + * @return string The server's public key. + */ public function getServerPublicKey(): string { return $this->serverPublicKey; } - public function sendRequest(array $data) + /** + * Retrieves the session UUID. + * + * @return string|null The session UUID or null if not set. + */ + public function getSessionUuid(): ?string { - $ch = curl_init($this->endpoint); + return $this->sessionUuid; + } - 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) + /** + * Sets the session UUID. + * + * @param string|null $sessionUuid The session UUID to set. Can be null. + * @return void + */ + public function setSessionUuid(?string $sessionUuid): void + { + $this->sessionUuid = $sessionUuid; + } + + /** + * Retrieves the private key. + * + * @return string|null The private key if available, or null if not set. + */ + public function getPrivateKey(): ?string + { + return $this->privateKey; + } + + /** + * Sets the private key. + * + * @param string|null $privateKey The private key to be set. Can be null. + * @return void + */ + public function setPrivateKey(?string $privateKey): void + { + $this->privateKey = $privateKey; + } + + /** + * Sends an RPC request to the specified endpoint. + * + * @param RpcRequest $request The RPC request to be sent. + * @return RpcResponse|RpcError|null The response from the RPC server, an error object, or null if no content. + * @throws CryptographyException If an error occurs during the signing of the content. + * @throws RpcException If an error occurs while sending the request or processing the response. + */ + public function sendRequest(RpcRequest $request): RpcResponse|RpcError|null + { + $curl = curl_init($this->endpoint); + $content = Utilities::jsonEncode($request->toArray()); + curl_setopt_array($curl, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_HTTPHEADER => $this->getHeaders($content), + CURLOPT_POSTFIELDS => $content, ]); - curl_setopt($ch, CURLOPT_HEADER, true); - $response = curl_exec($ch); + $response = curl_exec($curl); + $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); - if (curl_errno($ch)) + if(curl_errno($curl)) { - $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); + throw new RpcException(sprintf('Failed to send request: %s', curl_error($curl))); } - $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($curl); - // 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); + // Return null if the response is empty + if($httpCode === 204) + { + return null; + } - curl_close($ch); + if(!$this->isSuccessful($httpCode)) + { + if(!empty($response)) + { + throw new RpcException($response); + } + + throw new RpcException(sprintf('Error occurred while processing request: %d', $httpCode)); + } + + if(empty($response)) + { + throw new RpcException('Response was empty but status code was successful'); + } + + return RpcResponse::fromArray(Utilities::jsonDecode($response)); + } + + /** + * Sends multiple requests to the designated endpoint and returns their responses. + * + * @param array $requests An array of request objects, each implementing the method toArray(). + * @return RpcResponse[]|RpcError[] An array of response objects, each implementing the method toArray(). + * @throws CryptographyException If an error occurs during the signing of the content. + * @throws RpcException If any errors occur during the request process or in case of unsuccessful HTTP codes. + */ + public function sendRequests(array $requests): array + { + $curl = curl_init($this->endpoint); + $contents = null; + + foreach($requests as $request) + { + $contents[] = $request->toArray(); + } + + $content = Utilities::jsonEncode($contents); + + curl_setopt_array($curl, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_HTTPHEADER => $this->getHeaders($content), + CURLOPT_POSTFIELDS => $content, + ]); + + $response = curl_exec($curl); + $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); + + if(curl_errno($curl)) + { + throw new RpcException(sprintf('Failed to send request: %s', curl_error($curl))); + } + + curl_close($curl); + + // Return null if the response is empty + if($httpCode === 204) + { + return []; + } + + if(!$this->isSuccessful($httpCode)) + { + if(!empty($response)) + { + throw new RpcException($response); + } + + throw new RpcException(sprintf('Error occurred while processing request: %d', $httpCode)); + } + + if(empty($response)) + { + throw new RpcException('Response was empty but status code was successful'); + } + + $results = Utilities::jsonDecode($response); + $responses = []; + + foreach($results as $result) + { + $responses[] = RpcResponse::fromArray($result); + } + + return $responses; + } + + /** + * Determines if the provided HTTP status code indicates a successful response. + * + * @param int $code The HTTP status code to evaluate. + * @return bool True if the status code represents success (2xx), false otherwise. + */ + private function isSuccessful(int $code): bool + { + return $code >= 200 && $code < 300; + } + + /** + * Generates an array of headers based on standard headers and instance-specific properties. + * + * @param string $content The content to be signed if a private key is available. + * @return array An array of headers to be included in an HTTP request. + * @throws CryptographyException If an error occurs during the signing of the content. + */ + private function getHeaders(string $content): array + { + $headers = [ + sprintf('%s: %s', StandardHeaders::CLIENT_NAME->value, self::CLIENT_NAME), + sprintf('%s: %s', StandardHeaders::CLIENT_VERSION->value, self::CLIENT_VERSION), + sprintf('%s: %s', StandardHeaders::CONTENT_TYPE->value, self::CONTENT_TYPE), + ]; + + if($this->sessionUuid !== null) + { + $headers[] = sprintf('%s: %s', StandardHeaders::SESSION_UUID->value, $this->sessionUuid); + } + + if($this->privateKey !== null) + { + try + { + $headers[] = sprintf('%s: %s', StandardHeaders::SIGNATURE->value, Cryptography::signContent($content, $this->privateKey, true)); + } + catch (CryptographyException $e) + { + Logger::getLogger()->error('Failed to sign content: ' . $e->getMessage()); + throw $e; + } + } + + return $headers; } } \ No newline at end of file From 38b96db83a370bfcc2fe9fed42f0506d4524eac5 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 31 Oct 2024 15:49:42 -0400 Subject: [PATCH 044/420] Update captcha handling and introduce new captcha mechanism --- .idea/socialbox-php.iml | 2 + .../Configuration/SecurityConfiguration.php | 8 +- .../Classes/StandardMethods/GetCaptcha.php | 83 +++++++++++++++++++ src/Socialbox/Enums/StandardError.php | 7 ++ src/Socialbox/Enums/StandardMethods.php | 3 + src/Socialbox/Managers/CaptchaManager.php | 16 +++- .../Objects/Database/CaptchaRecord.php | 11 +++ .../Objects/Standard/ImageCaptcha.php | 53 ++++++++++++ src/Socialbox/Socialbox.php | 1 + src/Socialbox/Socialclient.php | 11 +++ 10 files changed, 189 insertions(+), 6 deletions(-) create mode 100644 src/Socialbox/Classes/StandardMethods/GetCaptcha.php create mode 100644 src/Socialbox/Objects/Standard/ImageCaptcha.php create mode 100644 src/Socialbox/Socialclient.php diff --git a/.idea/socialbox-php.iml b/.idea/socialbox-php.iml index 9ac0b9d..47ac8d8 100644 --- a/.idea/socialbox-php.iml +++ b/.idea/socialbox-php.iml @@ -2,6 +2,8 @@ + + diff --git a/src/Socialbox/Classes/Configuration/SecurityConfiguration.php b/src/Socialbox/Classes/Configuration/SecurityConfiguration.php index 70aa930..805437c 100644 --- a/src/Socialbox/Classes/Configuration/SecurityConfiguration.php +++ b/src/Socialbox/Classes/Configuration/SecurityConfiguration.php @@ -4,7 +4,7 @@ namespace Socialbox\Classes\Configuration; class SecurityConfiguration { - private bool $displayInternalErrors; + private bool $displayInternalExceptions; private int $resolvedServersTtl; private int $captchaTtl; @@ -17,7 +17,7 @@ class SecurityConfiguration */ public function __construct(array $data) { - $this->displayInternalErrors = $data['display_internal_errors']; + $this->displayInternalExceptions = $data['display_internal_exceptions']; $this->resolvedServersTtl = $data['resolved_servers_ttl']; $this->captchaTtl = $data['captcha_ttl']; } @@ -27,9 +27,9 @@ class SecurityConfiguration * * @return bool True if the display of internal errors is enabled, false otherwise. */ - public function isDisplayInternalErrors(): bool + public function isDisplayInternalExceptions(): bool { - return $this->displayInternalErrors; + return $this->displayInternalExceptions; } /** diff --git a/src/Socialbox/Classes/StandardMethods/GetCaptcha.php b/src/Socialbox/Classes/StandardMethods/GetCaptcha.php new file mode 100644 index 0000000..a7bbc78 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/GetCaptcha.php @@ -0,0 +1,83 @@ +getSessionUuid() === null) + { + return $rpcRequest->produceError(StandardError::SESSION_REQUIRED); + } + + // Get the session and check if it's already authenticated + try + { + $session = SessionManager::getSession($request->getSessionUuid()); + } + catch(DatabaseOperationException $e) + { + throw new StandardException("There was an unexpected error while trying to get the session", StandardError::INTERNAL_SERVER_ERROR, $e); + } + + // Check for session conditions + if($session->getPeerUuid() === null) + { + return $rpcRequest->produceError(StandardError::AUTHENTICATION_REQUIRED); + } + + // Get the peer + try + { + $peer = RegisteredPeerManager::getPeer($session->getPeerUuid()); + } + catch(DatabaseOperationException $e) + { + throw new StandardException("There was unexpected error while trying to get the peer", StandardError::INTERNAL_SERVER_ERROR, $e); + } + + // Check if the VER_SOLVE_IMAGE_CAPTCHA flag exists. + if(!$peer->flagExists(PeerFlags::VER_SOLVE_IMAGE_CAPTCHA)) + { + return $rpcRequest->produceError(StandardError::CAPTCHA_NOT_AVAILABLE, 'You are not required to complete a captcha at this time'); + } + + try + { + Logger::getLogger()->debug('Creating a new captcha for peer ' . $peer->getUuid()); + $answer = CaptchaManager::createCaptcha($peer); + $captchaRecord = CaptchaManager::getCaptcha($peer); + } + catch (DatabaseOperationException $e) + { + throw new StandardException("There was an unexpected error while trying create the captcha", StandardError::INTERNAL_SERVER_ERROR, $e); + } + + // Build the captcha + Logger::getLogger()->debug('Building captcha for peer ' . $peer->getUuid()); + return $rpcRequest->produceResponse(new ImageCaptcha([ + 'expires' => $captchaRecord->getExpires()->getTimestamp(), + 'image' => (new CaptchaBuilder($answer))->build()->inline()] // Returns HTML base64 encoded image of the captcha + )); + } +} \ No newline at end of file diff --git a/src/Socialbox/Enums/StandardError.php b/src/Socialbox/Enums/StandardError.php index 22f3599..9ab424f 100644 --- a/src/Socialbox/Enums/StandardError.php +++ b/src/Socialbox/Enums/StandardError.php @@ -23,6 +23,9 @@ enum StandardError : int case SESSION_NOT_FOUND = -3004; case SESSION_REQUIRED = -3005; case REGISTRATION_DISABLED = -3006; + case CAPTCHA_NOT_AVAILABLE = -3007; + case INCORRECT_CAPTCHA_ANSWER = -3008; + case CAPTCHA_EXPIRED = -3009; // General Error Messages case PEER_NOT_FOUND = -4000; @@ -52,6 +55,10 @@ enum StandardError : int self::AUTHENTICATION_REQUIRED => 'Authentication is required to preform this action', self::SESSION_NOT_FOUND => 'The requested session UUID was not found', self::SESSION_REQUIRED => 'A session is required to preform this action', + self::REGISTRATION_DISABLED => 'Registration is disabled on the server', + self::CAPTCHA_NOT_AVAILABLE => 'Captcha is not available', + self::INCORRECT_CAPTCHA_ANSWER => 'The Captcha answer is incorrect', + self::CAPTCHA_EXPIRED => 'The captcha has expired and a new captcha needs to be requested', self::PEER_NOT_FOUND => 'The requested peer was not found', self::INVALID_USERNAME => 'The given username is invalid, it must be Alphanumeric with a minimum of 3 character but no greater than 255 characters', diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index 3d683e1..e412502 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -3,6 +3,7 @@ namespace Socialbox\Enums; use Socialbox\Classes\StandardMethods\CreateSession; +use Socialbox\Classes\StandardMethods\GetCaptcha; use Socialbox\Classes\StandardMethods\GetMe; use Socialbox\Classes\StandardMethods\Ping; use Socialbox\Classes\StandardMethods\Register; @@ -17,6 +18,7 @@ enum StandardMethods : string case CREATE_SESSION = 'createSession'; case REGISTER = 'register'; case GET_ME = 'getMe'; + case GET_CAPTCHA = 'getCaptcha'; /** * @param ClientRequest $request @@ -32,6 +34,7 @@ enum StandardMethods : string self::CREATE_SESSION => CreateSession::execute($request, $rpcRequest), self::REGISTER => Register::execute($request, $rpcRequest), self::GET_ME => GetMe::execute($request, $rpcRequest), + self::GET_CAPTCHA => GetCaptcha::execute($request, $rpcRequest), }; } } \ No newline at end of file diff --git a/src/Socialbox/Managers/CaptchaManager.php b/src/Socialbox/Managers/CaptchaManager.php index fc75d1c..ddf62ca 100644 --- a/src/Socialbox/Managers/CaptchaManager.php +++ b/src/Socialbox/Managers/CaptchaManager.php @@ -6,6 +6,7 @@ use DateTime; use DateTimeInterface; use PDOException; use Socialbox\Classes\Database; +use Socialbox\Classes\Logger; use Socialbox\Classes\Utilities; use Socialbox\Enums\Status\CaptchaStatus; use Socialbox\Exceptions\DatabaseOperationException; @@ -21,7 +22,7 @@ class CaptchaManager * @return string The answer to the captcha. * @throws DatabaseOperationException If the operation fails. */ - private static function createCaptcha(string|RegisteredPeerRecord $peer_uuid): string + public static function createCaptcha(string|RegisteredPeerRecord $peer_uuid): string { // If the peer_uuid is a RegisteredPeerRecord, get the UUID if($peer_uuid instanceof RegisteredPeerRecord) @@ -33,6 +34,7 @@ class CaptchaManager if(!self::captchaExists($peer_uuid)) { + Logger::getLogger()->debug('Creating a new captcha record for peer ' . $peer_uuid); $statement = Database::getConnection()->prepare("INSERT INTO captcha_images (peer_uuid, answer) VALUES (?, ?)"); $statement->bindParam(1, $peer_uuid); $statement->bindParam(2, $answer); @@ -49,6 +51,7 @@ class CaptchaManager return $answer; } + Logger::getLogger()->debug('Updating an existing captcha record for peer ' . $peer_uuid); $statement = Database::getConnection()->prepare("UPDATE captcha_images SET answer=?, status='UNSOLVED', created=NOW() WHERE peer_uuid=?"); $statement->bindParam(1, $answer); $statement->bindParam(2, $peer_uuid); @@ -83,6 +86,7 @@ class CaptchaManager // Return false if the captcha does not exist if(!self::captchaExists($peer_uuid)) { + Logger::getLogger()->warning(sprintf("The requested captcha does not exist for the peer %s", $peer_uuid)); return false; } @@ -91,21 +95,25 @@ class CaptchaManager // Return false if the captcha has already been solved if($captcha->getStatus() === CaptchaStatus::SOLVED) { + Logger::getLogger()->warning(sprintf("The requested captcha has already been solved for the peer %s", $peer_uuid)); return false; } // Return false if the captcha is older than 5 minutes - if ($captcha->getCreated() instanceof DateTimeInterface && $captcha->getCreated()->diff(new DateTime())->i > 5) + if ($captcha->isExpired()) { + Logger::getLogger()->warning(sprintf("The requested captcha is older than 5 minutes for the peer %s", $peer_uuid)); return false; } // Verify the answer if($captcha->getAnswer() !== $answer) { + Logger::getLogger()->warning(sprintf("The answer to the requested captcha is incorrect for the peer %s", $peer_uuid)); return false; } + Logger::getLogger()->debug(sprintf("The answer to the requested captcha is correct for the peer %s", $peer_uuid)); $statement = Database::getConnection()->prepare("UPDATE captcha_images SET status='SOLVED', answered=NOW() WHERE peer_uuid=?"); $statement->bindParam(1, $peer_uuid); @@ -136,6 +144,8 @@ class CaptchaManager $peer_uuid = $peer_uuid->getUuid(); } + Logger::getLogger()->debug('Getting the captcha record for peer ' . $peer_uuid); + try { $statement = Database::getConnection()->prepare("SELECT * FROM captcha_images WHERE peer_uuid=? LIMIT 1"); @@ -171,6 +181,8 @@ class CaptchaManager $peer_uuid = $peer_uuid->getUuid(); } + Logger::getLogger()->debug('Checking if a captcha exists for peer ' . $peer_uuid); + try { $statement = Database::getConnection()->prepare("SELECT COUNT(*) FROM captcha_images WHERE peer_uuid=?"); diff --git a/src/Socialbox/Objects/Database/CaptchaRecord.php b/src/Socialbox/Objects/Database/CaptchaRecord.php index 4427310..6526e26 100644 --- a/src/Socialbox/Objects/Database/CaptchaRecord.php +++ b/src/Socialbox/Objects/Database/CaptchaRecord.php @@ -3,6 +3,7 @@ namespace Socialbox\Objects\Database; use DateTime; +use Socialbox\Classes\Configuration; use Socialbox\Enums\Status\CaptchaStatus; use Socialbox\Interfaces\SerializableInterface; @@ -55,6 +56,16 @@ class CaptchaRecord implements SerializableInterface return $this->created; } + public function getExpires(): DateTime + { + return $this->created->modify(sprintf("+%s seconds", Configuration::getSecurityConfiguration()->getCaptchaTtl())); + } + + public function isExpired(): bool + { + return $this->getExpires() < new DateTime(); + } + /** * @inheritDoc */ diff --git a/src/Socialbox/Objects/Standard/ImageCaptcha.php b/src/Socialbox/Objects/Standard/ImageCaptcha.php new file mode 100644 index 0000000..ed66b24 --- /dev/null +++ b/src/Socialbox/Objects/Standard/ImageCaptcha.php @@ -0,0 +1,53 @@ +expires = $data['expires']; + $this->image = $data['image']; + } + + /** + * @return int + */ + public function getExpires(): int + { + return $this->expires; + } + + /** + * @return string + */ + public function getImage(): string + { + return $this->image; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): object + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'expires' => $this->expires, + 'image' => $this->image + ]; + } +} \ No newline at end of file diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index 713cb7d..8687d3f 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -52,6 +52,7 @@ { Logger::getLogger()->debug(sprintf('Processing RPC request for method %s', $rpcRequest->getMethod())); $response = $method->execute($clientRequest, $rpcRequest); + Logger::getLogger()->debug(sprintf('%s method executed successfully', $rpcRequest->getMethod())); } catch(StandardException $e) { diff --git a/src/Socialbox/Socialclient.php b/src/Socialbox/Socialclient.php new file mode 100644 index 0000000..7e33df6 --- /dev/null +++ b/src/Socialbox/Socialclient.php @@ -0,0 +1,11 @@ + Date: Thu, 31 Oct 2024 19:00:22 -0400 Subject: [PATCH 045/420] Rename GetCaptcha to VerificationGetCaptcha --- .../{GetCaptcha.php => VerificationGetCaptcha.php} | 3 +-- src/Socialbox/Enums/StandardMethods.php | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) rename src/Socialbox/Classes/StandardMethods/{GetCaptcha.php => VerificationGetCaptcha.php} (96%) diff --git a/src/Socialbox/Classes/StandardMethods/GetCaptcha.php b/src/Socialbox/Classes/StandardMethods/VerificationGetCaptcha.php similarity index 96% rename from src/Socialbox/Classes/StandardMethods/GetCaptcha.php rename to src/Socialbox/Classes/StandardMethods/VerificationGetCaptcha.php index a7bbc78..854be86 100644 --- a/src/Socialbox/Classes/StandardMethods/GetCaptcha.php +++ b/src/Socialbox/Classes/StandardMethods/VerificationGetCaptcha.php @@ -17,7 +17,7 @@ use Socialbox\Objects\ClientRequest; use Socialbox\Objects\RpcRequest; use Socialbox\Objects\Standard\ImageCaptcha; -class GetCaptcha extends Method +class VerificationGetCaptcha extends Method { /** * @inheritDoc @@ -74,7 +74,6 @@ class GetCaptcha extends Method } // Build the captcha - Logger::getLogger()->debug('Building captcha for peer ' . $peer->getUuid()); return $rpcRequest->produceResponse(new ImageCaptcha([ 'expires' => $captchaRecord->getExpires()->getTimestamp(), 'image' => (new CaptchaBuilder($answer))->build()->inline()] // Returns HTML base64 encoded image of the captcha diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index e412502..c13b9c4 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -3,7 +3,7 @@ namespace Socialbox\Enums; use Socialbox\Classes\StandardMethods\CreateSession; -use Socialbox\Classes\StandardMethods\GetCaptcha; +use Socialbox\Classes\StandardMethods\VerificationGetCaptcha; use Socialbox\Classes\StandardMethods\GetMe; use Socialbox\Classes\StandardMethods\Ping; use Socialbox\Classes\StandardMethods\Register; @@ -18,7 +18,7 @@ enum StandardMethods : string case CREATE_SESSION = 'createSession'; case REGISTER = 'register'; case GET_ME = 'getMe'; - case GET_CAPTCHA = 'getCaptcha'; + case VERIFICATION_GET_CAPTCHA = 'verificationGetCaptcha'; /** * @param ClientRequest $request @@ -34,7 +34,7 @@ enum StandardMethods : string self::CREATE_SESSION => CreateSession::execute($request, $rpcRequest), self::REGISTER => Register::execute($request, $rpcRequest), self::GET_ME => GetMe::execute($request, $rpcRequest), - self::GET_CAPTCHA => GetCaptcha::execute($request, $rpcRequest), + self::VERIFICATION_GET_CAPTCHA => VerificationGetCaptcha::execute($request, $rpcRequest), }; } } \ No newline at end of file From e328a8c5e064f70af900cab828239d0efc1c0e62 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 31 Oct 2024 19:09:13 -0400 Subject: [PATCH 046/420] Refactor peer flag removal logic --- .../Managers/RegisteredPeerManager.php | 18 ++++++++---------- .../Objects/Database/RegisteredPeerRecord.php | 9 +++++++++ 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/Socialbox/Managers/RegisteredPeerManager.php b/src/Socialbox/Managers/RegisteredPeerManager.php index b37bc58..9b0ec58 100644 --- a/src/Socialbox/Managers/RegisteredPeerManager.php +++ b/src/Socialbox/Managers/RegisteredPeerManager.php @@ -331,35 +331,33 @@ class RegisteredPeerManager /** * Removes a specific flag from the peer identified by the given UUID or RegisteredPeerRecord. * - * @param string|RegisteredPeerRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer. + * @param string|RegisteredPeerRecord $peer * @param PeerFlags $flag The flag to be removed from the peer. * @return void * @throws DatabaseOperationException If there is an error while updating the database. */ - public static function removeFlag(string|RegisteredPeerRecord $uuid, PeerFlags $flag): void + public static function removeFlag(string|RegisteredPeerRecord $peer, PeerFlags $flag): void { - if($uuid instanceof RegisteredPeerRecord) + if(is_string($peer)) { - $uuid = $uuid->getUuid(); + $peer = self::getPeer($peer); } - Logger::getLogger()->verbose(sprintf("Removing flag %s from peer %s", $flag->value, $uuid)); + Logger::getLogger()->verbose(sprintf("Removing flag %s from peer %s", $flag->value, $peer->getUuid())); - $peer = self::getPeer($uuid); if(!$peer->flagExists($flag)) { return; } - $flags = $peer->getFlags(); - unset($flags[array_search($flag, $flags)]); + $peer->removeFlag($flag); try { - $implodedFlags = implode(',', $flags); + $implodedFlags = PeerFlags::toString($peer->getFlags()); $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET flags=? WHERE uuid=?'); $statement->bindParam(1, $implodedFlags); - $statement->bindParam(2, $uuid); + $statement->bindParam(2, $registeredPeer); $statement->execute(); } catch(PDOException $e) diff --git a/src/Socialbox/Objects/Database/RegisteredPeerRecord.php b/src/Socialbox/Objects/Database/RegisteredPeerRecord.php index 417c3e4..acb864a 100644 --- a/src/Socialbox/Objects/Database/RegisteredPeerRecord.php +++ b/src/Socialbox/Objects/Database/RegisteredPeerRecord.php @@ -104,6 +104,15 @@ class RegisteredPeerRecord implements SerializableInterface return in_array($flag, $this->flags, true); } + public function removeFlag(PeerFlags $flag): void + { + $key = array_search($flag, $this->flags, true); + if($key !== false) + { + unset($this->flags[$key]); + } + } + /** * Checks if the current instance is enabled. * From cad2ea3419ef0fc9dd0d6cbcad9c5a85fd05b76e Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 31 Oct 2024 19:13:05 -0400 Subject: [PATCH 047/420] Add image captcha verification methods --- .../VerificationAnswerImageCaptcha.php | 96 +++++++++++++++++++ ...ha.php => VerificationGetImageCaptcha.php} | 2 +- src/Socialbox/Enums/StandardMethods.php | 9 +- 3 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php rename src/Socialbox/Classes/StandardMethods/{VerificationGetCaptcha.php => VerificationGetImageCaptcha.php} (98%) diff --git a/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php b/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php new file mode 100644 index 0000000..5cb8524 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php @@ -0,0 +1,96 @@ +getSessionUuid() === null) + { + return $rpcRequest->produceError(StandardError::SESSION_REQUIRED); + } + + // Get the session and check if it's already authenticated + try + { + $session = SessionManager::getSession($request->getSessionUuid()); + } + catch(DatabaseOperationException $e) + { + throw new StandardException("There was an unexpected error while trying to get the session", StandardError::INTERNAL_SERVER_ERROR, $e); + } + + // Check for session conditions + if($session->getPeerUuid() === null) + { + return $rpcRequest->produceError(StandardError::AUTHENTICATION_REQUIRED); + } + + // Get the peer + try + { + $peer = RegisteredPeerManager::getPeer($session->getPeerUuid()); + } + catch(DatabaseOperationException $e) + { + throw new StandardException("There was unexpected error while trying to get the peer", StandardError::INTERNAL_SERVER_ERROR, $e); + } + + // Check if the VER_SOLVE_IMAGE_CAPTCHA flag exists. + if(!$peer->flagExists(PeerFlags::VER_SOLVE_IMAGE_CAPTCHA)) + { + return $rpcRequest->produceError(StandardError::CAPTCHA_NOT_AVAILABLE, 'You are not required to complete a captcha at this time'); + } + + if(!$rpcRequest->containsParameter('answer')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The answer parameter is required'); + } + + try + { + if(CaptchaManager::getCaptcha($session->getPeerUuid())->isExpired()) + { + return $rpcRequest->produceError(StandardError::CAPTCHA_EXPIRED, 'The captcha has expired'); + } + } + catch(DatabaseOperationException $e) + { + throw new StandardException("There was an unexpected error while trying to get the captcha", StandardError::INTERNAL_SERVER_ERROR, $e); + } + + try + { + $result = CaptchaManager::answerCaptcha($session->getPeerUuid(), $rpcRequest->getParameter('answer')); + + if($result) + { + RegisteredPeerManager::removeFlag($session->getPeerUuid(), PeerFlags::VER_SOLVE_IMAGE_CAPTCHA); + } + + return $rpcRequest->produceResponse($result); + } + catch (DatabaseOperationException $e) + { + throw new StandardException("There was an unexpected error while trying to answer the captcha", StandardError::INTERNAL_SERVER_ERROR, $e); + } + } +} \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/VerificationGetCaptcha.php b/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php similarity index 98% rename from src/Socialbox/Classes/StandardMethods/VerificationGetCaptcha.php rename to src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php index 854be86..b99a358 100644 --- a/src/Socialbox/Classes/StandardMethods/VerificationGetCaptcha.php +++ b/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php @@ -17,7 +17,7 @@ use Socialbox\Objects\ClientRequest; use Socialbox\Objects\RpcRequest; use Socialbox\Objects\Standard\ImageCaptcha; -class VerificationGetCaptcha extends Method +class VerificationGetImageCaptcha extends Method { /** * @inheritDoc diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index c13b9c4..6eb44cc 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -3,7 +3,8 @@ namespace Socialbox\Enums; use Socialbox\Classes\StandardMethods\CreateSession; -use Socialbox\Classes\StandardMethods\VerificationGetCaptcha; +use Socialbox\Classes\StandardMethods\VerificationAnswerImageCaptcha; +use Socialbox\Classes\StandardMethods\VerificationGetImageCaptcha; use Socialbox\Classes\StandardMethods\GetMe; use Socialbox\Classes\StandardMethods\Ping; use Socialbox\Classes\StandardMethods\Register; @@ -18,7 +19,8 @@ enum StandardMethods : string case CREATE_SESSION = 'createSession'; case REGISTER = 'register'; case GET_ME = 'getMe'; - case VERIFICATION_GET_CAPTCHA = 'verificationGetCaptcha'; + case VERIFICATION_GET_IMAGE_CAPTCHA = 'verificationGetImageCaptcha'; + case VERIFICATION_ANSWER_IMAGE_CAPTCHA = 'verificationAnswerImageCaptcha'; /** * @param ClientRequest $request @@ -34,7 +36,8 @@ enum StandardMethods : string self::CREATE_SESSION => CreateSession::execute($request, $rpcRequest), self::REGISTER => Register::execute($request, $rpcRequest), self::GET_ME => GetMe::execute($request, $rpcRequest), - self::VERIFICATION_GET_CAPTCHA => VerificationGetCaptcha::execute($request, $rpcRequest), + self::VERIFICATION_GET_IMAGE_CAPTCHA => VerificationGetImageCaptcha::execute($request, $rpcRequest), + self::VERIFICATION_ANSWER_IMAGE_CAPTCHA => VerificationAnswerImageCaptcha::execute($request, $rpcRequest), }; } } \ No newline at end of file From 3a10e01bd8a55e04775cc860cc122321eddb855d Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 9 Dec 2024 19:01:56 -0500 Subject: [PATCH 048/420] Implement client session management and refactoring --- .../Classes/ClientCommands/ConnectCommand.php | 117 ++++++++++++++++++ src/Socialbox/Classes/RpcClient.php | 2 +- .../VerificationGetImageCaptcha.php | 6 +- src/Socialbox/Classes/Utilities.php | 22 ++++ src/Socialbox/Enums/CliCommands.php | 2 + src/Socialbox/Enums/ClientCommands.php | 8 ++ src/Socialbox/Exceptions/RpcException.php | 13 ++ src/Socialbox/Managers/CaptchaManager.php | 12 +- .../Objects/Database/CaptchaRecord.php | 7 +- src/Socialbox/SocialClient.php | 57 +++++++++ src/Socialbox/Socialclient.php | 11 -- 11 files changed, 234 insertions(+), 23 deletions(-) create mode 100644 src/Socialbox/Classes/ClientCommands/ConnectCommand.php create mode 100644 src/Socialbox/Enums/ClientCommands.php create mode 100644 src/Socialbox/SocialClient.php delete mode 100644 src/Socialbox/Socialclient.php diff --git a/src/Socialbox/Classes/ClientCommands/ConnectCommand.php b/src/Socialbox/Classes/ClientCommands/ConnectCommand.php new file mode 100644 index 0000000..e2229b8 --- /dev/null +++ b/src/Socialbox/Classes/ClientCommands/ConnectCommand.php @@ -0,0 +1,117 @@ +error('The name argument is required, this is the name of the session'); + } + + $workingDirectory = getcwd(); + + if(isset($args['directory'])) + { + if(!is_dir($args['directory'])) + { + Logger::getLogger()->error('The directory provided does not exist'); + return 1; + } + + $workingDirectory = $args['directory']; + } + + $sessionFile = $workingDirectory . DIRECTORY_SEPARATOR . Utilities::sanitizeFileName($args['name']) . '.json'; + + if(!file_exists($sessionFile)) + { + return self::createSession($args, $sessionFile); + } + + Logger::getLogger()->info(sprintf('Session file already exists at %s', $sessionFile)); + return 0; + } + + private static function createSession(array $args, string $sessionFile): int + { + if(!isset($args['domain'])) + { + Logger::getLogger()->error('The domain argument is required, this is the domain of the socialbox instance'); + return 1; + } + + try + { + $client = new SocialClient($args['domain']); + } + catch (DatabaseOperationException $e) + { + Logger::getLogger()->error('Failed to create the client session', $e); + return 1; + } + catch (ResolutionException $e) + { + Logger::getLogger()->error('Failed to resolve the domain', $e); + return 1; + } + + try + { + $keyPair = Cryptography::generateKeyPair(); + $session = $client->createSession($keyPair); + } + catch (CryptographyException | RpcException $e) + { + Logger::getLogger()->error('Failed to create the session', $e); + return 1; + } + + $sessionData = new ClientSession([ + 'domain' => $args['domain'], + 'session_uuid' => $session, + 'public_key' => $keyPair->getPublicKey(), + 'private_key' => $keyPair->getPrivateKey() + ]); + + $sessionData->save($sessionFile); + Logger::getLogger()->info(sprintf('Session created and saved to %s', $sessionFile)); + return 0; + } + + public static function getHelpMessage(): string + { + return << --domain [--directory ] + +Creates a new session with the specified name and domain. The session will be saved to the current working directory by default, or to the specified directory if provided. + +Options: + --name The name of the session to create. + --domain The domain of the socialbox instance. + --directory The directory where the session file should be saved. + +Example: + socialbox connect --name mysession --domain socialbox.example.com +HELP; + + } + + public static function getShortHelpMessage(): string + { + return 'Connect Command - Creates a new session with the specified name and domain'; + } +} \ No newline at end of file diff --git a/src/Socialbox/Classes/RpcClient.php b/src/Socialbox/Classes/RpcClient.php index 0b8963e..269e679 100644 --- a/src/Socialbox/Classes/RpcClient.php +++ b/src/Socialbox/Classes/RpcClient.php @@ -27,8 +27,8 @@ * Constructor for initializing the server connection with a given domain. * * @param string $domain The domain used to resolve the server's endpoint and public key. - * @throws DatabaseOperationException * @throws ResolutionException + * @noinspection PhpUnhandledExceptionInspection */ public function __construct(string $domain) { diff --git a/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php b/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php index b99a358..681e366 100644 --- a/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php +++ b/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php @@ -75,8 +75,8 @@ class VerificationGetImageCaptcha extends Method // Build the captcha return $rpcRequest->produceResponse(new ImageCaptcha([ - 'expires' => $captchaRecord->getExpires()->getTimestamp(), - 'image' => (new CaptchaBuilder($answer))->build()->inline()] // Returns HTML base64 encoded image of the captcha - )); + 'expires' => $captchaRecord->getExpires(), + 'image' => (new CaptchaBuilder($answer))->build()->inline() + ])); // Returns HTML base64 encoded image of the captcha } } \ No newline at end of file diff --git a/src/Socialbox/Classes/Utilities.php b/src/Socialbox/Classes/Utilities.php index 5f9a513..b538531 100644 --- a/src/Socialbox/Classes/Utilities.php +++ b/src/Socialbox/Classes/Utilities.php @@ -2,6 +2,7 @@ namespace Socialbox\Classes; +use DateTime; use InvalidArgumentException; use JsonException; use RuntimeException; @@ -170,4 +171,25 @@ class Utilities return $randomString; } + + /** + * Generates a random CRC32 hash. + * + * @return string The generated CRC32 hash as a string. + */ + public static function randomCrc32(): string + { + return hash('crc32b', uniqid()); + } + + /** + * Sanitizes a file name by removing any characters that are not alphanumeric, hyphen, or underscore. + * + * @param string $name The file name to be sanitized. + * @return string The sanitized file name. + */ + public static function sanitizeFileName(string $name): string + { + return preg_replace('/[^a-zA-Z0-9-_]/', '', $name); + } } \ No newline at end of file diff --git a/src/Socialbox/Enums/CliCommands.php b/src/Socialbox/Enums/CliCommands.php index d160006..40ef855 100644 --- a/src/Socialbox/Enums/CliCommands.php +++ b/src/Socialbox/Enums/CliCommands.php @@ -8,6 +8,7 @@ use Socialbox\Classes\CliCommands\InitializeCommand; enum CliCommands : string { case INITIALIZE = 'init'; + case CLIENT = 'client'; /** * Handles the command execution, returns the exit code. @@ -20,6 +21,7 @@ enum CliCommands : string return match ($this) { self::INITIALIZE => InitializeCommand::execute($args), + self::CLIENT => ClientCommand::execute($args) }; } public function getHelpMessage(): string diff --git a/src/Socialbox/Enums/ClientCommands.php b/src/Socialbox/Enums/ClientCommands.php new file mode 100644 index 0000000..9a1e83a --- /dev/null +++ b/src/Socialbox/Enums/ClientCommands.php @@ -0,0 +1,8 @@ +getError(), $error->getCode()->value, $e); + } } \ No newline at end of file diff --git a/src/Socialbox/Managers/CaptchaManager.php b/src/Socialbox/Managers/CaptchaManager.php index ddf62ca..f599161 100644 --- a/src/Socialbox/Managers/CaptchaManager.php +++ b/src/Socialbox/Managers/CaptchaManager.php @@ -3,7 +3,6 @@ namespace Socialbox\Managers; use DateTime; -use DateTimeInterface; use PDOException; use Socialbox\Classes\Database; use Socialbox\Classes\Logger; @@ -31,13 +30,15 @@ class CaptchaManager } $answer = Utilities::randomString(6, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'); + $current_time = (new DateTime())->setTimestamp(time()); if(!self::captchaExists($peer_uuid)) { Logger::getLogger()->debug('Creating a new captcha record for peer ' . $peer_uuid); - $statement = Database::getConnection()->prepare("INSERT INTO captcha_images (peer_uuid, answer) VALUES (?, ?)"); + $statement = Database::getConnection()->prepare("INSERT INTO captcha_images (peer_uuid, created, answer) VALUES (?, ?, ?)"); $statement->bindParam(1, $peer_uuid); - $statement->bindParam(2, $answer); + $statement->bindParam(2, $current_time); + $statement->bindParam(3, $answer); try { @@ -52,9 +53,10 @@ class CaptchaManager } Logger::getLogger()->debug('Updating an existing captcha record for peer ' . $peer_uuid); - $statement = Database::getConnection()->prepare("UPDATE captcha_images SET answer=?, status='UNSOLVED', created=NOW() WHERE peer_uuid=?"); + $statement = Database::getConnection()->prepare("UPDATE captcha_images SET answer=?, status='UNSOLVED', created=? WHERE peer_uuid=?"); $statement->bindParam(1, $answer); - $statement->bindParam(2, $peer_uuid); + $statement->bindParam(2, $current_time); + $statement->bindParam(3, $peer_uuid); try { diff --git a/src/Socialbox/Objects/Database/CaptchaRecord.php b/src/Socialbox/Objects/Database/CaptchaRecord.php index 6526e26..0e94d68 100644 --- a/src/Socialbox/Objects/Database/CaptchaRecord.php +++ b/src/Socialbox/Objects/Database/CaptchaRecord.php @@ -4,6 +4,7 @@ namespace Socialbox\Objects\Database; use DateTime; use Socialbox\Classes\Configuration; +use Socialbox\Classes\Logger; use Socialbox\Enums\Status\CaptchaStatus; use Socialbox\Interfaces\SerializableInterface; @@ -56,14 +57,14 @@ class CaptchaRecord implements SerializableInterface return $this->created; } - public function getExpires(): DateTime + public function getExpires(): int { - return $this->created->modify(sprintf("+%s seconds", Configuration::getSecurityConfiguration()->getCaptchaTtl())); + return $this->created->getTimestamp() + Configuration::getSecurityConfiguration()->getCaptchaTtl(); } public function isExpired(): bool { - return $this->getExpires() < new DateTime(); + return time() > $this->getExpires(); } /** diff --git a/src/Socialbox/SocialClient.php b/src/Socialbox/SocialClient.php new file mode 100644 index 0000000..c11c100 --- /dev/null +++ b/src/Socialbox/SocialClient.php @@ -0,0 +1,57 @@ +sendRequest(new RpcRequest('createSession', Utilities::randomCrc32(), [ + 'public_key' => $keyPair->getPublicKey() + ])); + + if($response === null) + { + throw new RpcException('Failed to create the session, no response received'); + } + + if($response instanceof RpcError) + { + throw RpcException::fromRpcError($response); + } + + $this->setSessionUuid($response->getResult()); + $this->setPrivateKey($keyPair->getPrivateKey()); + + return $response->getResult(); + } + + } \ No newline at end of file diff --git a/src/Socialbox/Socialclient.php b/src/Socialbox/Socialclient.php deleted file mode 100644 index 7e33df6..0000000 --- a/src/Socialbox/Socialclient.php +++ /dev/null @@ -1,11 +0,0 @@ - Date: Mon, 9 Dec 2024 19:26:20 -0500 Subject: [PATCH 049/420] Add DnsRecordCommand to CliCommands for DNS configuration --- .../Classes/CliCommands/DnsRecordCommand.php | 46 +++++++++++++++++++ src/Socialbox/Classes/Logger.php | 1 - src/Socialbox/Enums/CliCommands.php | 12 +++-- src/Socialbox/Program.php | 7 ++- 4 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 src/Socialbox/Classes/CliCommands/DnsRecordCommand.php diff --git a/src/Socialbox/Classes/CliCommands/DnsRecordCommand.php b/src/Socialbox/Classes/CliCommands/DnsRecordCommand.php new file mode 100644 index 0000000..a066af6 --- /dev/null +++ b/src/Socialbox/Classes/CliCommands/DnsRecordCommand.php @@ -0,0 +1,46 @@ +getRpcEndpoint(), + Configuration::getInstanceConfiguration()->getPublicKey() + ); + + Logger::getLogger()->info('Please set the following DNS TXT record for the domain:'); + Logger::getLogger()->info(sprintf(' %s', $txt_record)); + return 0; + } + + /** + * @inheritDoc + */ + public static function getHelpMessage(): string + { + return << InitializeCommand::execute($args), - self::CLIENT => ClientCommand::execute($args) + self::DNS_RECORD => DnsRecordCommand::execute($args) }; } public function getHelpMessage(): string { return match ($this) { - self::INITIALIZE => InitializeCommand::getHelpMessage() + self::INITIALIZE => InitializeCommand::getHelpMessage(), + self::DNS_RECORD => DnsRecordCommand::getHelpMessage() }; } @@ -36,7 +37,8 @@ enum CliCommands : string { return match ($this) { - self::INITIALIZE => InitializeCommand::getShortHelpMessage() + self::INITIALIZE => InitializeCommand::getShortHelpMessage(), + self::DNS_RECORD => DnsRecordCommand::getShortHelpMessage() }; } } diff --git a/src/Socialbox/Program.php b/src/Socialbox/Program.php index 1a8be43..198c953 100644 --- a/src/Socialbox/Program.php +++ b/src/Socialbox/Program.php @@ -33,7 +33,7 @@ return 0; } - print($command->getHelpMessage()); + print($command->getHelpMessage() . "\n"); return 0; } @@ -42,6 +42,11 @@ return CliCommands::INITIALIZE->handle($args); } + if(isset($args[CliCommands::DNS_RECORD->value])) + { + return CliCommands::DNS_RECORD->handle($args); + } + return self::displayHelp(); } From 6c8cbfddeca7f747621f8f035906d44f6e1295a3 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 9 Dec 2024 21:26:25 -0500 Subject: [PATCH 050/420] Improve error handling and timestamp formatting --- examples/index.php | 17 ++++++++++++++++- src/Socialbox/Managers/CaptchaManager.php | 2 +- src/Socialbox/Socialbox.php | 1 - 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/examples/index.php b/examples/index.php index 52926d0..875a454 100644 --- a/examples/index.php +++ b/examples/index.php @@ -3,4 +3,19 @@ require 'ncc'; import('net.nosial.socialbox'); - \Socialbox\Socialbox::handleRpc(); \ No newline at end of file + try + { + \Socialbox\Socialbox::handleRpc(); + } + catch(Exception $e) + { + http_response_code(500); + + if(\Socialbox\Classes\Configuration::getSecurityConfiguration()->isDisplayInternalExceptions()) + { + print_r($e); + return; + } + + print('An internal error occurred'); + } diff --git a/src/Socialbox/Managers/CaptchaManager.php b/src/Socialbox/Managers/CaptchaManager.php index f599161..a2407a4 100644 --- a/src/Socialbox/Managers/CaptchaManager.php +++ b/src/Socialbox/Managers/CaptchaManager.php @@ -30,7 +30,7 @@ class CaptchaManager } $answer = Utilities::randomString(6, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'); - $current_time = (new DateTime())->setTimestamp(time()); + $current_time = (new DateTime())->setTimestamp(time())->format('Y-m-d H:i:s'); if(!self::captchaExists($peer_uuid)) { diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index 8687d3f..d70ca08 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -30,7 +30,6 @@ { Logger::getLogger()->error('Failed to parse the client request', $e); http_response_code($e->getCode()); - print($e->getMessage()); return; } From 790262db08aeb2fa069c920542c3a36e2feed62c Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 10 Dec 2024 12:54:02 -0500 Subject: [PATCH 051/420] Enhance session handling and introduce session flags --- .../Classes/StandardMethods/GetSession.php | 40 +++ src/Socialbox/Classes/Utilities.php | 22 ++ src/Socialbox/Enums/Flags/SessionFlags.php | 21 ++ src/Socialbox/Managers/SessionManager.php | 4 +- .../Objects/Database/SessionRecord.php | 237 ++++++++++++------ .../Objects/Standard/SessionState.php | 8 + 6 files changed, 251 insertions(+), 81 deletions(-) create mode 100644 src/Socialbox/Classes/StandardMethods/GetSession.php create mode 100644 src/Socialbox/Enums/Flags/SessionFlags.php create mode 100644 src/Socialbox/Objects/Standard/SessionState.php diff --git a/src/Socialbox/Classes/StandardMethods/GetSession.php b/src/Socialbox/Classes/StandardMethods/GetSession.php new file mode 100644 index 0000000..f0035ae --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/GetSession.php @@ -0,0 +1,40 @@ +getSessionUuid() === null) + { + return $rpcRequest->produceError(StandardError::SESSION_REQUIRED); + } + + try + { + // Get the session + $session = SessionManager::getSession($request->getSessionUuid()); + } + catch(DatabaseOperationException $e) + { + throw new StandardException("There was an unexpected error while trying to retrieve the session", StandardError::INTERNAL_SERVER_ERROR, $e); + } + + + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/Utilities.php b/src/Socialbox/Classes/Utilities.php index b538531..a935c76 100644 --- a/src/Socialbox/Classes/Utilities.php +++ b/src/Socialbox/Classes/Utilities.php @@ -192,4 +192,26 @@ class Utilities { return preg_replace('/[^a-zA-Z0-9-_]/', '', $name); } + + /** + * Converts an array into a serialized string by joining the elements with a comma. + * + * @param array $list An array of elements that need to be converted to a comma-separated string. + * @return string A string representation of the array elements, joined by commas. + */ + public static function serializeList(array $list): string + { + return implode(',', $list); + } + + /** + * Converts a serialized string into an array by splitting the string at each comma. + * + * @param string $list A comma-separated string that needs to be converted to an array. + * @return array An array of string values obtained by splitting the input string. + */ + public static function unserializeList(string $list): array + { + return explode(',', $list); + } } \ No newline at end of file diff --git a/src/Socialbox/Enums/Flags/SessionFlags.php b/src/Socialbox/Enums/Flags/SessionFlags.php new file mode 100644 index 0000000..821c514 --- /dev/null +++ b/src/Socialbox/Enums/Flags/SessionFlags.php @@ -0,0 +1,21 @@ +uuid = $data['uuid']; - $this->peerUuid = $data['peer_uuid'] ?? null; - $this->authenticated = $data['authenticated'] ?? false; - $this->publicKey = $data['public_key']; - $this->created = $data['created']; - $this->lastRequest = $data['last_request']; + private string $uuid; + private ?string $peerUuid; + private bool $authenticated; + private string $publicKey; + private SessionState $state; + /** + * @var SessionFlags[] + */ + private array $flags; + private DateTime $created; + private ?DateTime $lastRequest; - if(SessionState::tryFrom($data['state']) == null) + /** + * Constructs a new instance using the provided data array. + * + * @param array $data An associative array containing the initialization data, + * which should include keys such as 'uuid', 'peer_uuid', + * 'authenticated', 'public_key', 'created', 'last_request', + * 'flags', and 'state'. + * + * @return void + */ + public function __construct(array $data) { - $this->state = SessionState::CLOSED; - } - else - { - $this->state = SessionState::from($data['state']); - } - } + $this->uuid = $data['uuid']; + $this->peerUuid = $data['peer_uuid'] ?? null; + $this->authenticated = $data['authenticated'] ?? false; + $this->publicKey = $data['public_key']; + $this->created = $data['created']; + $this->lastRequest = $data['last_request']; + $this->flags = Utilities::unserializeList($data['flags']); - public function getUuid(): string - { - return $this->uuid; - } + if(SessionState::tryFrom($data['state']) == null) + { + $this->state = SessionState::CLOSED; + } + else + { + $this->state = SessionState::from($data['state']); + } - public function getPeerUuid(): ?string - { - return $this->peerUuid; - } - - public function isAuthenticated(): bool - { - if($this->peerUuid === null) - { - return false; } - return $this->authenticated; - } + /** + * Retrieves the UUID. + * + * @return string The UUID of the object. + */ + public function getUuid(): string + { + return $this->uuid; + } - public function getPublicKey(): string - { - return $this->publicKey; - } + /** + * Retrieves the UUID of the peer. + * + * @return string|null The UUID of the peer or null if not set. + */ + public function getPeerUuid(): ?string + { + return $this->peerUuid; + } - public function getState(): SessionState - { - return $this->state; - } + /** + * Checks whether the user is authenticated. + * + * @return bool Returns true if the user is authenticated; otherwise, false. + */ + public function isAuthenticated(): bool + { + if($this->peerUuid === null) + { + return false; + } - public function getCreated(): DateTime - { - return $this->created; - } + if(in_array(SessionFlags::AUTHENTICATED, $this->flags)) + { + return true; + } - public function getLastRequest(): ?DateTime - { - return $this->lastRequest; - } + return $this->authenticated; + } - public static function fromArray(array $data): object - { - return new self($data); - } + /** + * Retrieves the public key associated with the instance. + * + * @return string Returns the public key as a string. + */ + public function getPublicKey(): string + { + return $this->publicKey; + } - public function toArray(): array - { - return [ - 'uuid' => $this->uuid, - 'peer_uuid' => $this->peerUuid, - 'authenticated' => $this->authenticated, - 'public_key' => $this->publicKey, - 'state' => $this->state->value, - 'created' => $this->created, - 'last_request' => $this->lastRequest, - ]; - } -} \ No newline at end of file + /** + * Retrieves the current session state. + * + * @return SessionState Returns the current state of the session. + */ + public function getState(): SessionState + { + return $this->state; + } + + /** + * Retrieves the creation date and time of the object. + * + * @return DateTime Returns a DateTime object representing when the object was created. + */ + public function getCreated(): DateTime + { + return $this->created; + } + + /** + * Retrieves the list of flags associated with the current instance. + * + * @return array Returns an array of flags. + */ + public function getFlags(): array + { + return $this->flags; + } + + /** + * Retrieves the timestamp of the last request made. + * + * @return DateTime|null The DateTime object representing the last request time, or null if no request has been made. + */ + public function getLastRequest(): ?DateTime + { + return $this->lastRequest; + } + + /** + * Creates a new instance of the class using the provided array data. + * + * @param array $data An associative array of data used to initialize the object properties. + * @return object Returns a newly created object instance. + */ + public static function fromArray(array $data): object + { + return new self($data); + } + + /** + * Converts the object's properties to an associative array. + * + * @return array An associative array representing the object's data, including keys 'uuid', 'peer_uuid', + * 'authenticated', 'public_key', 'state', 'flags', 'created', and 'last_request'. + */ + public function toArray(): array + { + return [ + 'uuid' => $this->uuid, + 'peer_uuid' => $this->peerUuid, + 'authenticated' => $this->authenticated, + 'public_key' => $this->publicKey, + 'state' => $this->state->value, + 'flags' => Utilities::serializeList($this->flags), + 'created' => $this->created, + 'last_request' => $this->lastRequest, + ]; + } + } \ No newline at end of file diff --git a/src/Socialbox/Objects/Standard/SessionState.php b/src/Socialbox/Objects/Standard/SessionState.php new file mode 100644 index 0000000..67819a5 --- /dev/null +++ b/src/Socialbox/Objects/Standard/SessionState.php @@ -0,0 +1,8 @@ + Date: Tue, 10 Dec 2024 13:30:08 -0500 Subject: [PATCH 052/420] Add session flags management and encryption key support --- .../Classes/CliCommands/InitializeCommand.php | 11 +- src/Socialbox/Classes/Configuration.php | 1 + .../Configuration/InstanceConfiguration.php | 145 ++++++++++-------- src/Socialbox/Classes/Cryptography.php | 21 +++ src/Socialbox/Enums/StandardError.php | 4 +- src/Socialbox/Enums/StandardMethods.php | 3 + src/Socialbox/Managers/SessionManager.php | 95 ++++++++++++ .../Objects/Database/SessionRecord.php | 5 - 8 files changed, 209 insertions(+), 76 deletions(-) diff --git a/src/Socialbox/Classes/CliCommands/InitializeCommand.php b/src/Socialbox/Classes/CliCommands/InitializeCommand.php index c45e09d..8498fea 100644 --- a/src/Socialbox/Classes/CliCommands/InitializeCommand.php +++ b/src/Socialbox/Classes/CliCommands/InitializeCommand.php @@ -3,7 +3,6 @@ namespace Socialbox\Classes\CliCommands; use Exception; -use LogLib\Log; use PDOException; use Socialbox\Abstracts\CacheLayer; use Socialbox\Classes\Configuration; @@ -97,22 +96,28 @@ class InitializeCommand implements CliCommandInterface } } - if(!Configuration::getInstanceConfiguration()->getPublicKey() || !Configuration::getInstanceConfiguration()->getPrivateKey()) + if( + !Configuration::getInstanceConfiguration()->getPublicKey() || + !Configuration::getInstanceConfiguration()->getPrivateKey() || + !Configuration::getInstanceConfiguration()->getEncryptionKey() + ) { try { Logger::getLogger()->info('Generating new key pair...'); $keyPair = Cryptography::generateKeyPair(); + $encryptionKey = Cryptography::randomBytes(230, 314); } catch (CryptographyException $e) { - Logger::getLogger()->error('Failed to generate keypair', $e); + Logger::getLogger()->error('Failed to generate cryptography values', $e); return 1; } Logger::getLogger()->info('Updating configuration...'); Configuration::getConfigurationLib()->set('instance.private_key', $keyPair->getPrivateKey()); Configuration::getConfigurationLib()->set('instance.public_key', $keyPair->getPublicKey()); + Configuration::getConfigurationLib()->set('instance.encryption_key', $encryptionKey); Configuration::getConfigurationLib()->save(); Logger::getLogger()->info(sprintf('Set the DNS TXT record for the domain %s to the following value:', Configuration::getInstanceConfiguration()->getDomain())); diff --git a/src/Socialbox/Classes/Configuration.php b/src/Socialbox/Classes/Configuration.php index d667097..c26c9fc 100644 --- a/src/Socialbox/Classes/Configuration.php +++ b/src/Socialbox/Classes/Configuration.php @@ -36,6 +36,7 @@ class Configuration $config->setDefault('instance.rpc_endpoint', null); $config->setDefault('instance.private_key', null); $config->setDefault('instance.public_key', null); + $config->setDefault('instance.encryption_key', null); // Security Configuration $config->setDefault('security.display_internal_exceptions', false); diff --git a/src/Socialbox/Classes/Configuration/InstanceConfiguration.php b/src/Socialbox/Classes/Configuration/InstanceConfiguration.php index 26ea951..4ebafc6 100644 --- a/src/Socialbox/Classes/Configuration/InstanceConfiguration.php +++ b/src/Socialbox/Classes/Configuration/InstanceConfiguration.php @@ -1,76 +1,87 @@ enabled = (bool)$data['enabled']; - $this->domain = $data['domain']; - $this->rpcEndpoint = $data['rpc_endpoint']; - $this->privateKey = $data['private_key']; - $this->publicKey = $data['public_key']; - } + private bool $enabled; + private ?string $domain; + private ?string $rpcEndpoint; + private ?string $privateKey; + private ?string $publicKey; + private ?string $encryptionKey; - /** - * Checks if the current object is enabled. - * - * @return bool True if the object is enabled, false otherwise. - */ - public function isEnabled(): bool - { - return $this->enabled; - } + /** + * Constructor that initializes object properties with the provided data. + * + * @param array $data An associative array with keys 'enabled', 'domain', 'private_key', and 'public_key'. + * @return void + */ + public function __construct(array $data) + { + $this->enabled = (bool)$data['enabled']; + $this->domain = $data['domain']; + $this->rpcEndpoint = $data['rpc_endpoint']; + $this->privateKey = $data['private_key']; + $this->publicKey = $data['public_key']; + $this->encryptionKey = $data['encryption_key']; + } - /** - * Retrieves the domain. - * - * @return string|null The domain. - */ - public function getDomain(): ?string - { - return $this->domain; - } + /** + * Checks if the current object is enabled. + * + * @return bool True if the object is enabled, false otherwise. + */ + public function isEnabled(): bool + { + return $this->enabled; + } - /** - * @return string|null - */ - public function getRpcEndpoint(): ?string - { - return $this->rpcEndpoint; - } + /** + * Retrieves the domain. + * + * @return string|null The domain. + */ + public function getDomain(): ?string + { + return $this->domain; + } - /** - * Retrieves the private key. - * - * @return string|null The private key. - */ - public function getPrivateKey(): ?string - { - return $this->privateKey; - } + /** + * @return string|null + */ + public function getRpcEndpoint(): ?string + { + return $this->rpcEndpoint; + } - /** - * Retrieves the public key. - * - * @return string|null The public key. - */ - public function getPublicKey(): ?string - { - return $this->publicKey; - } -} \ No newline at end of file + /** + * Retrieves the private key. + * + * @return string|null The private key. + */ + public function getPrivateKey(): ?string + { + return $this->privateKey; + } + + /** + * Retrieves the public key. + * + * @return string|null The public key. + */ + public function getPublicKey(): ?string + { + return $this->publicKey; + } + + /** + * Retrieves the encryption key. + * + * @return string|null The encryption key. + */ + public function getEncryptionKey(): ?string + { + return $this->encryptionKey; + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/Cryptography.php b/src/Socialbox/Classes/Cryptography.php index f69605d..7872d60 100644 --- a/src/Socialbox/Classes/Cryptography.php +++ b/src/Socialbox/Classes/Cryptography.php @@ -3,6 +3,7 @@ namespace Socialbox\Classes; use InvalidArgumentException; +use Random\RandomException; use Socialbox\Exceptions\CryptographyException; use Socialbox\Objects\KeyPair; @@ -266,4 +267,24 @@ class Cryptography return true; } + + /** + * Generates a random sequence of bytes with a length determined between the specified minimum and maximum. + * + * @param int $minLength The minimum length of the generated byte sequence. + * @param int $maxLength The maximum length of the generated byte sequence. + * @return string A hexadecimal string representing the random byte sequence. + * @throws CryptographyException If the random byte generation fails. + */ + public static function randomBytes(int $minLength, int $maxLength): string + { + try + { + return bin2hex(random_bytes(random_int($minLength, $maxLength))); + } + catch(RandomException $e) + { + throw new CryptographyException('Failed to generate random bytes: ' . $e->getMessage()); + } + } } \ No newline at end of file diff --git a/src/Socialbox/Enums/StandardError.php b/src/Socialbox/Enums/StandardError.php index 9ab424f..ad0c31c 100644 --- a/src/Socialbox/Enums/StandardError.php +++ b/src/Socialbox/Enums/StandardError.php @@ -31,6 +31,7 @@ enum StandardError : int case PEER_NOT_FOUND = -4000; case INVALID_USERNAME = -4001; case USERNAME_ALREADY_EXISTS = -4002; + case NOT_REGISTERED = -4003; /** * Returns the default generic message for the error @@ -62,7 +63,8 @@ enum StandardError : int self::PEER_NOT_FOUND => 'The requested peer was not found', self::INVALID_USERNAME => 'The given username is invalid, it must be Alphanumeric with a minimum of 3 character but no greater than 255 characters', - self::USERNAME_ALREADY_EXISTS => 'The given username already exists on the network' + self::USERNAME_ALREADY_EXISTS => 'The given username already exists on the network', + self::NOT_REGISTERED => 'The given username is not registered on the server', }; } diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index 6eb44cc..b454ec2 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -3,6 +3,7 @@ namespace Socialbox\Enums; use Socialbox\Classes\StandardMethods\CreateSession; +use Socialbox\Classes\StandardMethods\Identify; use Socialbox\Classes\StandardMethods\VerificationAnswerImageCaptcha; use Socialbox\Classes\StandardMethods\VerificationGetImageCaptcha; use Socialbox\Classes\StandardMethods\GetMe; @@ -18,6 +19,7 @@ enum StandardMethods : string case PING = 'ping'; case CREATE_SESSION = 'createSession'; case REGISTER = 'register'; + case IDENTIFY = 'identify'; case GET_ME = 'getMe'; case VERIFICATION_GET_IMAGE_CAPTCHA = 'verificationGetImageCaptcha'; case VERIFICATION_ANSWER_IMAGE_CAPTCHA = 'verificationAnswerImageCaptcha'; @@ -35,6 +37,7 @@ enum StandardMethods : string self::PING => Ping::execute($request, $rpcRequest), self::CREATE_SESSION => CreateSession::execute($request, $rpcRequest), self::REGISTER => Register::execute($request, $rpcRequest), + self::IDENTIFY => Identify::execute($request, $rpcRequest), self::GET_ME => GetMe::execute($request, $rpcRequest), self::VERIFICATION_GET_IMAGE_CAPTCHA => VerificationGetImageCaptcha::execute($request, $rpcRequest), self::VERIFICATION_ANSWER_IMAGE_CAPTCHA => VerificationAnswerImageCaptcha::execute($request, $rpcRequest), diff --git a/src/Socialbox/Managers/SessionManager.php b/src/Socialbox/Managers/SessionManager.php index 160eefc..f7565da 100644 --- a/src/Socialbox/Managers/SessionManager.php +++ b/src/Socialbox/Managers/SessionManager.php @@ -10,6 +10,7 @@ use Socialbox\Classes\Cryptography; use Socialbox\Classes\Database; use Socialbox\Classes\Logger; + use Socialbox\Classes\Utilities; use Socialbox\Enums\SessionState; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; @@ -224,4 +225,98 @@ throw new DatabaseOperationException('Failed to update session state', $e); } } + + /** + * Retrieves the flags associated with a specific session. + * + * @param string $uuid The UUID of the session to retrieve flags for. + * @return array An array of flags associated with the specified session. + * @throws StandardException If the specified session does not exist. + * @throws DatabaseOperationException If there + */ + private static function getFlags(string $uuid): array + { + Logger::getLogger()->verbose(sprintf("Retrieving flags for session %s", $uuid)); + + try + { + $statement = Database::getConnection()->prepare("SELECT flags FROM sessions WHERE uuid=?"); + $statement->bindParam(1, $uuid); + $statement->execute(); + $data = $statement->fetch(PDO::FETCH_ASSOC); + + if ($data === false) + { + throw new StandardException(sprintf("The requested session '%s' does not exist", $uuid), StandardError::SESSION_NOT_FOUND); + } + + return Utilities::unserializeList($data['flags']); + } + catch (PDOException $e) + { + throw new DatabaseOperationException(sprintf('Failed to retrieve flags for session %s', $uuid), $e); + } + } + + /** + * Adds the specified flags to the session identified by the given UUID. + * + * @param string $uuid The unique identifier of the session to which the flags will be added. + * @param array $flags The flags to add to the session. + * @return void + * @throws DatabaseOperationException|StandardException If there is an error while updating the session in the database. + */ + public static function addFlags(string $uuid, array $flags): void + { + Logger::getLogger()->verbose(sprintf("Adding flags to session %s", $uuid)); + + // First get the existing flags + $existingFlags = self::getFlags($uuid); + + // Merge the new flags with the existing ones + $flags = array_unique(array_merge($existingFlags, $flags)); + + try + { + $statement = Database::getConnection()->prepare("UPDATE sessions SET flags=? WHERE uuid=?"); + $statement->bindValue(1, Utilities::serializeList($flags)); + $statement->bindParam(2, $uuid); + $statement->execute(); + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to add flags to session', $e); + } + } + + /** + * Removes specified flags from the session associated with the given UUID. + * + * @param string $uuid The UUID of the session from which the flags will be removed. + * @param array $flags An array of flags to be removed from the session. + * @return void + * @throws DatabaseOperationException|StandardException If there is an error while updating the session in the database. + */ + public static function removeFlags(string $uuid, array $flags): void + { + Logger::getLogger()->verbose(sprintf("Removing flags from session %s", $uuid)); + + // First get the existing flags + $existingFlags = self::getFlags($uuid); + + // Remove the specified flags + $flags = array_diff($existingFlags, $flags); + + try + { + $statement = Database::getConnection()->prepare("UPDATE sessions SET flags=? WHERE uuid=?"); + $statement->bindValue(1, Utilities::serializeList($flags)); + $statement->bindParam(2, $uuid); + $statement->execute(); + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to remove flags from session', $e); + } + } } \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/SessionRecord.php b/src/Socialbox/Objects/Database/SessionRecord.php index 70d7a8e..ddce0cc 100644 --- a/src/Socialbox/Objects/Database/SessionRecord.php +++ b/src/Socialbox/Objects/Database/SessionRecord.php @@ -85,11 +85,6 @@ return false; } - if(in_array(SessionFlags::AUTHENTICATED, $this->flags)) - { - return true; - } - return $this->authenticated; } From 86435a3d0bb4b3b7c796d5286755e2da18babc48 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 10 Dec 2024 22:14:43 -0500 Subject: [PATCH 053/420] Add encryption handling and session flags management. --- .idea/sqldialects.xml | 1 + .../Classes/CliCommands/InitializeCommand.php | 272 ++++++++++-------- src/Socialbox/Classes/Configuration.php | 23 +- .../Configuration/InstanceConfiguration.php | 44 ++- src/Socialbox/Classes/Cryptography.php | 22 +- src/Socialbox/Classes/SecuredPassword.php | 96 +++++++ .../Classes/StandardMethods/Identify.php | 73 +++++ .../Managers/EncryptionRecordsManager.php | 205 +++++++++++++ .../Objects/Database/DecryptedRecord.php | 41 +++ .../Objects/Database/EncryptionRecord.php | 84 ++++++ .../Objects/Database/SecurePasswordRecord.php | 84 ++++++ .../Socialbox/Classes/SecuredPasswordTest.php | 22 ++ tests/test.php | 24 ++ 13 files changed, 857 insertions(+), 134 deletions(-) create mode 100644 src/Socialbox/Classes/SecuredPassword.php create mode 100644 src/Socialbox/Classes/StandardMethods/Identify.php create mode 100644 src/Socialbox/Managers/EncryptionRecordsManager.php create mode 100644 src/Socialbox/Objects/Database/DecryptedRecord.php create mode 100644 src/Socialbox/Objects/Database/EncryptionRecord.php create mode 100644 src/Socialbox/Objects/Database/SecurePasswordRecord.php create mode 100644 tests/Socialbox/Classes/SecuredPasswordTest.php create mode 100644 tests/test.php diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml index 016146b..80dc28a 100644 --- a/.idea/sqldialects.xml +++ b/.idea/sqldialects.xml @@ -6,6 +6,7 @@ + diff --git a/src/Socialbox/Classes/CliCommands/InitializeCommand.php b/src/Socialbox/Classes/CliCommands/InitializeCommand.php index 8498fea..aa3c145 100644 --- a/src/Socialbox/Classes/CliCommands/InitializeCommand.php +++ b/src/Socialbox/Classes/CliCommands/InitializeCommand.php @@ -1,152 +1,172 @@ isEnabled() === false && !isset($args['force'])) + /** + * @inheritDoc + */ + public static function execute(array $args): int { - $required_configurations = [ - 'database.host', 'database.port', 'database.username', 'database.password', 'database.name', - 'instance.enabled', 'instance.domain', 'registration.*' - ]; - - Logger::getLogger()->error('Socialbox is disabled. Use --force to initialize the instance or set `instance.enabled` to True in the configuration'); - Logger::getLogger()->info('The reason you are required to do this is to allow you to configure the instance before enabling it'); - Logger::getLogger()->info('The following configurations are required to be set before enabling the instance:'); - foreach($required_configurations as $config) + if(Configuration::getInstanceConfiguration()->isEnabled() === false && !isset($args['force'])) { - Logger::getLogger()->info(sprintf(' - %s', $config)); - } + $required_configurations = [ + 'database.host', 'database.port', 'database.username', 'database.password', 'database.name', + 'instance.enabled', 'instance.domain', 'registration.*' + ]; - Logger::getLogger()->info('instance.private_key & instance.public_key are automatically generated if not set'); - Logger::getLogger()->info('instance.domain is required to be set to the domain name of the instance'); - Logger::getLogger()->info('instance.rpc_endpoint is required to be set to the publicly accessible http rpc endpoint of this server'); - Logger::getLogger()->info('registration.* are required to be set to allow users to register to the instance'); - Logger::getLogger()->info('You will be given a DNS TXT record to set for the public key after the initialization process'); - Logger::getLogger()->info('The configuration file can be edited using ConfigLib:'); - Logger::getLogger()->info(' configlib --conf socialbox -e nano'); - Logger::getLogger()->info('Or manually at:'); - Logger::getLogger()->info(sprintf(' %s', Configuration::getConfigurationLib()->getPath())); - return 1; - } - - if(Configuration::getInstanceConfiguration()->getDomain() === null) - { - Logger::getLogger()->error('instance.domain is required but was not set'); - return 1; - } - - if(Configuration::getInstanceConfiguration()->getRpcEndpoint() === null) - { - Logger::getLogger()->error('instance.rpc_endpoint is required but was not set'); - return 1; - } - - Logger::getLogger()->info('Initializing Socialbox...'); - if(Configuration::getCacheConfiguration()->isEnabled()) - { - Logger::getLogger()->verbose('Clearing cache layer...'); - CacheLayer::getInstance()->clear(); - } - - foreach(DatabaseObjects::casesOrdered() as $object) - { - Logger::getLogger()->verbose("Initializing database object {$object->value}"); - - try - { - Database::getConnection()->exec(file_get_contents(Resources::getDatabaseResource($object))); - } - catch (PDOException $e) - { - // Check if the error code is for "table already exists" - if ($e->getCode() === '42S01') + Logger::getLogger()->error('Socialbox is disabled. Use --force to initialize the instance or set `instance.enabled` to True in the configuration'); + Logger::getLogger()->info('The reason you are required to do this is to allow you to configure the instance before enabling it'); + Logger::getLogger()->info('The following configurations are required to be set before enabling the instance:'); + foreach($required_configurations as $config) { - Logger::getLogger()->warning("Database object {$object->value} already exists, skipping..."); - continue; + Logger::getLogger()->info(sprintf(' - %s', $config)); } - else + + Logger::getLogger()->info('instance.private_key & instance.public_key are automatically generated if not set'); + Logger::getLogger()->info('instance.domain is required to be set to the domain name of the instance'); + Logger::getLogger()->info('instance.rpc_endpoint is required to be set to the publicly accessible http rpc endpoint of this server'); + Logger::getLogger()->info('registration.* are required to be set to allow users to register to the instance'); + Logger::getLogger()->info('You will be given a DNS TXT record to set for the public key after the initialization process'); + Logger::getLogger()->info('The configuration file can be edited using ConfigLib:'); + Logger::getLogger()->info(' configlib --conf socialbox -e nano'); + Logger::getLogger()->info('Or manually at:'); + Logger::getLogger()->info(sprintf(' %s', Configuration::getConfigurationLib()->getPath())); + return 1; + } + + if(Configuration::getInstanceConfiguration()->getDomain() === null) + { + Logger::getLogger()->error('instance.domain is required but was not set'); + return 1; + } + + if(Configuration::getInstanceConfiguration()->getRpcEndpoint() === null) + { + Logger::getLogger()->error('instance.rpc_endpoint is required but was not set'); + return 1; + } + + Logger::getLogger()->info('Initializing Socialbox...'); + if(Configuration::getCacheConfiguration()->isEnabled()) + { + Logger::getLogger()->verbose('Clearing cache layer...'); + CacheLayer::getInstance()->clear(); + } + + foreach(DatabaseObjects::casesOrdered() as $object) + { + Logger::getLogger()->verbose("Initializing database object {$object->value}"); + + try + { + Database::getConnection()->exec(file_get_contents(Resources::getDatabaseResource($object))); + } + catch (PDOException $e) + { + // Check if the error code is for "table already exists" + if ($e->getCode() === '42S01') + { + Logger::getLogger()->warning("Database object {$object->value} already exists, skipping..."); + continue; + } + else + { + Logger::getLogger()->error("Failed to initialize database object {$object->value}: {$e->getMessage()}", $e); + return 1; + } + } + catch(Exception $e) { Logger::getLogger()->error("Failed to initialize database object {$object->value}: {$e->getMessage()}", $e); return 1; } } - catch(Exception $e) - { - Logger::getLogger()->error("Failed to initialize database object {$object->value}: {$e->getMessage()}", $e); - return 1; - } - } - if( - !Configuration::getInstanceConfiguration()->getPublicKey() || - !Configuration::getInstanceConfiguration()->getPrivateKey() || - !Configuration::getInstanceConfiguration()->getEncryptionKey() - ) - { + if( + !Configuration::getInstanceConfiguration()->getPublicKey() || + !Configuration::getInstanceConfiguration()->getPrivateKey() || + !Configuration::getInstanceConfiguration()->getEncryptionKeys() + ) + { + try + { + Logger::getLogger()->info('Generating new key pair...'); + $keyPair = Cryptography::generateKeyPair(); + $encryptionKeys = Cryptography::randomKeyS(230, 314, Configuration::getInstanceConfiguration()->getEncryptionKeysCount()); + } + catch (CryptographyException $e) + { + Logger::getLogger()->error('Failed to generate cryptography values', $e); + return 1; + } + + Logger::getLogger()->info('Updating configuration...'); + Configuration::getConfigurationLib()->set('instance.private_key', $keyPair->getPrivateKey()); + Configuration::getConfigurationLib()->set('instance.public_key', $keyPair->getPublicKey()); + Configuration::getConfigurationLib()->set('instance.encryption_keys', $encryptionKeys); + Configuration::getConfigurationLib()->save(); // Save + Configuration::reload(); // Reload + + Logger::getLogger()->info(sprintf('Set the DNS TXT record for the domain %s to the following value:', Configuration::getInstanceConfiguration()->getDomain())); + Logger::getLogger()->info(sprintf("v=socialbox;sb-rpc=%s;sb-key=%s;", + Configuration::getInstanceConfiguration()->getRpcEndpoint(), $keyPair->getPublicKey() + )); + } + try { - Logger::getLogger()->info('Generating new key pair...'); - $keyPair = Cryptography::generateKeyPair(); - $encryptionKey = Cryptography::randomBytes(230, 314); + if(EncryptionRecordsManager::getRecordCount() < Configuration::getInstanceConfiguration()->getEncryptionRecordsCount()) + { + Logger::getLogger()->info('Generating encryption records...'); + EncryptionRecordsManager::generateRecords(Configuration::getInstanceConfiguration()->getEncryptionRecordsCount()); + } } catch (CryptographyException $e) { - Logger::getLogger()->error('Failed to generate cryptography values', $e); - return 1; + Logger::getLogger()->error('Failed to generate encryption records due to a cryptography exception', $e); + } + catch (DatabaseOperationException $e) + { + Logger::getLogger()->error('Failed to generate encryption records due to a database error', $e); } - Logger::getLogger()->info('Updating configuration...'); - Configuration::getConfigurationLib()->set('instance.private_key', $keyPair->getPrivateKey()); - Configuration::getConfigurationLib()->set('instance.public_key', $keyPair->getPublicKey()); - Configuration::getConfigurationLib()->set('instance.encryption_key', $encryptionKey); - Configuration::getConfigurationLib()->save(); - - Logger::getLogger()->info(sprintf('Set the DNS TXT record for the domain %s to the following value:', Configuration::getInstanceConfiguration()->getDomain())); - Logger::getLogger()->info(sprintf("v=socialbox;sb-rpc=%s;sb-key=%s;", - Configuration::getInstanceConfiguration()->getRpcEndpoint(), $keyPair->getPublicKey() - )); + // TODO: Create a host peer here? + Logger::getLogger()->info('Socialbox has been initialized successfully'); + return 0; } - // TODO: Create a host peer here? - Logger::getLogger()->info('Socialbox has been initialized successfully'); - return 0; - } + /** + * @inheritDoc + */ + public static function getHelpMessage(): string + { + return "Initialize Command - Initializes Socialbox for first-runs\n" . + "Usage: socialbox init [arguments]\n\n" . + "Arguments:\n" . + " --force - Forces the initialization process to run even the instance is disabled\n"; + } - /** - * @inheritDoc - */ - public static function getHelpMessage(): string - { - return "Initialize Command - Initializes Socialbox for first-runs\n" . - "Usage: socialbox init [arguments]\n\n" . - "Arguments:\n" . - " --force - Forces the initialization process to run even the instance is disabled\n"; - } - - /** - * @inheritDoc - */ - public static function getShortHelpMessage(): string - { - return "Initializes Socialbox for first-runs"; - } -} \ No newline at end of file + /** + * @inheritDoc + */ + public static function getShortHelpMessage(): string + { + return "Initializes Socialbox for first-runs"; + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/Configuration.php b/src/Socialbox/Classes/Configuration.php index c26c9fc..7537424 100644 --- a/src/Socialbox/Classes/Configuration.php +++ b/src/Socialbox/Classes/Configuration.php @@ -34,9 +34,11 @@ class Configuration $config->setDefault('instance.enabled', false); // False by default, requires the user to enable it. $config->setDefault('instance.domain', null); $config->setDefault('instance.rpc_endpoint', null); + $config->setDefault('instance.encryption_keys_count', 5); + $config->setDefault('instance.encryption_record_count', 5); $config->setDefault('instance.private_key', null); $config->setDefault('instance.public_key', null); - $config->setDefault('instance.encryption_key', null); + $config->setDefault('instance.encryption_keys', null); // Security Configuration $config->setDefault('security.display_internal_exceptions', false); @@ -89,6 +91,25 @@ class Configuration self::$registrationConfiguration = new RegistrationConfiguration(self::$configuration->getConfiguration()['registration']); } + /** + * Resets all configuration instances by setting them to null and then + * reinitializes the configurations. + * + * @return void + */ + public static function reload(): void + { + self::$configuration = null; + self::$instanceConfiguration = null; + self::$securityConfiguration = null; + self::$databaseConfiguration = null; + self::$loggingConfiguration = null; + self::$cacheConfiguration = null; + self::$registrationConfiguration = null; + + self::initializeConfiguration(); + } + /** * Retrieves the current configuration array. If the configuration is not initialized, * it triggers the initialization process. diff --git a/src/Socialbox/Classes/Configuration/InstanceConfiguration.php b/src/Socialbox/Classes/Configuration/InstanceConfiguration.php index 4ebafc6..7d8b3d2 100644 --- a/src/Socialbox/Classes/Configuration/InstanceConfiguration.php +++ b/src/Socialbox/Classes/Configuration/InstanceConfiguration.php @@ -7,9 +7,11 @@ private bool $enabled; private ?string $domain; private ?string $rpcEndpoint; + private int $encryptionKeysCount; + private int $encryptionRecordsCount; private ?string $privateKey; private ?string $publicKey; - private ?string $encryptionKey; + private ?array $encryptionKeys; /** * Constructor that initializes object properties with the provided data. @@ -22,9 +24,11 @@ $this->enabled = (bool)$data['enabled']; $this->domain = $data['domain']; $this->rpcEndpoint = $data['rpc_endpoint']; + $this->encryptionKeysCount = $data['encryption_keys_count']; + $this->encryptionRecordsCount = $data['encryption_records_count']; $this->privateKey = $data['private_key']; $this->publicKey = $data['public_key']; - $this->encryptionKey = $data['encryption_key']; + $this->encryptionKeys = $data['encryption_keys']; } /** @@ -55,6 +59,26 @@ return $this->rpcEndpoint; } + /** + * Retrieves the number of encryption keys. + * + * @return int The number of encryption keys. + */ + public function getEncryptionKeysCount(): int + { + return $this->encryptionKeysCount; + } + + /** + * Retrieves the number of encryption records. + * + * @return int The number of encryption records. + */ + public function getEncryptionRecordsCount(): int + { + return $this->encryptionRecordsCount; + } + /** * Retrieves the private key. * @@ -76,12 +100,20 @@ } /** - * Retrieves the encryption key. + * Retrieves the encryption keys. * - * @return string|null The encryption key. + * @return array|null The encryption keys. */ - public function getEncryptionKey(): ?string + public function getEncryptionKeys(): ?array { - return $this->encryptionKey; + return $this->encryptionKeys; + } + + /** + * @return string + */ + public function getRandomEncryptionKey(): string + { + return $this->encryptionKeys[array_rand($this->encryptionKeys)]; } } \ No newline at end of file diff --git a/src/Socialbox/Classes/Cryptography.php b/src/Socialbox/Classes/Cryptography.php index 7872d60..28e0b17 100644 --- a/src/Socialbox/Classes/Cryptography.php +++ b/src/Socialbox/Classes/Cryptography.php @@ -276,7 +276,7 @@ class Cryptography * @return string A hexadecimal string representing the random byte sequence. * @throws CryptographyException If the random byte generation fails. */ - public static function randomBytes(int $minLength, int $maxLength): string + public static function randomKey(int $minLength, int $maxLength): string { try { @@ -287,4 +287,24 @@ class Cryptography throw new CryptographyException('Failed to generate random bytes: ' . $e->getMessage()); } } + + /** + * Generates an array of random keys, each with a length within the specified range. + * + * @param int $minLength The minimum length for each random key. + * @param int $maxLength The maximum length for each random key. + * @param int $amount The number of random keys to generate. + * @return array An array of randomly generated keys. + * @throws CryptographyException If the random key generation fails. + */ + public static function randomKeys(int $minLength, int $maxLength, int $amount): array + { + $keys = []; + for($i = 0; $i < $amount; $i++) + { + $keys[] = self::randomKey($minLength, $maxLength); + } + + return $keys; + } } \ No newline at end of file diff --git a/src/Socialbox/Classes/SecuredPassword.php b/src/Socialbox/Classes/SecuredPassword.php new file mode 100644 index 0000000..30e031f --- /dev/null +++ b/src/Socialbox/Classes/SecuredPassword.php @@ -0,0 +1,96 @@ +decrypt(); + $saltedPassword = $decrypted->getSalt() . $password; + $derivedKey = hash_pbkdf2('sha512', $saltedPassword, $decrypted->getPepper(), self::ITERATIONS, self::KEY_LENGTH / 8, true); + + try + { + $iv = random_bytes(openssl_cipher_iv_length(self::ENCRYPTION_ALGORITHM)); + } + catch (RandomException $e) + { + throw new CryptographyException("Failed to generate IV for password encryption", $e); + } + + $tag = null; + $encryptedPassword = openssl_encrypt($derivedKey, self::ENCRYPTION_ALGORITHM, base64_decode($decrypted->getKey()), OPENSSL_RAW_DATA, $iv, $tag); + + if ($encryptedPassword === false) + { + throw new CryptographyException("Password encryption failed"); + } + + return new SecurePasswordRecord([ + 'peer_uuid' => $peerUuid, + 'iv' => base64_encode($iv), + 'encrypted_password' => base64_encode($encryptedPassword), + 'encrypted_tag' => base64_encode($tag), + 'updated' => (new DateTime())->setTimestamp(time()) + ]); + } + + /** + * Verifies the provided password against the secured data and encryption records. + * + * @param string $input The user-provided password to be verified. + * @param SecurePasswordRecord $secured An array containing encrypted data required for verification. + * @param EncryptionRecord[] $encryptionRecords An array of encryption records used to perform decryption and validation. + * @return bool Returns true if the password matches the secured data; otherwise, returns false. + * @throws CryptographyException + */ + public static function verifyPassword(string $input, SecurePasswordRecord $secured, array $encryptionRecords): bool + { + foreach ($encryptionRecords as $record) + { + $decrypted = $record->decrypt(); + $saltedInput = $decrypted->getSalt() . $input; + $derivedKey = hash_pbkdf2('sha512', $saltedInput, $decrypted->getPepper(), self::ITERATIONS, self::KEY_LENGTH / 8, true); + + // Validation by re-encrypting and comparing + $encryptedTag = base64_decode($secured->getEncryptedTag()); + $reEncryptedPassword = openssl_encrypt($derivedKey, + self::ENCRYPTION_ALGORITHM, base64_decode($decrypted->getKey()), OPENSSL_RAW_DATA, + base64_decode($secured->getIv()), $encryptedTag + ); + + if ($reEncryptedPassword !== false && hash_equals($reEncryptedPassword, base64_decode($secured->getEncryptedPassword()))) + { + return true; + } + } + + return false; + } + } diff --git a/src/Socialbox/Classes/StandardMethods/Identify.php b/src/Socialbox/Classes/StandardMethods/Identify.php new file mode 100644 index 0000000..42d324d --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/Identify.php @@ -0,0 +1,73 @@ +containsParameter('username')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing parameter \'username\''); + } + + // Check if the username is valid + if(!Validator::validateUsername($rpcRequest->getParameter('username'))) + { + return $rpcRequest->produceError(StandardError::INVALID_USERNAME, StandardError::INVALID_USERNAME->getMessage()); + } + + // Check if the request has a Session UUID + if($request->getSessionUuid() === null) + { + return $rpcRequest->produceError(StandardError::SESSION_REQUIRED); + } + + try + { + // Get the session and check if it's already authenticated + $session = SessionManager::getSession($request->getSessionUuid()); + + // If the session is already authenticated, return an error + if($session->getPeerUuid() !== null) + { + return $rpcRequest->produceError(StandardError::ALREADY_AUTHENTICATED); + } + + // If the username does not exist, return an error + if(!RegisteredPeerManager::usernameExists($rpcRequest->getParameter('username'))) + { + return $rpcRequest->produceError(StandardError::NOT_REGISTERED, StandardError::NOT_REGISTERED->getMessage()); + } + + // Create session to be identified as the provided username + SessionManager::updatePeer($session->getUuid(), $rpcRequest->getParameter('username')); + + // Set the required session flags + $initialFlags = []; + } + catch(DatabaseOperationException $e) + { + throw new StandardException("There was an unexpected error while trying to register", StandardError::INTERNAL_SERVER_ERROR, $e); + } + + // Return true to indicate the operation was a success + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file diff --git a/src/Socialbox/Managers/EncryptionRecordsManager.php b/src/Socialbox/Managers/EncryptionRecordsManager.php new file mode 100644 index 0000000..3d2cc90 --- /dev/null +++ b/src/Socialbox/Managers/EncryptionRecordsManager.php @@ -0,0 +1,205 @@ +prepare('SELECT COUNT(*) FROM encryption_records'); + $stmt->execute(); + return $stmt->fetchColumn(); + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to retrieve encryption record count', $e); + } + } + + /** + * Inserts a new encryption record into the encryption_records table. + * + * @param EncryptionRecord $record The encryption record to insert, containing data, IV, and tag. + * @return void + * @throws DatabaseOperationException If the insertion into the database fails. + */ + private static function insertRecord(EncryptionRecord $record): void + { + try + { + $stmt = Database::getConnection()->prepare('INSERT INTO encryption_records (data, iv, tag) VALUES (?, ?, ?)'); + + $data = $record->getData(); + $stmt->bindParam(1, $data); + + $iv = $record->getIv(); + $stmt->bindParam(2, $iv); + + $tag = $record->getTag(); + $stmt->bindParam(3, $tag); + + $stmt->execute(); + } + catch(PDOException $e) + { + + throw new DatabaseOperationException('Failed to insert encryption record into the database', $e); + } + } + + /** + * Retrieves a random encryption record from the database. + * + * @return EncryptionRecord An instance of EncryptionRecord containing the data of a randomly selected record. + * @throws DatabaseOperationException If an error occurs while attempting to retrieve the record from the database. + */ + public static function getRandomRecord(): EncryptionRecord + { + try + { + $stmt = Database::getConnection()->prepare('SELECT * FROM encryption_records ORDER BY RAND() LIMIT 1'); + $stmt->execute(); + $data = $stmt->fetch(); + + return new EncryptionRecord($data); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to retrieve a random encryption record', $e); + } + } + + /** + * Retrieves all encryption records from the database. + * + * @return EncryptionRecord[] An array of EncryptionRecord instances, each representing a record from the database. + * @throws DatabaseOperationException If an error occurs while attempting to retrieve the records from the database. + */ + public static function getAllRecords(): array + { + try + { + $stmt = Database::getConnection()->prepare('SELECT * FROM encryption_records'); + $stmt->execute(); + $data = $stmt->fetchAll(); + + $records = []; + foreach ($data as $record) + { + $records[] = new EncryptionRecord($record); + } + + return $records; + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to retrieve all encryption records', $e); + } + } + + /** + * Generates encryption records and inserts them into the database until the specified total count is reached. + * + * @param int $count The total number of encryption records desired in the database. + * @return int The number of new records that were created and inserted. + * @throws CryptographyException + * @throws DatabaseOperationException + */ + public static function generateRecords(int $count): int + { + $currentCount = self::getRecordCount(); + if($currentCount >= $count) + { + return 0; + } + + $created = 0; + for($i = 0; $i < $count - $currentCount; $i++) + { + self::insertRecord(self::generateEncryptionRecord()); + $created++; + } + + return $created; + } + + /** + * Generates a new encryption record containing a key, pepper, and salt. + * + * @return EncryptionRecord An instance of EncryptionRecord containing an encrypted structure + * with the generated key, pepper, and salt. + * @throws CryptographyException If random byte generation fails during the creation of the encryption record. + */ + private static function generateEncryptionRecord(): EncryptionRecord + { + try + { + $key = random_bytes(self::KEY_LENGTH / 8); + $pepper = bin2hex(random_bytes(SecuredPassword::PEPPER_LENGTH / 2)); + $salt = bin2hex(random_bytes(self::KEY_LENGTH / 16)); + + } + catch (RandomException $e) + { + throw new CryptographyException("Random bytes generation failed", $e->getCode(), $e); + } + + return self::encrypt(['key' => base64_encode($key), 'pepper' => $pepper, 'salt' => $salt,]); + } + + /** + * Encrypts the given vault item and returns an EncryptionRecord containing the encrypted data. + * + * @param array $vaultItem The associative array representing the vault item to be encrypted. + * @return EncryptionRecord An instance of EncryptionRecord containing the encrypted vault data, initialization vector (IV), and authentication tag. + * @throws CryptographyException If the initialization vector generation or vault encryption process fails. + */ + private static function encrypt(array $vaultItem): EncryptionRecord + { + $serializedVault = json_encode($vaultItem); + + try + { + $iv = random_bytes(openssl_cipher_iv_length(SecuredPassword::ENCRYPTION_ALGORITHM)); + } + catch (RandomException $e) + { + throw new CryptographyException("IV generation failed", $e->getCode(), $e); + } + $tag = null; + + $encryptedVault = openssl_encrypt($serializedVault, SecuredPassword::ENCRYPTION_ALGORITHM, + Configuration::getInstanceConfiguration()->getRandomEncryptionKey(), OPENSSL_RAW_DATA, $iv, $tag + ); + + if ($encryptedVault === false) + { + throw new CryptographyException("Vault encryption failed"); + } + + return new EncryptionRecord([ + 'data' => base64_encode($encryptedVault), + 'iv' => base64_encode($iv), + 'tag' => base64_encode($tag), + ]); + } + } \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/DecryptedRecord.php b/src/Socialbox/Objects/Database/DecryptedRecord.php new file mode 100644 index 0000000..3757949 --- /dev/null +++ b/src/Socialbox/Objects/Database/DecryptedRecord.php @@ -0,0 +1,41 @@ +key = $data['key']; + $this->pepper = $data['pepper']; + $this->salt = $data['salt']; + } + + /** + * @return string + */ + public function getKey(): string + { + return $this->key; + } + + /** + * @return string + */ + public function getPepper(): string + { + return $this->pepper; + } + + /** + * @return string + */ + public function getSalt(): string + { + return $this->salt; + } + } \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/EncryptionRecord.php b/src/Socialbox/Objects/Database/EncryptionRecord.php new file mode 100644 index 0000000..e2aad9c --- /dev/null +++ b/src/Socialbox/Objects/Database/EncryptionRecord.php @@ -0,0 +1,84 @@ +data = $data['data']; + $this->iv = $data['iv']; + $this->tag = $data['tag']; + } + + /** + * Retrieves the stored data. + * + * @return string The stored data. + */ + public function getData(): string + { + return $this->data; + } + + /** + * Retrieves the initialization vector (IV). + * + * @return string The initialization vector. + */ + public function getIv(): string + { + return $this->iv; + } + + /** + * Retrieves the tag. + * + * @return string The tag. + */ + public function getTag(): string + { + return $this->tag; + } + + /** + * Decrypts the encrypted record using available encryption keys. + * + * Iterates through the configured encryption keys to attempt decryption of the data. + * If successful, returns a DecryptedRecord object with the decrypted data. + * Throws an exception if decryption fails with all available keys. + * + * @return DecryptedRecord The decrypted record containing the original data. + * @throws CryptographyException If decryption fails with all provided keys. + */ + public function decrypt(): DecryptedRecord + { + foreach(Configuration::getInstanceConfiguration()->getEncryptionKeys() as $encryptionKey) + { + $decryptedVault = openssl_decrypt(base64_decode($this->data), SecuredPassword::ENCRYPTION_ALGORITHM, + $encryptionKey, OPENSSL_RAW_DATA, base64_decode($this->iv), base64_decode($this->tag) + ); + + if ($decryptedVault !== false) + { + return new DecryptedRecord(json_decode($decryptedVault, true)); + } + } + + throw new CryptographyException("Decryption failed"); + } + } \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/SecurePasswordRecord.php b/src/Socialbox/Objects/Database/SecurePasswordRecord.php new file mode 100644 index 0000000..fb12350 --- /dev/null +++ b/src/Socialbox/Objects/Database/SecurePasswordRecord.php @@ -0,0 +1,84 @@ +peerUuid = $data['peer_uuid']; + $this->iv = $data['iv']; + $this->encryptedPassword = $data['encrypted_password']; + $this->encryptedTag = $data['encrypted_tag']; + $this->updated = new DateTime($data['updated']); + } + + /** + * Retrieves the UUID of the peer. + * + * @return string The UUID of the peer. + */ + public function getPeerUuid(): string + { + return $this->peerUuid; + } + + /** + * Retrieves the initialization vector (IV) value. + * + * @return string The initialization vector. + */ + public function getIv(): string + { + return $this->iv; + } + + /** + * Retrieves the encrypted password. + * + * @return string The encrypted password. + */ + public function getEncryptedPassword(): string + { + return $this->encryptedPassword; + } + + /** + * Retrieves the encrypted tag. + * + * @return string The encrypted tag. + */ + public function getEncryptedTag(): string + { + return $this->encryptedTag; + } + + /** + * Retrieves the updated timestamp. + * + * @return DateTime The updated timestamp. + */ + public function getUpdated(): DateTime + { + return $this->updated; + } + } \ No newline at end of file diff --git a/tests/Socialbox/Classes/SecuredPasswordTest.php b/tests/Socialbox/Classes/SecuredPasswordTest.php new file mode 100644 index 0000000..85de779 --- /dev/null +++ b/tests/Socialbox/Classes/SecuredPasswordTest.php @@ -0,0 +1,22 @@ +assertTrue(SecuredPassword::verifyPassword('password!', $securedPassword, EncryptionRecordsManager::getAllRecords())); + } + } diff --git a/tests/test.php b/tests/test.php new file mode 100644 index 0000000..3d3dd60 --- /dev/null +++ b/tests/test.php @@ -0,0 +1,24 @@ + Date: Thu, 12 Dec 2024 04:33:10 -0500 Subject: [PATCH 054/420] Remove unused StandardMethods and improve session logic --- examples/index.php | 2 +- src/Socialbox/Abstracts/Method.php | 24 +- src/Socialbox/Classes/Configuration.php | 2 +- src/Socialbox/Classes/Cryptography.php | 71 ++++ src/Socialbox/Classes/RpcHandler.php | 17 +- .../Classes/StandardMethods/Authenticate.php | 50 --- .../Classes/StandardMethods/CreateSession.php | 45 --- .../Classes/StandardMethods/GetMe.php | 47 --- .../Classes/StandardMethods/GetSession.php | 40 --- .../Classes/StandardMethods/Identify.php | 73 ---- .../Classes/StandardMethods/Ping.php | 29 +- .../Classes/StandardMethods/Register.php | 80 ----- .../VerificationAnswerImageCaptcha.php | 4 +- .../VerificationGetImageCaptcha.php | 4 +- src/Socialbox/Classes/Utilities.php | 1 + src/Socialbox/Enums/Flags/PeerFlags.php | 25 -- src/Socialbox/Enums/Flags/SessionFlags.php | 33 +- src/Socialbox/Enums/SessionState.php | 5 + src/Socialbox/Enums/StandardError.php | 22 +- src/Socialbox/Enums/StandardHeaders.php | 74 ++-- src/Socialbox/Enums/StandardMethods.php | 13 +- src/Socialbox/Enums/Types/RequestType.php | 21 ++ .../Exceptions/DatabaseOperationException.php | 18 +- src/Socialbox/Exceptions/RequestException.php | 21 ++ .../Managers/RegisteredPeerManager.php | 100 ++---- src/Socialbox/Managers/SessionManager.php | 106 +++++- src/Socialbox/Objects/ClientRequest.php | 323 ++++++++++++------ src/Socialbox/Objects/ClientRequestOld.php | 162 +++++++++ .../Objects/Database/SessionRecord.php | 17 +- src/Socialbox/Socialbox.php | 307 +++++++++++++++-- 30 files changed, 1032 insertions(+), 704 deletions(-) delete mode 100644 src/Socialbox/Classes/StandardMethods/Authenticate.php delete mode 100644 src/Socialbox/Classes/StandardMethods/CreateSession.php delete mode 100644 src/Socialbox/Classes/StandardMethods/GetMe.php delete mode 100644 src/Socialbox/Classes/StandardMethods/GetSession.php delete mode 100644 src/Socialbox/Classes/StandardMethods/Identify.php delete mode 100644 src/Socialbox/Classes/StandardMethods/Register.php create mode 100644 src/Socialbox/Enums/Types/RequestType.php create mode 100644 src/Socialbox/Exceptions/RequestException.php create mode 100644 src/Socialbox/Objects/ClientRequestOld.php diff --git a/examples/index.php b/examples/index.php index 875a454..fc2b206 100644 --- a/examples/index.php +++ b/examples/index.php @@ -5,7 +5,7 @@ try { - \Socialbox\Socialbox::handleRpc(); + \Socialbox\Socialbox::handleRequest(); } catch(Exception $e) { diff --git a/src/Socialbox/Abstracts/Method.php b/src/Socialbox/Abstracts/Method.php index d3f5971..beac6a0 100644 --- a/src/Socialbox/Abstracts/Method.php +++ b/src/Socialbox/Abstracts/Method.php @@ -8,6 +8,7 @@ use Socialbox\Exceptions\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\SessionManager; use Socialbox\Objects\ClientRequest; +use Socialbox\Objects\ClientRequestOld; use Socialbox\Objects\Database\SessionRecord; use Socialbox\Objects\RpcRequest; @@ -22,27 +23,4 @@ abstract class Method * @throws StandardException If a standard exception is thrown, it will be handled by the engine. */ public static abstract function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface; - - /** - * @param ClientRequest $request The client request object - * @return SessionRecord|null Returns null if the client has not provided a Session UUID - * @throws StandardException Thrown if standard exceptions are to be thrown regarding this - */ - protected static function getSession(ClientRequest $request): ?SessionRecord - { - if($request->getSessionUuid() === null) - { - return null; - } - - try - { - // NOTE: If the session UUID was provided, it has already been validated up until this point - return SessionManager::getSession($request->getSessionUuid()); - } - catch(DatabaseOperationException $e) - { - throw new StandardException("There was an error while retrieving the session from the server", StandardError::INTERNAL_SERVER_ERROR); - } - } } \ No newline at end of file diff --git a/src/Socialbox/Classes/Configuration.php b/src/Socialbox/Classes/Configuration.php index 7537424..2b0d315 100644 --- a/src/Socialbox/Classes/Configuration.php +++ b/src/Socialbox/Classes/Configuration.php @@ -35,7 +35,7 @@ class Configuration $config->setDefault('instance.domain', null); $config->setDefault('instance.rpc_endpoint', null); $config->setDefault('instance.encryption_keys_count', 5); - $config->setDefault('instance.encryption_record_count', 5); + $config->setDefault('instance.encryption_records_count', 5); $config->setDefault('instance.private_key', null); $config->setDefault('instance.public_key', null); $config->setDefault('instance.encryption_keys', null); diff --git a/src/Socialbox/Classes/Cryptography.php b/src/Socialbox/Classes/Cryptography.php index 28e0b17..19b17a0 100644 --- a/src/Socialbox/Classes/Cryptography.php +++ b/src/Socialbox/Classes/Cryptography.php @@ -16,6 +16,7 @@ class Cryptography private const int PADDING = OPENSSL_PKCS1_OAEP_PADDING; private const string PEM_PRIVATE_HEADER = 'PRIVATE'; private const string PEM_PUBLIC_HEADER = 'PUBLIC'; + private const string TRANSPORT_ENCRYPTION = 'aes-256-cbc'; /** * Generates a new public-private key pair. @@ -307,4 +308,74 @@ class Cryptography return $keys; } + + public static function generateEncryptionKey(): string + { + try + { + return base64_encode(random_bytes(32)); + } + catch (RandomException $e) + { + throw new CryptographyException('Failed to generate encryption key: ' . $e->getMessage()); + } + } + + /** + * Encrypts the given content for transport using the provided encryption key. + * + * @param string $content The content to be encrypted. + * @param string $encryptionKey The encryption key used for encrypting the content. + * @return string The Base64 encoded string containing the IV and the encrypted content. + * @throws CryptographyException If the IV generation or encryption process fails. + */ + public static function encryptTransport(string $content, string $encryptionKey): string + { + try + { + $iv = random_bytes(openssl_cipher_iv_length('aes-256-cbc')); + } + catch (RandomException $e) + { + throw new CryptographyException('Failed to generate IV: ' . $e->getMessage()); + } + + $encrypted = openssl_encrypt($content, self::TRANSPORT_ENCRYPTION, base64_decode($encryptionKey), OPENSSL_RAW_DATA, $iv); + + if($encrypted === false) + { + throw new CryptographyException('Failed to encrypt transport content: ' . openssl_error_string()); + } + + return base64_encode($iv . $encrypted); + } + + /** + * Decrypts the given encrypted transport content using the provided encryption key. + * + * @param string $encryptedContent The Base64 encoded encrypted content to be decrypted. + * @param string $encryptionKey The Base64 encoded encryption key used for decryption. + * @return string The decrypted content as a string. + * @throws CryptographyException If the decryption process fails. + */ + public static function decryptTransport(string $encryptedContent, string $encryptionKey): string + { + $decodedData = base64_decode($encryptedContent); + $ivLength = openssl_cipher_iv_length(self::TRANSPORT_ENCRYPTION); + + // Perform decryption + $decryption = openssl_decrypt(substr($decodedData, $ivLength), + self::TRANSPORT_ENCRYPTION, + base64_decode($encryptionKey), + OPENSSL_RAW_DATA, + substr($decodedData, 0, $ivLength) + ); + + if($decryption === false) + { + throw new CryptographyException('Failed to decrypt transport content: ' . openssl_error_string()); + } + + return $decryption; + } } \ No newline at end of file diff --git a/src/Socialbox/Classes/RpcHandler.php b/src/Socialbox/Classes/RpcHandler.php index 458a57c..e64268b 100644 --- a/src/Socialbox/Classes/RpcHandler.php +++ b/src/Socialbox/Classes/RpcHandler.php @@ -7,10 +7,11 @@ use InvalidArgumentException; use RuntimeException; use Socialbox\Enums\StandardHeaders; use Socialbox\Exceptions\DatabaseOperationException; +use Socialbox\Exceptions\RequestException; use Socialbox\Exceptions\RpcException; use Socialbox\Exceptions\StandardException; use Socialbox\Managers\SessionManager; -use Socialbox\Objects\ClientRequest; +use Socialbox\Objects\ClientRequestOld; use Socialbox\Objects\RpcRequest; class RpcHandler @@ -19,16 +20,12 @@ class RpcHandler * Gets the incoming ClientRequest object, validates if the request is valid & if a session UUID is provided * checks if the request signature matches the client's provided public key. * - * @return ClientRequest The parsed ClientRequest object + * @return ClientRequestOld The parsed ClientRequest object + * @throws RequestException * @throws RpcException Thrown if the request is invalid */ - public static function getClientRequest(): ClientRequest + public static function getClientRequest(): ClientRequestOld { - if($_SERVER['REQUEST_METHOD'] !== 'POST') - { - throw new RpcException('Invalid Request Method, expected POST', 400); - } - try { $headers = Utilities::getRequestHeaders(); @@ -36,7 +33,7 @@ class RpcHandler { if (!isset($headers[$header])) { - throw new RpcException("Missing required header: $header", 400); + throw new RequestException("Missing required header: $header", 400); } // Validate the headers @@ -73,7 +70,7 @@ class RpcHandler throw new RpcException("Failed to parse request: " . $e->getMessage(), 400, $e); } - $clientRequest = new ClientRequest($headers, self::getRpcRequests(), self::getRequestHash()); + $clientRequest = new ClientRequestOld($headers, self::getRpcRequests(), self::getRequestHash()); // Verify the session & request signature if($clientRequest->getSessionUuid() !== null) diff --git a/src/Socialbox/Classes/StandardMethods/Authenticate.php b/src/Socialbox/Classes/StandardMethods/Authenticate.php deleted file mode 100644 index b3eaf19..0000000 --- a/src/Socialbox/Classes/StandardMethods/Authenticate.php +++ /dev/null @@ -1,50 +0,0 @@ -getParameters()['type'])) - { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing required parameter \'type\''); - } - - if(strlen($rpcRequest->getParameters()['type']) == 0) - { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Parameter \'type\' cannot be empty'); - } - - return match (FirstLevelAuthentication::tryFrom($rpcRequest->getParameters()['type'])) - { - FirstLevelAuthentication::PASSWORD => self::handlePassword($request), - - default => $rpcRequest->produceError(StandardError::UNSUPPORTED_AUTHENTICATION_TYPE, - sprintf('Unsupported authentication type: %s', $rpcRequest->getParameters()['type']) - ), - }; - } - - /** - * Handles the password authentication phase for the peer - * - * @param ClientRequest $request - * @return SerializableInterface - */ - private static function handlePassword(ClientRequest $request): SerializableInterface - { - - } -} \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/CreateSession.php b/src/Socialbox/Classes/StandardMethods/CreateSession.php deleted file mode 100644 index 9b11360..0000000 --- a/src/Socialbox/Classes/StandardMethods/CreateSession.php +++ /dev/null @@ -1,45 +0,0 @@ -containsParameter('public_key')) - { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing parameter \'public_key\''); - } - - try - { - $uuid = SessionManager::createSession($rpcRequest->getParameter('public_key')); - } - catch(DatabaseOperationException $e) - { - return $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, 'There was an error while trying to create a new session: ' . $e->getMessage()); - } - catch(InvalidArgumentException $e) - { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, $e->getMessage()); - } - - return $rpcRequest->produceResponse($uuid); - } -} \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/GetMe.php b/src/Socialbox/Classes/StandardMethods/GetMe.php deleted file mode 100644 index fbd9826..0000000 --- a/src/Socialbox/Classes/StandardMethods/GetMe.php +++ /dev/null @@ -1,47 +0,0 @@ -getSessionUuid() === null) - { - return $rpcRequest->produceError(StandardError::SESSION_REQUIRED); - } - - try - { - // Get the session and check if it's already authenticated - $session = SessionManager::getSession($request->getSessionUuid()); - if($session->getPeerUuid() === null) - { - return $rpcRequest->produceError(StandardError::AUTHENTICATION_REQUIRED); - } - - // Get the peer and return it - return $rpcRequest->produceResponse(RegisteredPeerManager::getPeer($session->getPeerUuid())->toSelfUser()); - } - catch(DatabaseOperationException $e) - { - throw new StandardException("There was an unexpected error while trying to register", StandardError::INTERNAL_SERVER_ERROR, $e); - } - } -} \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/GetSession.php b/src/Socialbox/Classes/StandardMethods/GetSession.php deleted file mode 100644 index f0035ae..0000000 --- a/src/Socialbox/Classes/StandardMethods/GetSession.php +++ /dev/null @@ -1,40 +0,0 @@ -getSessionUuid() === null) - { - return $rpcRequest->produceError(StandardError::SESSION_REQUIRED); - } - - try - { - // Get the session - $session = SessionManager::getSession($request->getSessionUuid()); - } - catch(DatabaseOperationException $e) - { - throw new StandardException("There was an unexpected error while trying to retrieve the session", StandardError::INTERNAL_SERVER_ERROR, $e); - } - - - } - } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/Identify.php b/src/Socialbox/Classes/StandardMethods/Identify.php deleted file mode 100644 index 42d324d..0000000 --- a/src/Socialbox/Classes/StandardMethods/Identify.php +++ /dev/null @@ -1,73 +0,0 @@ -containsParameter('username')) - { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing parameter \'username\''); - } - - // Check if the username is valid - if(!Validator::validateUsername($rpcRequest->getParameter('username'))) - { - return $rpcRequest->produceError(StandardError::INVALID_USERNAME, StandardError::INVALID_USERNAME->getMessage()); - } - - // Check if the request has a Session UUID - if($request->getSessionUuid() === null) - { - return $rpcRequest->produceError(StandardError::SESSION_REQUIRED); - } - - try - { - // Get the session and check if it's already authenticated - $session = SessionManager::getSession($request->getSessionUuid()); - - // If the session is already authenticated, return an error - if($session->getPeerUuid() !== null) - { - return $rpcRequest->produceError(StandardError::ALREADY_AUTHENTICATED); - } - - // If the username does not exist, return an error - if(!RegisteredPeerManager::usernameExists($rpcRequest->getParameter('username'))) - { - return $rpcRequest->produceError(StandardError::NOT_REGISTERED, StandardError::NOT_REGISTERED->getMessage()); - } - - // Create session to be identified as the provided username - SessionManager::updatePeer($session->getUuid(), $rpcRequest->getParameter('username')); - - // Set the required session flags - $initialFlags = []; - } - catch(DatabaseOperationException $e) - { - throw new StandardException("There was an unexpected error while trying to register", StandardError::INTERNAL_SERVER_ERROR, $e); - } - - // Return true to indicate the operation was a success - return $rpcRequest->produceResponse(true); - } - } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/Ping.php b/src/Socialbox/Classes/StandardMethods/Ping.php index c0b8007..93724a9 100644 --- a/src/Socialbox/Classes/StandardMethods/Ping.php +++ b/src/Socialbox/Classes/StandardMethods/Ping.php @@ -1,19 +1,20 @@ produceResponse(true); - } -} \ No newline at end of file + /** + * @inheritDoc + */ + public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface + { + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/Register.php b/src/Socialbox/Classes/StandardMethods/Register.php deleted file mode 100644 index 6bedc2d..0000000 --- a/src/Socialbox/Classes/StandardMethods/Register.php +++ /dev/null @@ -1,80 +0,0 @@ -isRegistrationEnabled()) - { - return $rpcRequest->produceError(StandardError::REGISTRATION_DISABLED, StandardError::REGISTRATION_DISABLED->getMessage()); - } - - // Check if the username parameter exists - if(!$rpcRequest->containsParameter('username')) - { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing parameter \'username\''); - } - - // Check if the username is valid - if(!Validator::validateUsername($rpcRequest->getParameter('username'))) - { - return $rpcRequest->produceError(StandardError::INVALID_USERNAME, StandardError::INVALID_USERNAME->getMessage()); - } - - // Check if the username exists already - try - { - if (RegisteredPeerManager::usernameExists($rpcRequest->getParameter('username'))) - { - return $rpcRequest->produceError(StandardError::USERNAME_ALREADY_EXISTS, StandardError::USERNAME_ALREADY_EXISTS->getMessage()); - } - } - catch (DatabaseOperationException $e) - { - throw new StandardException("There was an unexpected error while trying to check the username existence", StandardError::INTERNAL_SERVER_ERROR, $e); - } - - // Check if the request has a Session UUID - if($request->getSessionUuid() === null) - { - return $rpcRequest->produceError(StandardError::SESSION_REQUIRED); - } - - try - { - // Get the session and check if it's already authenticated - $session = SessionManager::getSession($request->getSessionUuid()); - if($session->getPeerUuid() !== null) - { - return $rpcRequest->produceError(StandardError::ALREADY_AUTHENTICATED); - } - - // Create the peer & set the current's session authenticated peer as the newly created peer - SessionManager::updatePeer($session->getUuid(), RegisteredPeerManager::createPeer($rpcRequest->getParameter('username'))); - } - catch(DatabaseOperationException $e) - { - throw new StandardException("There was an unexpected error while trying to register", StandardError::INTERNAL_SERVER_ERROR, $e); - } - - // Return true to indicate the operation was a success - return $rpcRequest->produceResponse(true); - } -} \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php b/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php index 5cb8524..3d7f5fb 100644 --- a/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php +++ b/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php @@ -11,7 +11,7 @@ use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\CaptchaManager; use Socialbox\Managers\RegisteredPeerManager; use Socialbox\Managers\SessionManager; -use Socialbox\Objects\ClientRequest; +use Socialbox\Objects\ClientRequestOld; use Socialbox\Objects\RpcRequest; class VerificationAnswerImageCaptcha extends Method @@ -20,7 +20,7 @@ class VerificationAnswerImageCaptcha extends Method /** * @inheritDoc */ - public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface + public static function execute(ClientRequestOld $request, RpcRequest $rpcRequest): ?SerializableInterface { // Check if the request has a Session UUID if($request->getSessionUuid() === null) diff --git a/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php b/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php index 681e366..1956706 100644 --- a/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php +++ b/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php @@ -13,7 +13,7 @@ use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\CaptchaManager; use Socialbox\Managers\RegisteredPeerManager; use Socialbox\Managers\SessionManager; -use Socialbox\Objects\ClientRequest; +use Socialbox\Objects\ClientRequestOld; use Socialbox\Objects\RpcRequest; use Socialbox\Objects\Standard\ImageCaptcha; @@ -22,7 +22,7 @@ class VerificationGetImageCaptcha extends Method /** * @inheritDoc */ - public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface + public static function execute(ClientRequestOld $request, RpcRequest $rpcRequest): ?SerializableInterface { // Check if the request has a Session UUID if($request->getSessionUuid() === null) diff --git a/src/Socialbox/Classes/Utilities.php b/src/Socialbox/Classes/Utilities.php index a935c76..efa786b 100644 --- a/src/Socialbox/Classes/Utilities.php +++ b/src/Socialbox/Classes/Utilities.php @@ -7,6 +7,7 @@ use InvalidArgumentException; use JsonException; use RuntimeException; use Socialbox\Enums\StandardHeaders; +use Socialbox\Objects\PeerAddress; use Throwable; class Utilities diff --git a/src/Socialbox/Enums/Flags/PeerFlags.php b/src/Socialbox/Enums/Flags/PeerFlags.php index 86b69bb..1ca08b2 100644 --- a/src/Socialbox/Enums/Flags/PeerFlags.php +++ b/src/Socialbox/Enums/Flags/PeerFlags.php @@ -13,15 +13,6 @@ enum PeerFlags : string // General Flags case VERIFIED = 'VERIFIED'; - // Verification Flags - case VER_SET_PASSWORD = 'VER_SET_PASSWORD'; - case VER_SET_OTP = 'VER_SET_OTP'; - case VER_SET_DISPLAY_NAME = 'VER_SET_DISPLAY_NAME'; - case VER_EMAIL = 'VER_EMAIL'; - case VER_SMS = 'VER_SMS'; - case VER_PHONE_CALL = 'VER_PHONE_CALL'; - case VER_SOLVE_IMAGE_CAPTCHA = 'VER_SOLVE_IMAGE_CAPTCHA'; - /** * Converts an array of PeerFlags enums to a string representation * @@ -48,20 +39,4 @@ enum PeerFlags : string return array_map(fn(string $value) => PeerFlags::from(trim($value)), explode(',', $flagString)); } - - /** - * Returns whether the flag is public. Public flags can be seen by other peers. - * - * @return bool - */ - public function isPublic(): bool - { - return match($this) - { - self::VER_SET_PASSWORD, - self::VER_SET_OTP, - self::VER_SOLVE_IMAGE_CAPTCHA => false, - default => true, - }; - } } diff --git a/src/Socialbox/Enums/Flags/SessionFlags.php b/src/Socialbox/Enums/Flags/SessionFlags.php index 821c514..15be9a7 100644 --- a/src/Socialbox/Enums/Flags/SessionFlags.php +++ b/src/Socialbox/Enums/Flags/SessionFlags.php @@ -5,9 +5,9 @@ enum SessionFlags : string { // Verification, require fields - case VER_SET_PASSWORD = 'VER_SET_PASSWORD'; // Peer has to set a password - case VER_SET_OTP = 'VER_SET_OTP'; // Peer has to set an OTP - case VER_SET_DISPLAY_NAME = 'VER_SET_DISPLAY_NAME'; // Peer has to set a display name + case SET_PASSWORD = 'SET_PASSWORD'; // Peer has to set a password + case SET_OTP = 'SET_OTP'; // Peer has to set an OTP + case SET_DISPLAY_NAME = 'SET_DISPLAY_NAME'; // Peer has to set a display name // Verification, verification requirements case VER_EMAIL = 'VER_EMAIL'; // Peer has to verify their email @@ -18,4 +18,31 @@ // Login, require fields case VER_PASSWORD = 'VER_PASSWORD'; // Peer has to enter their password case VER_OTP = 'VER_OTP'; // Peer has to enter their OTP + + /** + * Converts an array of SessionFlags to a comma-separated string of their values. + * + * @param array $flags An array of SessionFlags objects to be converted. + * @return string A comma-separated string of the values of the provided SessionFlags. + */ + public static function toString(array $flags): string + { + return implode(',', array_map(fn(SessionFlags $flag) => $flag->value, $flags)); + } + + /** + * Converts a comma-separated string of flag values into an array of SessionFlags objects. + * + * @param string $flagString A comma-separated string representing flag values. + * @return array An array of SessionFlags objects created from the provided string. + */ + public static function fromString(string $flagString): array + { + if (empty($flagString)) + { + return []; + } + + return array_map(fn(string $value) => SessionFlags::from(trim($value)), explode(',', $flagString)); + } } diff --git a/src/Socialbox/Enums/SessionState.php b/src/Socialbox/Enums/SessionState.php index f277d2b..21aff42 100644 --- a/src/Socialbox/Enums/SessionState.php +++ b/src/Socialbox/Enums/SessionState.php @@ -4,6 +4,11 @@ namespace Socialbox\Enums; enum SessionState : string { + /** + * The session is awaiting a Diffie-Hellman exchange to be completed + */ + case AWAITING_DHE = 'AWAITING_DHE'; + /** * The session is currently active and usable */ diff --git a/src/Socialbox/Enums/StandardError.php b/src/Socialbox/Enums/StandardError.php index ad0c31c..7f80aaf 100644 --- a/src/Socialbox/Enums/StandardError.php +++ b/src/Socialbox/Enums/StandardError.php @@ -17,15 +17,19 @@ enum StandardError : int // Authentication/Cryptography Errors case INVALID_PUBLIC_KEY = -3000; - case UNSUPPORTED_AUTHENTICATION_TYPE = -3001; - case ALREADY_AUTHENTICATED = -3002; - case AUTHENTICATION_REQUIRED = -3003; - case SESSION_NOT_FOUND = -3004; - case SESSION_REQUIRED = -3005; - case REGISTRATION_DISABLED = -3006; - case CAPTCHA_NOT_AVAILABLE = -3007; - case INCORRECT_CAPTCHA_ANSWER = -3008; - case CAPTCHA_EXPIRED = -3009; + + case SESSION_REQUIRED = -3001; + case SESSION_NOT_FOUND = -3002; + case SESSION_EXPIRED = -3003; + case SESSION_DHE_REQUIRED = -3004; + + case ALREADY_AUTHENTICATED = -3005; + case UNSUPPORTED_AUTHENTICATION_TYPE = -3006; + case AUTHENTICATION_REQUIRED = -3007; + case REGISTRATION_DISABLED = -3008; + case CAPTCHA_NOT_AVAILABLE = -3009; + case INCORRECT_CAPTCHA_ANSWER = -3010; + case CAPTCHA_EXPIRED = -3011; // General Error Messages case PEER_NOT_FOUND = -4000; diff --git a/src/Socialbox/Enums/StandardHeaders.php b/src/Socialbox/Enums/StandardHeaders.php index 9d707de..931280d 100644 --- a/src/Socialbox/Enums/StandardHeaders.php +++ b/src/Socialbox/Enums/StandardHeaders.php @@ -1,68 +1,32 @@ true, + case REQUEST_TYPE = 'Request-Type'; + case IDENTIFY_AS = 'Identify-As'; + case CLIENT_NAME = 'Client-Name'; + case CLIENT_VERSION = 'Client-Version'; + case PUBLIC_KEY = 'Public-Key'; - default => false, - }; - } + case SESSION_UUID = 'Session-UUID'; + case SIGNATURE = 'Signature'; - /** - * Retrieves an array of required headers. - * - * @return array An array containing only the headers that are marked as required. - */ - public static function getRequiredHeaders(): array - { - $results = []; - foreach(StandardHeaders::cases() as $header) + /** + * @return array + */ + public static function toArray(): array { - if($header->isRequired()) + $results = []; + foreach(StandardHeaders::cases() as $header) { $results[] = $header->value; } + + return $results; } - - return $results; - } - - /** - * @return array - */ - public static function toArray(): array - { - $results = []; - foreach(StandardHeaders::cases() as $header) - { - $results[] = $header->value; - } - - return $results; - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index b454ec2..a606815 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -12,17 +12,12 @@ use Socialbox\Classes\StandardMethods\Register; use Socialbox\Exceptions\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\ClientRequest; +use Socialbox\Objects\ClientRequestOld; use Socialbox\Objects\RpcRequest; enum StandardMethods : string { case PING = 'ping'; - case CREATE_SESSION = 'createSession'; - case REGISTER = 'register'; - case IDENTIFY = 'identify'; - case GET_ME = 'getMe'; - case VERIFICATION_GET_IMAGE_CAPTCHA = 'verificationGetImageCaptcha'; - case VERIFICATION_ANSWER_IMAGE_CAPTCHA = 'verificationAnswerImageCaptcha'; /** * @param ClientRequest $request @@ -35,12 +30,6 @@ enum StandardMethods : string return match ($this) { self::PING => Ping::execute($request, $rpcRequest), - self::CREATE_SESSION => CreateSession::execute($request, $rpcRequest), - self::REGISTER => Register::execute($request, $rpcRequest), - self::IDENTIFY => Identify::execute($request, $rpcRequest), - self::GET_ME => GetMe::execute($request, $rpcRequest), - self::VERIFICATION_GET_IMAGE_CAPTCHA => VerificationGetImageCaptcha::execute($request, $rpcRequest), - self::VERIFICATION_ANSWER_IMAGE_CAPTCHA => VerificationAnswerImageCaptcha::execute($request, $rpcRequest), }; } } \ No newline at end of file diff --git a/src/Socialbox/Enums/Types/RequestType.php b/src/Socialbox/Enums/Types/RequestType.php new file mode 100644 index 0000000..0beef55 --- /dev/null +++ b/src/Socialbox/Enums/Types/RequestType.php @@ -0,0 +1,21 @@ +verbose(sprintf("Creating a new peer with username %s", $username)); $uuid = Uuid::v4()->toRfc4122(); - // If `enabled` is True, we insert the peer into the database as an activated account. - if($enabled) - { - try - { - $statement = Database::getConnection()->prepare('INSERT INTO `registered_peers` (uuid, username, enabled) VALUES (?, ?, ?)'); - $statement->bindParam(1, $uuid); - $statement->bindParam(2, $username); - $statement->bindParam(3, $enabled, PDO::PARAM_BOOL); - $statement->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to create the peer in the database', $e); - } - - return $uuid; - } - - // Otherwise, we insert the peer into the database as a disabled account & the required verification flags. - $flags = []; - - if(Configuration::getRegistrationConfiguration()->isPasswordRequired()) - { - $flags[] = PeerFlags::VER_SET_PASSWORD; - } - - if(Configuration::getRegistrationConfiguration()->isOtpRequired()) - { - $flags[] = PeerFlags::VER_SET_OTP; - } - - if(Configuration::getRegistrationConfiguration()->isDisplayNameRequired()) - { - $flags[] = PeerFlags::VER_SET_DISPLAY_NAME; - } - - if(Configuration::getRegistrationConfiguration()->isEmailVerificationRequired()) - { - $flags[] = PeerFlags::VER_EMAIL; - } - - if(Configuration::getRegistrationConfiguration()->isSmsVerificationRequired()) - { - $flags[] = PeerFlags::VER_SMS; - } - - if(Configuration::getRegistrationConfiguration()->isPhoneCallVerificationRequired()) - { - $flags[] = PeerFlags::VER_PHONE_CALL; - } - - if(Configuration::getRegistrationConfiguration()->isImageCaptchaVerificationRequired()) - { - $flags[] = PeerFlags::VER_SOLVE_IMAGE_CAPTCHA; - } - try { - $implodedFlags = implode(',', array_map(fn($flag) => $flag->name, $flags)); - $statement = Database::getConnection()->prepare('INSERT INTO `registered_peers` (uuid, username, enabled, flags) VALUES (?, ?, ?, ?)'); + $statement = Database::getConnection()->prepare('INSERT INTO `registered_peers` (uuid, username, enabled) VALUES (?, ?, ?)'); $statement->bindParam(1, $uuid); $statement->bindParam(2, $username); $statement->bindParam(3, $enabled, PDO::PARAM_BOOL); - $statement->bindParam(4, $implodedFlags); $statement->execute(); } catch(PDOException $e) @@ -200,11 +142,10 @@ class RegisteredPeerManager * Retrieves a peer record by the given username. * * @param string $username The username of the peer to be retrieved. - * @return RegisteredPeerRecord The record of the peer associated with the given username. + * @return RegisteredPeerRecord|null The record of the peer associated with the given username. * @throws DatabaseOperationException If there is an error while querying the database. - * @throws StandardException If the peer does not exist. */ - public static function getPeerByUsername(string $username): RegisteredPeerRecord + public static function getPeerByUsername(string $username): ?RegisteredPeerRecord { Logger::getLogger()->verbose(sprintf("Retrieving peer %s from the database", $username)); @@ -218,7 +159,7 @@ class RegisteredPeerManager if($result === false) { - throw new StandardException(sprintf("The requested peer '%s' does not exist", $username), StandardError::PEER_NOT_FOUND); + return null; } return new RegisteredPeerRecord($result); @@ -365,4 +306,35 @@ class RegisteredPeerManager throw new DatabaseOperationException('Failed to remove the flag from the peer in the database', $e); } } + + /** + * + */ + public static function getPasswordAuthentication(string|RegisteredPeerRecord $peerUuid): ?SecurePasswordRecord + { + if($peerUuid instanceof RegisteredPeerRecord) + { + $peerUuid = $peerUuid->getUuid(); + } + + try + { + $statement = Database::getConnection()->prepare('SELECT * FROM `authentication_passwords` WHERE peer_uuid=?'); + $statement->bindParam(1, $peerUuid); + $statement->execute(); + + $result = $statement->fetch(PDO::FETCH_ASSOC); + + if($result === false) + { + return null; + } + + return new SecurePasswordRecord($result); + } + catch(PDOException | \DateMalformedStringException $e) + { + throw new DatabaseOperationException('Failed to get the secure password record from the database', $e); + } + } } \ No newline at end of file diff --git a/src/Socialbox/Managers/SessionManager.php b/src/Socialbox/Managers/SessionManager.php index f7565da..d2c2c23 100644 --- a/src/Socialbox/Managers/SessionManager.php +++ b/src/Socialbox/Managers/SessionManager.php @@ -7,10 +7,12 @@ use InvalidArgumentException; use PDO; use PDOException; + use Socialbox\Classes\Configuration; use Socialbox\Classes\Cryptography; use Socialbox\Classes\Database; use Socialbox\Classes\Logger; use Socialbox\Classes\Utilities; + use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\SessionState; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; @@ -31,25 +33,89 @@ * @throws InvalidArgumentException If the public key is empty or invalid. * @throws DatabaseOperationException If there is an error while creating the session in the database. */ - public static function createSession(string $publicKey): string + public static function createSession(string $publicKey, RegisteredPeerRecord $peer): string { if($publicKey === '') { - throw new InvalidArgumentException('The public key cannot be empty', 400); + throw new InvalidArgumentException('The public key cannot be empty'); } if(!Cryptography::validatePublicKey($publicKey)) { - throw new InvalidArgumentException('The given public key is invalid', 400); + throw new InvalidArgumentException('The given public key is invalid'); } $uuid = Uuid::v4()->toRfc4122(); + $flags = []; + + if($peer->isEnabled()) + { + if(RegisteredPeerManager::getPasswordAuthentication($peer)) + { + $flags[] = SessionFlags::VER_PASSWORD; + } + + if(Configuration::getRegistrationConfiguration()->isImageCaptchaVerificationRequired()) + { + $flags[] = SessionFlags::VER_IMAGE_CAPTCHA; + } + } + else + { + if(Configuration::getRegistrationConfiguration()->isDisplayNameRequired()) + { + $flags[] = SessionFlags::SET_DISPLAY_NAME; + } + + if(Configuration::getRegistrationConfiguration()->isEmailVerificationRequired()) + { + $flags[] = SessionFlags::VER_EMAIL; + } + + if(Configuration::getRegistrationConfiguration()->isSmsVerificationRequired()) + { + $flags[] = SessionFlags::VER_SMS; + } + + if(Configuration::getRegistrationConfiguration()->isPhoneCallVerificationRequired()) + { + $flags[] = SessionFlags::VER_PHONE_CALL; + } + + if(Configuration::getRegistrationConfiguration()->isImageCaptchaVerificationRequired()) + { + $flags[] = SessionFlags::VER_IMAGE_CAPTCHA; + } + + if(Configuration::getRegistrationConfiguration()->isPasswordRequired()) + { + $flags[] = SessionFlags::SET_PASSWORD; + } + + if(Configuration::getRegistrationConfiguration()->isOtpRequired()) + { + $flags[] = SessionFlags::SET_OTP; + } + } + + if(count($flags) > 0) + { + $implodedFlags = SessionFlags::toString($flags); + } + else + { + $implodedFlags = null; + } + + $peerUuid = $peer->getUuid(); try { - $statement = Database::getConnection()->prepare("INSERT INTO sessions (uuid, public_key) VALUES (?, ?)"); + $statement = Database::getConnection()->prepare("INSERT INTO sessions (uuid, peer_uuid, public_key, flags) VALUES (?, ?, ?, ?)"); $statement->bindParam(1, $uuid); - $statement->bindParam(2, $publicKey); + $statement->bindParam(2, $peerUuid); + $statement->bindParam(3, $publicKey); + $statement->bindParam(4, $implodedFlags); $statement->execute(); } catch(PDOException $e) @@ -219,6 +285,8 @@ $statement = Database::getConnection()->prepare('UPDATE sessions SET state=? WHERE uuid=?'); $statement->bindParam(1, $state_value); $statement->bindParam(2, $uuid); + + $statement->execute(); } catch(PDOException $e) { @@ -226,6 +294,34 @@ } } + /** + * Updates the encryption key for the specified session. + * + * @param string $uuid The unique identifier of the session for which the encryption key is to be set. + * @param string $encryptionKey The new encryption key to be assigned. + * @return void + * @throws DatabaseOperationException If the database operation fails. + */ + public static function setEncryptionKey(string $uuid, string $encryptionKey): void + { + Logger::getLogger()->verbose(sprintf('Setting the encryption key for %s', $uuid)); + + try + { + $state_value = SessionState::ACTIVE->value; + $statement = Database::getConnection()->prepare('UPDATE sessions SET state=?, encryption_key=? WHERE uuid=?'); + $statement->bindParam(1, $state_value); + $statement->bindParam(2, $encryptionKey); + $statement->bindParam(3, $uuid); + + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to set the encryption key', $e); + } + } + /** * Retrieves the flags associated with a specific session. * diff --git a/src/Socialbox/Objects/ClientRequest.php b/src/Socialbox/Objects/ClientRequest.php index ecb942a..5084b5f 100644 --- a/src/Socialbox/Objects/ClientRequest.php +++ b/src/Socialbox/Objects/ClientRequest.php @@ -1,142 +1,245 @@ headers = $headers; - $this->requests = $requests; - $this->requestHash = $requestHash; - } + private array $headers; + private RequestType $requestType; + private ?string $requestBody; - /** - * @return array - */ - public function getHeaders(): array - { - return $this->headers; - } + private string $clientName; + private string $clientVersion; + private ?string $identifyAs; + private ?string $sessionUuid; + private ?string $signature; - /** - * @return RpcRequest[] - */ - public function getRequests(): array - { - return $this->requests; - } - - public function getHash(): string - { - return $this->requestHash; - } - - public function getClientName(): string - { - return $this->headers[StandardHeaders::CLIENT_NAME->value]; - } - - public function getClientVersion(): string - { - return $this->headers[StandardHeaders::CLIENT_VERSION->value]; - } - - public function getSessionUuid(): ?string - { - if(!isset($this->headers[StandardHeaders::SESSION_UUID->value])) + public function __construct(array $headers, ?string $requestBody) { - return null; + $this->headers = $headers; + $this->requestBody = $requestBody; + + $this->clientName = $headers[StandardHeaders::CLIENT_NAME->value]; + $this->clientVersion = $headers[StandardHeaders::CLIENT_VERSION->value]; + $this->requestType = RequestType::from($headers[StandardHeaders::REQUEST_TYPE->value]); + $this->identifyAs = $headers[StandardHeaders::IDENTIFY_AS->value] ?? null; + $this->sessionUuid = $headers[StandardHeaders::SESSION_UUID->value] ?? null; + $this->signature = $headers[StandardHeaders::SIGNATURE->value] ?? null; } - return $this->headers[StandardHeaders::SESSION_UUID->value]; - } - - public function getFromPeer(): ?PeerAddress - { - if(!isset($this->headers[StandardHeaders::FROM_PEER->value])) + public function getHeaders(): array { - return null; + return $this->headers; } - return PeerAddress::fromAddress($this->headers[StandardHeaders::FROM_PEER->value]); - } - - public function getSignature(): ?string - { - if(!isset($this->headers[StandardHeaders::SIGNATURE->value])) + public function headerExists(StandardHeaders|string $header): bool { - return null; + if(is_string($header)) + { + return isset($this->headers[$header]); + } + + return isset($this->headers[$header->value]); } - return $this->headers[StandardHeaders::SIGNATURE->value]; - } - - /** - * @return bool - * @throws DatabaseOperationException - */ - public function verifySignature(): bool - { - $signature = $this->getSignature(); - $sessionUuid = $this->getSessionUuid(); - - if($signature == null || $sessionUuid == null) + public function getHeader(StandardHeaders|string $header): ?string { - return false; + if(!$this->headerExists($header)) + { + return null; + } + + if(is_string($header)) + { + return $this->headers[$header]; + } + + return $this->headers[$header->value]; } - try + public function getRequestBody(): ?string { - $session = SessionManager::getSession($sessionUuid); + return $this->requestBody; } - catch(StandardException $e) + + public function getClientName(): string { - if($e->getStandardError() == StandardError::SESSION_NOT_FOUND) + return $this->clientName; + } + + public function getClientVersion(): string + { + return $this->clientVersion; + } + + public function getRequestType(): RequestType + { + return $this->requestType; + } + + public function getIdentifyAs(): ?PeerAddress + { + if($this->identifyAs === null) + { + return null; + } + + return PeerAddress::fromAddress($this->identifyAs); + } + + public function getSessionUuid(): ?string + { + return $this->sessionUuid; + } + + public function getSession(): ?SessionRecord + { + if($this->sessionUuid === null) + { + return null; + } + + return SessionManager::getSession($this->sessionUuid); + } + + public function getSignature(): ?string + { + return $this->signature; + } + + private function verifySignature(string $decryptedContent): bool + { + if($this->getSignature() == null || $this->getSessionUuid() == null) { return false; } - throw new RuntimeException($e); + try + { + return Cryptography::verifyContent(hash('sha1', $decryptedContent), $this->getSignature(), $this->getSession()->getPublicKey()); + } + catch(CryptographyException) + { + return false; + } } - try + /** + * Handles a POST request, returning an array of RpcRequest objects + * expects a JSON encoded body with either a single RpcRequest object or an array of RpcRequest objects + * + * @return RpcRequest[] The parsed RpcRequest objects + * @throws RequestException Thrown if the request is invalid + */ + public function getRpcRequests(): array { - return Cryptography::verifyContent($this->getHash(), $signature, $session->getPublicKey()); + if($this->getSessionUuid() === null) + { + throw new RequestException("Session UUID required", 400); + } + + // Get the existing session + $session = $this->getSession(); + + // If we're awaiting a DHE, encryption is not possible at this point + if($session->getState() === SessionState::AWAITING_DHE) + { + throw new RequestException("DHE exchange required", 400); + } + + // If the session is not active, we can't serve these requests + if($session->getState() !== SessionState::ACTIVE) + { + throw new RequestException("Session is not active", 400); + } + + // Attempt to decrypt the content and verify the signature of the request + try + { + $decrypted = Cryptography::decryptTransport($this->requestBody, $session->getEncryptionKey()); + + if(!$this->verifySignature($decrypted)) + { + throw new RequestException("Invalid request signature", 401); + } + } + catch (CryptographyException $e) + { + throw new RequestException("Failed to decrypt request body", 400, $e); + } + + // At this stage, all checks has passed; we can try parsing the RPC request + try + { + // Decode the request body + $body = Utilities::jsonDecode($decrypted); + } + catch(InvalidArgumentException $e) + { + throw new RequestException("Invalid JSON in request body: " . $e->getMessage(), 400, $e); + } + + // If the body only contains a method, we assume it's a single request + if(isset($body['method'])) + { + return [$this->parseRequest($body)]; + } + + // Otherwise, we assume it's an array of requests + return array_map(fn($request) => $this->parseRequest($request), $body); } - catch(CryptographyException $e) + + /** + * Parses the raw request data into an RpcRequest object + * + * @param array $data The raw request data + * @return RpcRequest The parsed RpcRequest object + * @throws RequestException If the request is invalid + */ + private function parseRequest(array $data): RpcRequest { - return false; + if(!isset($data['method'])) + { + throw new RequestException("Missing 'method' key in request", 400); + } + + if(isset($data['id'])) + { + if(!is_string($data['id'])) + { + throw new RequestException("Invalid 'id' key in request: Expected string", 400); + } + + if(strlen($data['id']) === 0) + { + throw new RequestException("Invalid 'id' key in request: Expected non-empty string", 400); + } + + if(strlen($data['id']) > 8) + { + throw new RequestException("Invalid 'id' key in request: Expected string of length <= 8", 400); + } + } + + if(isset($data['parameters'])) + { + if(!is_array($data['parameters'])) + { + throw new RequestException("Invalid 'parameters' key in request: Expected array", 400); + } + } + + return new RpcRequest($data['method'], $data['id'] ?? null, $data['parameters'] ?? null); } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/Socialbox/Objects/ClientRequestOld.php b/src/Socialbox/Objects/ClientRequestOld.php new file mode 100644 index 0000000..036a26b --- /dev/null +++ b/src/Socialbox/Objects/ClientRequestOld.php @@ -0,0 +1,162 @@ +headers = $headers; + $this->requests = $requests; + $this->requestHash = $requestHash; + } + + /** + * @return array + */ + public function getHeaders(): array + { + return $this->headers; + } + + /** + * @return RpcRequest[] + */ + public function getRequests(): array + { + return $this->requests; + } + + public function getHash(): string + { + return $this->requestHash; + } + + public function getClientName(): string + { + return $this->headers[StandardHeaders::CLIENT_NAME->value]; + } + + public function getClientVersion(): string + { + return $this->headers[StandardHeaders::CLIENT_VERSION->value]; + } + + public function getSessionUuid(): ?string + { + if(!isset($this->headers[StandardHeaders::SESSION_UUID->value])) + { + return null; + } + + return $this->headers[StandardHeaders::SESSION_UUID->value]; + } + + public function getFromPeer(): ?PeerAddress + { + if(!isset($this->headers[StandardHeaders::FROM_PEER->value])) + { + return null; + } + + return PeerAddress::fromAddress($this->headers[StandardHeaders::FROM_PEER->value]); + } + + public function getSignature(): ?string + { + if(!isset($this->headers[StandardHeaders::SIGNATURE->value])) + { + return null; + } + + return $this->headers[StandardHeaders::SIGNATURE->value]; + } + + public function validateSession(): void + { + if($this->getSessionUuid() == null) + { + throw new StandardException(StandardError::SESSION_REQUIRED->getMessage(), StandardError::SESSION_REQUIRED); + } + + $session = SessionManager::getSession($this->getSessionUuid()); + + switch($session->getState()) + { + case SessionState::AWAITING_DHE: + throw new StandardException(StandardError::SESSION_DHE_REQUIRED->getMessage(), StandardError::SESSION_DHE_REQUIRED); + + case SessionState::EXPIRED: + throw new StandardException(StandardError::SESSION_EXPIRED->getMessage(), StandardError::SESSION_EXPIRED); + } + } + + /** + * @return bool + * @throws DatabaseOperationException + */ + public function verifySignature(): bool + { + $signature = $this->getSignature(); + $sessionUuid = $this->getSessionUuid(); + + if($signature == null || $sessionUuid == null) + { + return false; + } + + try + { + $session = SessionManager::getSession($sessionUuid); + } + catch(StandardException $e) + { + if($e->getStandardError() == StandardError::SESSION_NOT_FOUND) + { + return false; + } + + throw new RuntimeException($e); + } + + try + { + return Cryptography::verifyContent($this->getHash(), $signature, $session->getPublicKey()); + } + catch(CryptographyException $e) + { + return false; + } + } + } \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/SessionRecord.php b/src/Socialbox/Objects/Database/SessionRecord.php index ddce0cc..6f654a5 100644 --- a/src/Socialbox/Objects/Database/SessionRecord.php +++ b/src/Socialbox/Objects/Database/SessionRecord.php @@ -3,7 +3,6 @@ namespace Socialbox\Objects\Database; use DateTime; - use Socialbox\Classes\Utilities; use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\SessionState; use Socialbox\Interfaces\SerializableInterface; @@ -15,6 +14,7 @@ private bool $authenticated; private string $publicKey; private SessionState $state; + private ?string $encryptionKey; /** * @var SessionFlags[] */ @@ -40,7 +40,8 @@ $this->publicKey = $data['public_key']; $this->created = $data['created']; $this->lastRequest = $data['last_request']; - $this->flags = Utilities::unserializeList($data['flags']); + $this->encryptionKey = $data['encryption_key'] ?? null; + $this->flags = SessionFlags::fromString($data['flags']); if(SessionState::tryFrom($data['state']) == null) { @@ -108,6 +109,16 @@ return $this->state; } + /** + * Retrieves the encryption key associated with the instance. + * + * @return string|null Returns the encryption key as a string. + */ + public function getEncryptionKey(): ?string + { + return $this->encryptionKey; + } + /** * Retrieves the creation date and time of the object. * @@ -163,7 +174,7 @@ 'authenticated' => $this->authenticated, 'public_key' => $this->publicKey, 'state' => $this->state->value, - 'flags' => Utilities::serializeList($this->flags), + 'flags' => SessionFlags::toString($this->flags), 'created' => $this->created, 'last_request' => $this->lastRequest, ]; diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index d70ca08..f0ba517 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -3,40 +3,283 @@ namespace Socialbox; use Exception; + use InvalidArgumentException; use Socialbox\Classes\Configuration; + use Socialbox\Classes\Cryptography; use Socialbox\Classes\Logger; - use Socialbox\Classes\RpcHandler; use Socialbox\Classes\Utilities; + use Socialbox\Classes\Validator; + use Socialbox\Enums\SessionState; use Socialbox\Enums\StandardError; + use Socialbox\Enums\StandardHeaders; use Socialbox\Enums\StandardMethods; - use Socialbox\Exceptions\RpcException; + use Socialbox\Enums\Types\RequestType; + use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\RequestException; use Socialbox\Exceptions\StandardException; + use Socialbox\Managers\RegisteredPeerManager; + use Socialbox\Managers\SessionManager; + use Socialbox\Objects\ClientRequest; + use Socialbox\Objects\PeerAddress; class Socialbox { /** - * Handles the RPC (Remote Procedure Call) requests by parsing the client request, - * executing the appropriate methods, and returning the responses. + * Handles incoming client requests by validating required headers and processing + * the request based on its type. The method ensures proper handling of + * specific request types like RPC, session initiation, and DHE exchange, + * while returning an appropriate HTTP response for invalid or missing data. * * @return void */ - public static function handleRpc(): void + public static function handleRequest(): void { - try + $requestHeaders = Utilities::getRequestHeaders(); + + if(!isset($requestHeaders[StandardHeaders::REQUEST_TYPE->value])) { - $clientRequest = RpcHandler::getClientRequest(); - } - catch(RpcException $e) - { - Logger::getLogger()->error('Failed to parse the client request', $e); - http_response_code($e->getCode()); + http_response_code(400); + print('Missing required header: ' . StandardHeaders::REQUEST_TYPE->value); return; } - Logger::getLogger()->verbose(sprintf('Received %d RPC request(s) from %s', count($clientRequest->getRequests()), $_SERVER['REMOTE_ADDR'])); + if(!isset($requestHeaders[StandardHeaders::CLIENT_NAME->value])) + { + http_response_code(400); + print('Missing required header: ' . StandardHeaders::CLIENT_NAME->value); + return; + } + + if(!isset($requestHeaders[StandardHeaders::CLIENT_VERSION->value])) + { + http_response_code(400); + print('Missing required header: ' . StandardHeaders::CLIENT_VERSION->value); + return; + } + + $clientRequest = new ClientRequest($requestHeaders, file_get_contents('php://input') ?? null); + + // Handle the request type, only `init` and `dhe` are not encrypted using the session's encrypted key + // RPC Requests must be encrypted and signed by the client, vice versa for server responses. + switch(RequestType::tryFrom($clientRequest->getHeader(StandardHeaders::REQUEST_TYPE))) + { + case RequestType::INITIATE_SESSION: + self::handleInitiateSession($clientRequest); + break; + + case RequestType::DHE_EXCHANGE: + self::handleDheExchange($clientRequest); + break; + + case RequestType::RPC: + self::handleRpc($clientRequest); + break; + + default: + http_response_code(400); + print('Invalid Request-Type header'); + break; + } + } + + /** + * Processes a client request to initiate a session. Validates required headers, + * ensures the peer is authorized and enabled, and creates a new session UUID + * if all checks pass. Handles edge cases like missing headers, invalid inputs, + * or unauthorized peers. + * + * @param ClientRequest $clientRequest The request from the client containing + * the required headers and information. + * @return void + */ + private static function handleInitiateSession(ClientRequest $clientRequest): void + { + if(!$clientRequest->headerExists(StandardHeaders::PUBLIC_KEY)) + { + http_response_code(400); + print('Missing required header: ' . StandardHeaders::PUBLIC_KEY->value); + return; + } + + if(!$clientRequest->headerExists(StandardHeaders::IDENTIFY_AS)) + { + http_response_code(400); + print('Missing required header: ' . StandardHeaders::IDENTIFY_AS->value); + return; + } + + if(!Validator::validatePeerAddress($clientRequest->getHeader(StandardHeaders::IDENTIFY_AS))) + { + http_response_code(400); + print('Invalid Identify-As header: ' . $clientRequest->getHeader(StandardHeaders::IDENTIFY_AS)); + return; + } + + // TODO: Check if the peer address points to the domain of this server, if not we can't accept the request + + try + { + $registeredPeer = RegisteredPeerManager::getPeerByUsername($clientRequest->getIdentifyAs()->getUsername()); + + // If the peer is registered, check if it is enabled + if($registeredPeer !== null && !$registeredPeer->isEnabled()) + { + // Refuse to create a session if the peer is disabled/banned + // This also prevents multiple sessions from being created for the same peer + // A cron job should be used to clean up disabled peers + http_response_code(403); + print('Unauthorized: The requested peer is disabled/banned'); + return; + } + else + { + // Check if registration is enabled + if(!Configuration::getRegistrationConfiguration()->isRegistrationEnabled()) + { + http_response_code(403); + print('Unauthorized: Registration is disabled'); + return; + } + + // Register the peer if it is not already registered + $peerUuid = RegisteredPeerManager::createPeer(PeerAddress::fromAddress($clientRequest->getHeader(StandardHeaders::IDENTIFY_AS))->getUsername()); + // Retrieve the peer object + $registeredPeer = RegisteredPeerManager::getPeer($peerUuid); + } + + // Create the session UUID + $sessionUuid = SessionManager::createSession($clientRequest->getHeader(StandardHeaders::PUBLIC_KEY), $registeredPeer); + http_response_code(201); // Created + print($sessionUuid); // Return the session UUID + } + catch(InvalidArgumentException $e) + { + http_response_code(412); // Precondition failed + print($e->getMessage()); // Why the request failed + } + catch(Exception $e) + { + Logger::getLogger()->error('An internal error occurred while initiating the session', $e); + http_response_code(500); // Internal server error + if(Configuration::getSecurityConfiguration()->isDisplayInternalExceptions()) + { + print(Utilities::throwableToString($e)); + } + else + { + print('An internal error occurred'); + } + } + } + + /** + * Handles the Diffie-Hellman key exchange by decrypting the encrypted key passed on from the client using + * the server's private key and setting the encryption key to the session. + * + * 412: Headers malformed + * 400: Bad request + * 500: Internal server error + * 204: Success, no content. + * + * @param ClientRequest $clientRequest + * @return void + */ + private static function handleDheExchange(ClientRequest $clientRequest): void + { + // Check if the session UUID is set in the headers + if(!$clientRequest->headerExists(StandardHeaders::SESSION_UUID)) + { + Logger::getLogger()->verbose('Missing required header: ' . StandardHeaders::SESSION_UUID->value); + + http_response_code(412); + print('Missing required header: ' . StandardHeaders::SESSION_UUID->value); + return; + } + + // Check if the request body is empty + if(empty($clientRequest->getRequestBody())) + { + Logger::getLogger()->verbose('Bad request: The key exchange request body is empty'); + + http_response_code(400); + print('Bad request: The key exchange request body is empty'); + return; + } + + // Check if the session is awaiting a DHE exchange + if($clientRequest->getSession()->getState() !== SessionState::AWAITING_DHE) + { + Logger::getLogger()->verbose('Bad request: The session is not awaiting a DHE exchange'); + + http_response_code(400); + print('Bad request: The session is not awaiting a DHE exchange'); + return; + } + + try + { + // Attempt to decrypt the encrypted key passed on from the client + $encryptionKey = Cryptography::decryptContent($clientRequest->getRequestBody(), Configuration::getInstanceConfiguration()->getPrivateKey()); + } + catch (Exceptions\CryptographyException $e) + { + Logger::getLogger()->error(sprintf('Bad Request: Failed to decrypt the key for session %s', $clientRequest->getSessionUuid()), $e); + + http_response_code(400); + print('Bad Request: Cryptography error, make sure you have encrypted the key using the server\'s public key; ' . $e->getMessage()); + return; + } + + try + { + // Finally set the encryption key to the session + SessionManager::setEncryptionKey($clientRequest->getSessionUuid(), $encryptionKey); + } + catch (DatabaseOperationException $e) + { + Logger::getLogger()->error('Failed to set the encryption key for the session', $e); + http_response_code(500); + + if(Configuration::getSecurityConfiguration()->isDisplayInternalExceptions()) + { + print(Utilities::throwableToString($e)); + } + else + { + print('Internal Server Error: Failed to set the encryption key for the session'); + } + + return; + } + + Logger::getLogger()->info(sprintf('DHE exchange completed for session %s', $clientRequest->getSessionUuid())); + http_response_code(204); // Success, no content + } + + /** + * Handles incoming RPC requests from a client, processes each request, + * and returns the appropriate response(s) or error(s). + * + * @param ClientRequest $clientRequest The client's request containing one or multiple RPC calls. + * @return void + */ + private static function handleRpc(ClientRequest $clientRequest): void + { + try + { + $clientRequests = $clientRequest->getRpcRequests(); + } + catch (RequestException $e) + { + http_response_code($e->getCode()); + print($e->getMessage()); + return; + } + + Logger::getLogger()->verbose(sprintf('Received %d RPC request(s) from %s', count($clientRequests), $_SERVER['REMOTE_ADDR'])); $results = []; - foreach($clientRequest->getRequests() as $rpcRequest) + foreach($clientRequests as $rpcRequest) { $method = StandardMethods::tryFrom($rpcRequest->getMethod()); @@ -61,7 +304,7 @@ catch(Exception $e) { Logger::getLogger()->error('An internal error occurred while processing the RPC request', $e); - if(Configuration::getConfiguration()['security']['display_internal_exceptions']) + if(Configuration::getSecurityConfiguration()->isDisplayInternalExceptions()) { $response = $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, Utilities::throwableToString($e)); } @@ -79,21 +322,43 @@ } } + $response = null; + if(count($results) == 0) { - Logger::getLogger()->verbose('No results to return'); + $response = null; + } + elseif(count($results) == 1) + { + $response = json_encode($results[0], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } + else + { + $response = json_encode($results, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } + + if($response === null) + { http_response_code(204); return; } - if(count($results) == 1) + try { - Logger::getLogger()->verbose('Returning single result'); - print(json_encode($results[0])); + $encryptedResponse = Cryptography::encryptTransport($response, $clientRequest->getSession()->getEncryptionKey()); + $signature = Cryptography::signContent($response, Configuration::getInstanceConfiguration()->getPrivateKey(), true); + } + catch (Exceptions\CryptographyException $e) + { + Logger::getLogger()->error('Failed to encrypt the response', $e); + http_response_code(500); + print('Internal Server Error: Failed to encrypt the response'); return; } - Logger::getLogger()->verbose('Returning multiple results'); - print(json_encode($results)); + http_response_code(200); + header('Content-Type: application/octet-stream'); + header(StandardHeaders::SIGNATURE->value . ': ' . $signature); + print($encryptedResponse); } } \ No newline at end of file From f0624e71d1259d3d830df77dc49cd8c0557d162c Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 12 Dec 2024 05:12:28 -0500 Subject: [PATCH 055/420] Refactor header validation and add GetSessionState scaffold. --- .../StandardMethods/GetSessionState.php | 20 ++++++++++ src/Socialbox/Objects/ClientRequest.php | 8 ++-- src/Socialbox/Socialbox.php | 39 ++++++++++++------- 3 files changed, 49 insertions(+), 18 deletions(-) create mode 100644 src/Socialbox/Classes/StandardMethods/GetSessionState.php diff --git a/src/Socialbox/Classes/StandardMethods/GetSessionState.php b/src/Socialbox/Classes/StandardMethods/GetSessionState.php new file mode 100644 index 0000000..ebe20f1 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/GetSessionState.php @@ -0,0 +1,20 @@ +headers = $headers; $this->requestBody = $requestBody; - $this->clientName = $headers[StandardHeaders::CLIENT_NAME->value]; - $this->clientVersion = $headers[StandardHeaders::CLIENT_VERSION->value]; + $this->clientName = $headers[StandardHeaders::CLIENT_NAME->value] ?? null; + $this->clientVersion = $headers[StandardHeaders::CLIENT_VERSION->value] ?? null; $this->requestType = RequestType::from($headers[StandardHeaders::REQUEST_TYPE->value]); $this->identifyAs = $headers[StandardHeaders::IDENTIFY_AS->value] ?? null; $this->sessionUuid = $headers[StandardHeaders::SESSION_UUID->value] ?? null; diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index f0ba517..b9c2553 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -43,20 +43,6 @@ return; } - if(!isset($requestHeaders[StandardHeaders::CLIENT_NAME->value])) - { - http_response_code(400); - print('Missing required header: ' . StandardHeaders::CLIENT_NAME->value); - return; - } - - if(!isset($requestHeaders[StandardHeaders::CLIENT_VERSION->value])) - { - http_response_code(400); - print('Missing required header: ' . StandardHeaders::CLIENT_VERSION->value); - return; - } - $clientRequest = new ClientRequest($requestHeaders, file_get_contents('php://input') ?? null); // Handle the request type, only `init` and `dhe` are not encrypted using the session's encrypted key @@ -94,6 +80,21 @@ */ private static function handleInitiateSession(ClientRequest $clientRequest): void { + + if(!isset($requestHeaders[StandardHeaders::CLIENT_NAME->value])) + { + http_response_code(400); + print('Missing required header: ' . StandardHeaders::CLIENT_NAME->value); + return; + } + + if(!isset($requestHeaders[StandardHeaders::CLIENT_VERSION->value])) + { + http_response_code(400); + print('Missing required header: ' . StandardHeaders::CLIENT_VERSION->value); + return; + } + if(!$clientRequest->headerExists(StandardHeaders::PUBLIC_KEY)) { http_response_code(400); @@ -148,6 +149,7 @@ } // Create the session UUID + // TODO: Save client name and version to the database $sessionUuid = SessionManager::createSession($clientRequest->getHeader(StandardHeaders::PUBLIC_KEY), $registeredPeer); http_response_code(201); // Created print($sessionUuid); // Return the session UUID @@ -265,6 +267,15 @@ */ private static function handleRpc(ClientRequest $clientRequest): void { + if(!$clientRequest->headerExists(StandardHeaders::SESSION_UUID)) + { + Logger::getLogger()->verbose('Missing required header: ' . StandardHeaders::SESSION_UUID->value); + + http_response_code(412); + print('Missing required header: ' . StandardHeaders::SESSION_UUID->value); + return; + } + try { $clientRequests = $clientRequest->getRpcRequests(); From d2119df82492899e82499b8701b11eb9b2c0a59f Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 12 Dec 2024 12:58:00 -0500 Subject: [PATCH 056/420] Make client name and version nullable in ClientRequest --- src/Socialbox/Objects/ClientRequest.php | 4 ++-- src/Socialbox/Socialbox.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Socialbox/Objects/ClientRequest.php b/src/Socialbox/Objects/ClientRequest.php index 7f61ee7..402c249 100644 --- a/src/Socialbox/Objects/ClientRequest.php +++ b/src/Socialbox/Objects/ClientRequest.php @@ -73,12 +73,12 @@ return $this->requestBody; } - public function getClientName(): string + public function getClientName(): ?string { return $this->clientName; } - public function getClientVersion(): string + public function getClientVersion(): ?string { return $this->clientVersion; } diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index b9c2553..3b5c175 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -81,14 +81,14 @@ private static function handleInitiateSession(ClientRequest $clientRequest): void { - if(!isset($requestHeaders[StandardHeaders::CLIENT_NAME->value])) + if(!$clientRequest->getClientName()) { http_response_code(400); print('Missing required header: ' . StandardHeaders::CLIENT_NAME->value); return; } - if(!isset($requestHeaders[StandardHeaders::CLIENT_VERSION->value])) + if(!$clientRequest->getClientVersion()) { http_response_code(400); print('Missing required header: ' . StandardHeaders::CLIENT_VERSION->value); From 756297671f8c4cbf951b99555538268975473911 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 12 Dec 2024 14:55:44 -0500 Subject: [PATCH 057/420] Add client metadata to session creation and enforce TOS/PP --- src/Socialbox/Classes/Configuration.php | 4 + .../RegistrationConfiguration.php | 49 +++ .../StandardMethods/GetSessionState.php | 8 +- src/Socialbox/Enums/Flags/SessionFlags.php | 6 + src/Socialbox/Enums/StandardMethods.php | 54 ++- src/Socialbox/Managers/SessionManager.php | 27 +- .../Objects/Database/SessionRecord.php | 37 ++ src/Socialbox/Objects/RpcRequest.php | 332 +++++++++--------- .../Objects/Standard/SessionState.php | 100 +++++- src/Socialbox/Socialbox.php | 3 +- 10 files changed, 414 insertions(+), 206 deletions(-) diff --git a/src/Socialbox/Classes/Configuration.php b/src/Socialbox/Classes/Configuration.php index 2b0d315..07fc492 100644 --- a/src/Socialbox/Classes/Configuration.php +++ b/src/Socialbox/Classes/Configuration.php @@ -72,6 +72,10 @@ class Configuration // Registration configuration $config->setDefault('registration.enabled', true); + $config->setDefault('registration.privacy_policy_document', null); + $config->setDefault('registration.accept_privacy_policy', true); + $config->setDefault('registration.terms_of_service_document', null); + $config->setDefault('registration.accept_terms_of_service', null); $config->setDefault('registration.password_required', true); $config->setDefault('registration.otp_required', false); $config->setDefault('registration.display_name_required', false); diff --git a/src/Socialbox/Classes/Configuration/RegistrationConfiguration.php b/src/Socialbox/Classes/Configuration/RegistrationConfiguration.php index 269124c..29ebcea 100644 --- a/src/Socialbox/Classes/Configuration/RegistrationConfiguration.php +++ b/src/Socialbox/Classes/Configuration/RegistrationConfiguration.php @@ -9,6 +9,10 @@ namespace Socialbox\Classes\Configuration; class RegistrationConfiguration { private bool $registrationEnabled; + private ?string $privacyPolicyDocument; + private bool $acceptPrivacyPolicy; + private ?string $termsOfServiceDocument; + private bool $acceptTermsOfService; private bool $passwordRequired; private bool $otpRequired; private bool $displayNameRequired; @@ -31,6 +35,10 @@ class RegistrationConfiguration public function __construct(array $data) { $this->registrationEnabled = (bool)$data['enabled']; + $this->privacyPolicyDocument = $data['privacy_policy_document'] ?? null; + $this->acceptPrivacyPolicy = $data['accept_privacy_policy'] ?? true; + $this->termsOfServiceDocument = $data['terms_of_service_document'] ?? null; + $this->acceptTermsOfService = $data['accept_terms_of_service'] ?? true; $this->passwordRequired = (bool)$data['password_required']; $this->otpRequired = (bool)$data['otp_required']; $this->displayNameRequired = (bool)$data['display_name_required']; @@ -50,6 +58,47 @@ class RegistrationConfiguration return $this->registrationEnabled; } + + /** + * Retrieves the privacy policy document. + * + * @return ?string Returns the privacy policy document or null if not set. + */ + public function getPrivacyPolicyDocument(): ?string + { + return $this->privacyPolicyDocument; + } + + /** + * Checks if accepting the privacy policy is required. + * + * @return bool Returns true if the privacy policy must be accepted, false otherwise. + */ + public function isAcceptPrivacyPolicyRequired(): bool + { + return $this->acceptPrivacyPolicy; + } + + /** + * Retrieves the terms of service document. + * + * @return ?string Returns the terms of service document or null if not set. + */ + public function getTermsOfServiceDocument(): ?string + { + return $this->termsOfServiceDocument; + } + + /** + * Checks if accepting the terms of service is required. + * + * @return bool Returns true if the terms of service must be accepted, false otherwise. + */ + public function isAcceptTermsOfServiceRequired(): bool + { + return $this->acceptTermsOfService; + } + /** * Determines if a password is required. * diff --git a/src/Socialbox/Classes/StandardMethods/GetSessionState.php b/src/Socialbox/Classes/StandardMethods/GetSessionState.php index ebe20f1..11f4d2d 100644 --- a/src/Socialbox/Classes/StandardMethods/GetSessionState.php +++ b/src/Socialbox/Classes/StandardMethods/GetSessionState.php @@ -3,6 +3,7 @@ namespace Socialbox\Classes\StandardMethods; use Socialbox\Abstracts\Method; + use Socialbox\Enums\StandardError; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\ClientRequest; use Socialbox\Objects\RpcRequest; @@ -15,6 +16,11 @@ */ public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { - // TODO: Implement execute() method. + if($request->getSessionUuid() === null) + { + return $rpcRequest->produceError(StandardError::SESSION_REQUIRED); + } + + return $rpcRequest->produceResponse($request->getSession()->toStandardSessionState()); } } \ No newline at end of file diff --git a/src/Socialbox/Enums/Flags/SessionFlags.php b/src/Socialbox/Enums/Flags/SessionFlags.php index 15be9a7..bf97b95 100644 --- a/src/Socialbox/Enums/Flags/SessionFlags.php +++ b/src/Socialbox/Enums/Flags/SessionFlags.php @@ -4,12 +4,18 @@ enum SessionFlags : string { + // Session states + case REGISTRATION_REQUIRED = 'REGISTRATION_REQUIRED'; // Peer has to register + case AUTHENTICATION_REQUIRED = 'AUTHENTICATION_REQUIRED'; // Peer has to authenticate + // Verification, require fields case SET_PASSWORD = 'SET_PASSWORD'; // Peer has to set a password case SET_OTP = 'SET_OTP'; // Peer has to set an OTP case SET_DISPLAY_NAME = 'SET_DISPLAY_NAME'; // Peer has to set a display name // Verification, verification requirements + case VER_PRIVACY_POLICY = 'VER_PRIVACY_POLICY'; // Peer has to accept the privacy policy + case VER_TERMS_OF_SERVICE = 'VER_TERMS_OF_SERVICE'; // Peer has to accept the terms of service case VER_EMAIL = 'VER_EMAIL'; // Peer has to verify their email case VER_SMS = 'VER_SMS'; // Peer has to verify their phone number case VER_PHONE_CALL = 'VER_PHONE_CALL'; // Peer has to verify their phone number via a phone call diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index a606815..65c83bd 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -1,35 +1,31 @@ Ping::execute($request, $rpcRequest), - }; - } -} \ No newline at end of file + return match ($this) + { + self::PING => Ping::execute($request, $rpcRequest), + self::GET_SESSION_STATE => GetSessionState::execute($request, $rpcRequest), + }; + } + } \ No newline at end of file diff --git a/src/Socialbox/Managers/SessionManager.php b/src/Socialbox/Managers/SessionManager.php index d2c2c23..374ff5d 100644 --- a/src/Socialbox/Managers/SessionManager.php +++ b/src/Socialbox/Managers/SessionManager.php @@ -27,13 +27,12 @@ * Creates a new session with the given public key. * * @param string $publicKey The public key to associate with the new session. - * - * @return string The UUID of the newly created session. + * @param RegisteredPeerRecord $peer * * @throws InvalidArgumentException If the public key is empty or invalid. * @throws DatabaseOperationException If there is an error while creating the session in the database. */ - public static function createSession(string $publicKey, RegisteredPeerRecord $peer): string + public static function createSession(string $publicKey, RegisteredPeerRecord $peer, string $clientName, string $clientVersion): string { if($publicKey === '') { @@ -50,6 +49,8 @@ if($peer->isEnabled()) { + $flags[] = SessionFlags::AUTHENTICATION_REQUIRED; + if(RegisteredPeerManager::getPasswordAuthentication($peer)) { $flags[] = SessionFlags::VER_PASSWORD; @@ -62,6 +63,8 @@ } else { + $flags[] = SessionFlags::REGISTRATION_REQUIRED; + if(Configuration::getRegistrationConfiguration()->isDisplayNameRequired()) { $flags[] = SessionFlags::SET_DISPLAY_NAME; @@ -96,6 +99,16 @@ { $flags[] = SessionFlags::SET_OTP; } + + if(Configuration::getRegistrationConfiguration()->isAcceptPrivacyPolicyRequired()) + { + $flags[] = SessionFlags::VER_PRIVACY_POLICY; + } + + if(Configuration::getRegistrationConfiguration()->isAcceptTermsOfServiceRequired()) + { + $flags[] = SessionFlags::VER_TERMS_OF_SERVICE; + } } if(count($flags) > 0) @@ -111,11 +124,13 @@ try { - $statement = Database::getConnection()->prepare("INSERT INTO sessions (uuid, peer_uuid, public_key, flags) VALUES (?, ?, ?, ?)"); + $statement = Database::getConnection()->prepare("INSERT INTO sessions (uuid, peer_uuid, client_name, client_version, public_key, flags) VALUES (?, ?, ?, ?, ?, ?)"); $statement->bindParam(1, $uuid); $statement->bindParam(2, $peerUuid); - $statement->bindParam(3, $publicKey); - $statement->bindParam(4, $implodedFlags); + $statement->bindParam(3, $clientName); + $statement->bindParam(4, $clientVersion); + $statement->bindParam(5, $publicKey); + $statement->bindParam(6, $implodedFlags); $statement->execute(); } catch(PDOException $e) diff --git a/src/Socialbox/Objects/Database/SessionRecord.php b/src/Socialbox/Objects/Database/SessionRecord.php index 6f654a5..03dea46 100644 --- a/src/Socialbox/Objects/Database/SessionRecord.php +++ b/src/Socialbox/Objects/Database/SessionRecord.php @@ -6,11 +6,14 @@ use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\SessionState; use Socialbox\Interfaces\SerializableInterface; + use Socialbox\Managers\RegisteredPeerManager; class SessionRecord implements SerializableInterface { private string $uuid; private ?string $peerUuid; + private string $clientName; + private string $clientVersion; private bool $authenticated; private string $publicKey; private SessionState $state; @@ -36,6 +39,8 @@ { $this->uuid = $data['uuid']; $this->peerUuid = $data['peer_uuid'] ?? null; + $this->clientName = $data['client_name']; + $this->clientVersion = $data['client_version']; $this->authenticated = $data['authenticated'] ?? false; $this->publicKey = $data['public_key']; $this->created = $data['created']; @@ -149,6 +154,38 @@ return $this->lastRequest; } + /** + * Retrieves the client name. + * + * @return string Returns the client name. + */ + public function getClientName(): string + { + return $this->clientName; + } + + /** + * Retrieves the client version. + * + * @return string Returns the client version. + */ + public function getClientVersion(): string + { + return $this->clientVersion; + } + + public function toStandardSessionState(): \Socialbox\Objects\Standard\SessionState + { + return new \Socialbox\Objects\Standard\SessionState([ + 'uuid' => $this->uuid, + 'identified_as' => RegisteredPeerManager::getPeer($this->peerUuid)->getAddress(), + 'authenticated' => $this->authenticated, + 'flags' => $this->flags, + 'created' => $this->created + ]); + } + + /** * Creates a new instance of the class using the provided array data. * diff --git a/src/Socialbox/Objects/RpcRequest.php b/src/Socialbox/Objects/RpcRequest.php index db85a8f..b44089c 100644 --- a/src/Socialbox/Objects/RpcRequest.php +++ b/src/Socialbox/Objects/RpcRequest.php @@ -1,193 +1,193 @@ method = $method; - $this->parameters = $parameters; - $this->id = $id; - } + private ?string $id; + private string $method; + private ?array $parameters; - /** - * Returns the ID of the request. - * - * @return string|null The ID of the request. - */ - public function getId(): ?string - { - return $this->id; - } - - /** - * Returns the method of the request. - * - * @return string The method of the request. - */ - public function getMethod(): string - { - return $this->method; - } - - /** - * Returns the parameters of the request. - * - * @return array|null The parameters of the request, null if the request is a notification. - */ - public function getParameters(): ?array - { - return $this->parameters; - } - - /** - * Checks if the parameter exists within the RPC request - * - * @param string $parameter The parameter to check - * @return bool True if the parameter exists, False otherwise. - */ - public function containsParameter(string $parameter): bool - { - return isset($this->parameters[$parameter]); - } - - /** - * Returns the parameter value from the RPC request - * - * @param string $parameter The parameter name to get - * @return mixed The parameter value, null if the parameter value is null or not found. - */ - public function getParameter(string $parameter): mixed - { - if(!$this->containsParameter($parameter)) + /** + * Constructs the object from an array of data. + * + * @param string $method The method of the request. + * @param string|null $id The ID of the request. + * @param array|null $parameters The parameters of the request. + */ + public function __construct(string $method, ?string $id, ?array $parameters) { - return null; + $this->method = $method; + $this->parameters = $parameters; + $this->id = $id; } - return $this->parameters[$parameter]; - } - - /** - * Produces a response based off the request, null if the request is a notification - * - * @param mixed|null $result - * @return RpcResponse|null - */ - public function produceResponse(mixed $result=null): ?RpcResponse - { - if($this->id == null) + /** + * Returns the ID of the request. + * + * @return string|null The ID of the request. + */ + public function getId(): ?string { - return null; + return $this->id; } - $valid = false; - if(is_array($result)) + /** + * Returns the method of the request. + * + * @return string The method of the request. + */ + public function getMethod(): string { - $valid = true; - } - elseif($result instanceof SerializableInterface) - { - $valid = true; - } - elseif(is_string($result)) - { - $valid = true; - } - elseif(is_bool($result)) - { - $valid = true; - } - elseif(is_int($result)) - { - $valid = true; - } - elseif(is_null($result)) - { - $valid = true; + return $this->method; } - if(!$valid) + /** + * Returns the parameters of the request. + * + * @return array|null The parameters of the request, null if the request is a notification. + */ + public function getParameters(): ?array { - throw new InvalidArgumentException('The \'$result\' property must either be string, boolean, integer, array, null or SerializableInterface'); + return $this->parameters; } - Logger::getLogger()->verbose(sprintf('Producing response for request %s', $this->id)); - return new RpcResponse($this->id, $result); - } - - /** - * Produces an error response based off the request, null if the request is a notification - * - * @param StandardError $error - * @param string|null $message - * @return RpcError|null - */ - public function produceError(StandardError $error, ?string $message=null): ?RpcError - { - if($this->id == null) + /** + * Checks if the parameter exists within the RPC request + * + * @param string $parameter The parameter to check + * @return bool True if the parameter exists, False otherwise. + */ + public function containsParameter(string $parameter): bool { - return null; + return isset($this->parameters[$parameter]); } - if($message == null) + /** + * Returns the parameter value from the RPC request + * + * @param string $parameter The parameter name to get + * @return mixed The parameter value, null if the parameter value is null or not found. + */ + public function getParameter(string $parameter): mixed { - $message = $error->getMessage(); + if(!$this->containsParameter($parameter)) + { + return null; + } + + return $this->parameters[$parameter]; } - return new RpcError($this->id, $error, $message); - } + /** + * Produces a response based off the request, null if the request is a notification + * + * @param mixed|null $result + * @return RpcResponse|null + */ + public function produceResponse(mixed $result=null): ?RpcResponse + { + if($this->id == null) + { + return null; + } - /** - * @param StandardException $e - * @return RpcError|null - */ - public function handleStandardException(StandardException $e): ?RpcError - { - return $this->produceError($e->getStandardError(), $e->getMessage()); - } + $valid = false; + if(is_array($result)) + { + $valid = true; + } + elseif($result instanceof SerializableInterface) + { + $valid = true; + } + elseif(is_string($result)) + { + $valid = true; + } + elseif(is_bool($result)) + { + $valid = true; + } + elseif(is_int($result)) + { + $valid = true; + } + elseif(is_null($result)) + { + $valid = true; + } - /** - * Returns an array representation of the object. - * - * @return array - */ - public function toArray(): array - { - return [ - 'id' => $this->id, - 'method' => $this->method, - 'parameters' => $this->parameters - ]; - } + if(!$valid) + { + throw new InvalidArgumentException('The \'$result\' property must either be string, boolean, integer, array, null or SerializableInterface'); + } - /** - * Returns the request object from an array of data. - * - * @param array $data The data to construct the object from. - * @return RpcRequest The request object. - */ - public static function fromArray(array $data): RpcRequest - { - return new RpcRequest($data['method'], $data['id'] ?? null, $data['parameters'] ?? null); - } -} \ No newline at end of file + Logger::getLogger()->verbose(sprintf('Producing response for request %s', $this->id)); + return new RpcResponse($this->id, $result); + } + + /** + * Produces an error response based off the request, null if the request is a notification + * + * @param StandardError $error + * @param string|null $message + * @return RpcError|null + */ + public function produceError(StandardError $error, ?string $message=null): ?RpcError + { + if($this->id == null) + { + return null; + } + + if($message == null) + { + $message = $error->getMessage(); + } + + return new RpcError($this->id, $error, $message); + } + + /** + * @param StandardException $e + * @return RpcError|null + */ + public function handleStandardException(StandardException $e): ?RpcError + { + return $this->produceError($e->getStandardError(), $e->getMessage()); + } + + /** + * Returns an array representation of the object. + * + * @return array + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'method' => $this->method, + 'parameters' => $this->parameters + ]; + } + + /** + * Returns the request object from an array of data. + * + * @param array $data The data to construct the object from. + * @return RpcRequest The request object. + */ + public static function fromArray(array $data): RpcRequest + { + return new RpcRequest($data['method'], $data['id'] ?? null, $data['parameters'] ?? null); + } + } \ No newline at end of file diff --git a/src/Socialbox/Objects/Standard/SessionState.php b/src/Socialbox/Objects/Standard/SessionState.php index 67819a5..5c81dd4 100644 --- a/src/Socialbox/Objects/Standard/SessionState.php +++ b/src/Socialbox/Objects/Standard/SessionState.php @@ -2,7 +2,103 @@ namespace Socialbox\Objects\Standard; - class SessionState - { + use DateTime; + use Socialbox\Enums\Flags\SessionFlags; + use Socialbox\Interfaces\SerializableInterface; + class SessionState implements SerializableInterface + { + private string $uuid; + private string $identifiedAs; + private bool $authenticated; + /** + * @var SessionFlags[]|null + */ + private ?array $flags; + private DateTime $created; + + /** + * Constructor for initializing the object with the provided data. + * + * @param array $data An associative array containing the values for initializing the object. + * - 'uuid': string, Unique identifier. + * - 'identified_as': mixed, The identity information. + * - 'authenticated': bool, Whether the object is authenticated. + * - 'flags': string|null, Optional flags in + * @throws \DateMalformedStringException + */ + public function __construct(array $data) + { + $this->uuid = $data['uuid']; + $this->identifiedAs = $data['identified_as']; + $this->authenticated = $data['authenticated']; + + if(is_string($data['flags'])) + { + $this->flags = SessionFlags::fromString($data['flags']); + } + elseif(is_array($data['flags'])) + { + $this->flags = $data['flags']; + } + else + { + $this->flags = null; + } + + if(is_int($data['created'])) + { + $this->created = new DateTime(); + $this->created->setTimestamp($data['created']); + } + elseif($data['created'] instanceof DateTime) + { + $this->created = $data['created']; + } + else + { + $this->created = new DateTime($data['created']); + } + } + + public function getUuid(): string + { + return $this->uuid; + } + + public function getIdentifiedAs(): string + { + return $this->identifiedAs; + } + + public function isAuthenticated(): bool + { + return $this->authenticated; + } + + public function getFlags(): ?array + { + return $this->flags; + } + + public function getCreated(): DateTime + { + return $this->created; + } + + public static function fromArray(array $data): SessionState + { + return new self($data); + } + + public function toArray(): array + { + return [ + 'uuid' => $this->uuid, + 'identified_as' => $this->identifiedAs, + 'authenticated' => $this->authenticated, + 'flags' => $this->flags, + 'created' => $this->created->getTimestamp() + ]; + } } \ No newline at end of file diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index 3b5c175..cee7473 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -149,8 +149,7 @@ } // Create the session UUID - // TODO: Save client name and version to the database - $sessionUuid = SessionManager::createSession($clientRequest->getHeader(StandardHeaders::PUBLIC_KEY), $registeredPeer); + $sessionUuid = SessionManager::createSession($clientRequest->getHeader(StandardHeaders::PUBLIC_KEY), $registeredPeer, $clientRequest->getClientName(), $clientRequest->getClientVersion()); http_response_code(201); // Created print($sessionUuid); // Return the session UUID } From c866e2f6967272e4a69daf11aee86f95e66b472d Mon Sep 17 00:00:00 2001 From: netkas Date: Sat, 14 Dec 2024 00:43:19 -0500 Subject: [PATCH 058/420] Add support for Privacy Policy, Terms of Service, and CAPTCHA --- .idea/php.xml | 15 ++ .idea/sqldialects.xml | 1 + src/Socialbox/Classes/Resources.php | 72 ++++++- .../Classes/Resources/documents/privacy.html | 2 + .../Classes/Resources/documents/tos.html | 2 + .../StandardMethods/AcceptPrivacyPolicy.php | 48 +++++ .../StandardMethods/AcceptTermsOfService.php | 47 +++++ .../StandardMethods/GetPrivacyPolicy.php | 22 +++ .../StandardMethods/GetTermsOfService.php | 22 +++ .../StandardMethods/SettingsSetPassword.php | 76 +++++++ .../VerificationAnswerImageCaptcha.php | 65 +++--- .../VerificationGetImageCaptcha.php | 128 ++++++------ src/Socialbox/Enums/Flags/SessionFlags.php | 39 ++++ src/Socialbox/Enums/StandardError.php | 4 + src/Socialbox/Enums/StandardMethods.php | 96 ++++++++- src/Socialbox/Managers/CaptchaManager.php | 8 +- src/Socialbox/Managers/PasswordManager.php | 187 ++++++++++++++++++ src/Socialbox/Managers/SessionManager.php | 38 +++- src/Socialbox/Objects/ClientRequest.php | 14 ++ .../Objects/Database/SecurePasswordRecord.php | 16 ++ .../Objects/Database/SessionRecord.php | 20 ++ src/Socialbox/Socialbox.php | 11 ++ 22 files changed, 795 insertions(+), 138 deletions(-) create mode 100644 src/Socialbox/Classes/Resources/documents/privacy.html create mode 100644 src/Socialbox/Classes/Resources/documents/tos.html create mode 100644 src/Socialbox/Classes/StandardMethods/AcceptPrivacyPolicy.php create mode 100644 src/Socialbox/Classes/StandardMethods/AcceptTermsOfService.php create mode 100644 src/Socialbox/Classes/StandardMethods/GetPrivacyPolicy.php create mode 100644 src/Socialbox/Classes/StandardMethods/GetTermsOfService.php create mode 100644 src/Socialbox/Classes/StandardMethods/SettingsSetPassword.php create mode 100644 src/Socialbox/Managers/PasswordManager.php diff --git a/.idea/php.xml b/.idea/php.xml index 2f19bb6..26fbabf 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -10,6 +10,11 @@ + + + + + @@ -121,6 +126,11 @@ + + + + + @@ -129,6 +139,11 @@ + + + + + diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml index 80dc28a..5975784 100644 --- a/.idea/sqldialects.xml +++ b/.idea/sqldialects.xml @@ -7,6 +7,7 @@ + diff --git a/src/Socialbox/Classes/Resources.php b/src/Socialbox/Classes/Resources.php index c932bab..a7c32cb 100644 --- a/src/Socialbox/Classes/Resources.php +++ b/src/Socialbox/Classes/Resources.php @@ -1,15 +1,67 @@ value; - } -} \ No newline at end of file + /** + * Retrieves the full path to a database resource based on the provided DatabaseObjects instance. + * + * @param DatabaseObjects $object An instance of DatabaseObjects containing the resource value. + * @return string The full file path to the specified database resource. + */ + public static function getDatabaseResource(DatabaseObjects $object): string + { + $tables_directory = __DIR__ . DIRECTORY_SEPARATOR . 'Resources' . DIRECTORY_SEPARATOR . 'database'; + return $tables_directory . DIRECTORY_SEPARATOR . $object->value; + } + + /** + * Retrieves the file path of a document resource based on the provided name. + * + * @param string $name The name of the document resource to retrieve. + * @return string The file path of the specified document resource. + */ + public static function getDocumentResource(String $name): string + { + $documents_directory = __DIR__ . DIRECTORY_SEPARATOR . 'Resources' . DIRECTORY_SEPARATOR . 'documents'; + return $documents_directory . DIRECTORY_SEPARATOR . $name . '.html'; + } + + /** + * Retrieves the content of the privacy policy document. + * + * @return string The content of the privacy policy document. Attempts to fetch the document + * from a configured location if available and valid; otherwise, retrieves it from a default resource. + */ + public static function getPrivacyPolicy(): string + { + $configuredLocation = Configuration::getRegistrationConfiguration()->getPrivacyPolicyDocument(); + if($configuredLocation !== null && file_exists($configuredLocation)) + { + return file_get_contents($configuredLocation); + } + + return file_get_contents(self::getDocumentResource('privacy')); + } + + /** + * Retrieves the content of the Terms of Service document. + * + * @return string The content of the Terms of Service file. The method checks a configured location first, + * and falls back to a default resource if the configured file is unavailable. + */ + public static function getTermsOfService(): string + { + $configuredLocation = Configuration::getRegistrationConfiguration()->getTermsOfServiceDocument(); + if($configuredLocation !== null && file_exists($configuredLocation)) + { + return file_get_contents($configuredLocation); + } + + return file_get_contents(self::getDocumentResource('tos')); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/Resources/documents/privacy.html b/src/Socialbox/Classes/Resources/documents/privacy.html new file mode 100644 index 0000000..e1cae66 --- /dev/null +++ b/src/Socialbox/Classes/Resources/documents/privacy.html @@ -0,0 +1,2 @@ +

Privacy Policy Document

+

This is where the privacy policy document would reside in

\ No newline at end of file diff --git a/src/Socialbox/Classes/Resources/documents/tos.html b/src/Socialbox/Classes/Resources/documents/tos.html new file mode 100644 index 0000000..721e8dc --- /dev/null +++ b/src/Socialbox/Classes/Resources/documents/tos.html @@ -0,0 +1,2 @@ +

Terms of Service Document

+

This is where the Terms of Service document would reside

\ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/AcceptPrivacyPolicy.php b/src/Socialbox/Classes/StandardMethods/AcceptPrivacyPolicy.php new file mode 100644 index 0000000..d87ada5 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/AcceptPrivacyPolicy.php @@ -0,0 +1,48 @@ +getSessionUuid(), [SessionFlags::VER_PRIVACY_POLICY]); + } + catch (DatabaseOperationException $e) + { + return $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, $e); + } + + // Check if all registration flags are removed + if(SessionFlags::isComplete($request->getSession()->getFlags())) + { + // Set the session as authenticated + try + { + SessionManager::setAuthenticated($request->getSessionUuid(), true); + SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::REGISTRATION_REQUIRED, SessionFlags::AUTHENTICATION_REQUIRED]); + } + catch (DatabaseOperationException $e) + { + return $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, $e); + } + } + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/AcceptTermsOfService.php b/src/Socialbox/Classes/StandardMethods/AcceptTermsOfService.php new file mode 100644 index 0000000..bd5fb8c --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/AcceptTermsOfService.php @@ -0,0 +1,47 @@ +getSessionUuid(), [SessionFlags::VER_TERMS_OF_SERVICE]); + } + catch (DatabaseOperationException $e) + { + return $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, $e); + } + + // Check if all registration flags are removed + if(SessionFlags::isComplete($request->getSession()->getFlags())) + { + // Set the session as authenticated + try + { + SessionManager::setAuthenticated($request->getSessionUuid(), true); + SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::REGISTRATION_REQUIRED, SessionFlags::AUTHENTICATION_REQUIRED]); + } + catch (DatabaseOperationException $e) + { + return $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, $e); + } + } + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/GetPrivacyPolicy.php b/src/Socialbox/Classes/StandardMethods/GetPrivacyPolicy.php new file mode 100644 index 0000000..20ab6c5 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/GetPrivacyPolicy.php @@ -0,0 +1,22 @@ +produceResponse(Resources::getPrivacyPolicy()); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/GetTermsOfService.php b/src/Socialbox/Classes/StandardMethods/GetTermsOfService.php new file mode 100644 index 0000000..c92a38f --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/GetTermsOfService.php @@ -0,0 +1,22 @@ +produceResponse(Resources::getTermsOfService()); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/SettingsSetPassword.php b/src/Socialbox/Classes/StandardMethods/SettingsSetPassword.php new file mode 100644 index 0000000..7269f15 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/SettingsSetPassword.php @@ -0,0 +1,76 @@ +containsParameter('password')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'password' parameter"); + } + + if(!preg_match('/^[a-f0-9]{128}$/', $rpcRequest->getParameter('password'))) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Invalid 'password' parameter, must be sha512 hexadecimal hash"); + } + + try + { + if (PasswordManager::usesPassword($request->getPeer()->getUuid())) + { + return $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, "Cannot set password when one is already set, use 'settingsChangePassword' instead"); + } + } + catch (DatabaseOperationException $e) + { + throw new StandardException('Failed to check password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + try + { + // Set the password + PasswordManager::setPassword($request->getPeer(), $rpcRequest->getParameter('password')); + + // Remove the SET_PASSWORD flag + SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::SET_PASSWORD]); + } + catch(Exception $e) + { + throw new StandardException('Failed to set password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + // Check if all registration flags are removed + if(SessionFlags::isComplete($request->getSession()->getFlags())) + { + // Set the session as authenticated + try + { + SessionManager::setAuthenticated($request->getSessionUuid(), true); + SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::REGISTRATION_REQUIRED, SessionFlags::AUTHENTICATION_REQUIRED]); + } + catch (DatabaseOperationException $e) + { + throw new StandardException('Failed to update session due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + } + } + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php b/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php index 3d7f5fb..88adeb3 100644 --- a/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php +++ b/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php @@ -4,6 +4,7 @@ namespace Socialbox\Classes\StandardMethods; use Socialbox\Abstracts\Method; use Socialbox\Enums\Flags\PeerFlags; +use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\StandardException; @@ -11,6 +12,7 @@ use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\CaptchaManager; use Socialbox\Managers\RegisteredPeerManager; use Socialbox\Managers\SessionManager; +use Socialbox\Objects\ClientRequest; use Socialbox\Objects\ClientRequestOld; use Socialbox\Objects\RpcRequest; @@ -20,51 +22,15 @@ class VerificationAnswerImageCaptcha extends Method /** * @inheritDoc */ - public static function execute(ClientRequestOld $request, RpcRequest $rpcRequest): ?SerializableInterface + public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { - // Check if the request has a Session UUID - if($request->getSessionUuid() === null) - { - return $rpcRequest->produceError(StandardError::SESSION_REQUIRED); - } - - // Get the session and check if it's already authenticated - try - { - $session = SessionManager::getSession($request->getSessionUuid()); - } - catch(DatabaseOperationException $e) - { - throw new StandardException("There was an unexpected error while trying to get the session", StandardError::INTERNAL_SERVER_ERROR, $e); - } - - // Check for session conditions - if($session->getPeerUuid() === null) - { - return $rpcRequest->produceError(StandardError::AUTHENTICATION_REQUIRED); - } - - // Get the peer - try - { - $peer = RegisteredPeerManager::getPeer($session->getPeerUuid()); - } - catch(DatabaseOperationException $e) - { - throw new StandardException("There was unexpected error while trying to get the peer", StandardError::INTERNAL_SERVER_ERROR, $e); - } - - // Check if the VER_SOLVE_IMAGE_CAPTCHA flag exists. - if(!$peer->flagExists(PeerFlags::VER_SOLVE_IMAGE_CAPTCHA)) - { - return $rpcRequest->produceError(StandardError::CAPTCHA_NOT_AVAILABLE, 'You are not required to complete a captcha at this time'); - } - if(!$rpcRequest->containsParameter('answer')) { return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The answer parameter is required'); } + $session = $request->getSession(); + try { if(CaptchaManager::getCaptcha($session->getPeerUuid())->isExpired()) @@ -83,14 +49,29 @@ class VerificationAnswerImageCaptcha extends Method if($result) { - RegisteredPeerManager::removeFlag($session->getPeerUuid(), PeerFlags::VER_SOLVE_IMAGE_CAPTCHA); + SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::VER_IMAGE_CAPTCHA]); } - - return $rpcRequest->produceResponse($result); } catch (DatabaseOperationException $e) { throw new StandardException("There was an unexpected error while trying to answer the captcha", StandardError::INTERNAL_SERVER_ERROR, $e); } + + // Check if all registration flags are removed + if(SessionFlags::isComplete($request->getSession()->getFlags())) + { + // Set the session as authenticated + try + { + SessionManager::setAuthenticated($request->getSessionUuid(), true); + SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::REGISTRATION_REQUIRED, SessionFlags::AUTHENTICATION_REQUIRED]); + } + catch (DatabaseOperationException $e) + { + return $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, $e); + } + } + + return $rpcRequest->produceResponse($result); } } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php b/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php index 1956706..f935346 100644 --- a/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php +++ b/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php @@ -1,82 +1,68 @@ getSessionUuid() === null) + /** + * @inheritDoc + */ + public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { - return $rpcRequest->produceError(StandardError::SESSION_REQUIRED); - } + $session = $request->getSession(); - // Get the session and check if it's already authenticated - try - { - $session = SessionManager::getSession($request->getSessionUuid()); - } - catch(DatabaseOperationException $e) - { - throw new StandardException("There was an unexpected error while trying to get the session", StandardError::INTERNAL_SERVER_ERROR, $e); - } + // Check for session conditions + if($session->getPeerUuid() === null) + { + return $rpcRequest->produceError(StandardError::AUTHENTICATION_REQUIRED); + } - // Check for session conditions - if($session->getPeerUuid() === null) - { - return $rpcRequest->produceError(StandardError::AUTHENTICATION_REQUIRED); - } + $peer = $request->getPeer(); - // Get the peer - try - { - $peer = RegisteredPeerManager::getPeer($session->getPeerUuid()); - } - catch(DatabaseOperationException $e) - { - throw new StandardException("There was unexpected error while trying to get the peer", StandardError::INTERNAL_SERVER_ERROR, $e); - } + try + { + Logger::getLogger()->debug('Creating a new captcha for peer ' . $peer->getUuid()); + if(CaptchaManager::captchaExists($peer)) + { + $captchaRecord = CaptchaManager::getCaptcha($peer); + if($captchaRecord->isExpired()) + { + $answer = CaptchaManager::createCaptcha($peer); + $captchaRecord = CaptchaManager::getCaptcha($peer); + } + else + { + $answer = $captchaRecord->getAnswer(); + } + } + else + { + $answer = CaptchaManager::createCaptcha($peer); + $captchaRecord = CaptchaManager::getCaptcha($peer); + } + } + catch (DatabaseOperationException $e) + { + throw new StandardException("There was an unexpected error while trying create the captcha", StandardError::INTERNAL_SERVER_ERROR, $e); + } - // Check if the VER_SOLVE_IMAGE_CAPTCHA flag exists. - if(!$peer->flagExists(PeerFlags::VER_SOLVE_IMAGE_CAPTCHA)) - { - return $rpcRequest->produceError(StandardError::CAPTCHA_NOT_AVAILABLE, 'You are not required to complete a captcha at this time'); + // Build the captcha + // Returns HTML base64 encoded image of the captcha + return $rpcRequest->produceResponse(new ImageCaptcha([ + 'expires' => $captchaRecord->getExpires(), + 'content' => (new CaptchaBuilder($answer))->build()->inline() + ])); } - - try - { - Logger::getLogger()->debug('Creating a new captcha for peer ' . $peer->getUuid()); - $answer = CaptchaManager::createCaptcha($peer); - $captchaRecord = CaptchaManager::getCaptcha($peer); - } - catch (DatabaseOperationException $e) - { - throw new StandardException("There was an unexpected error while trying create the captcha", StandardError::INTERNAL_SERVER_ERROR, $e); - } - - // Build the captcha - return $rpcRequest->produceResponse(new ImageCaptcha([ - 'expires' => $captchaRecord->getExpires(), - 'image' => (new CaptchaBuilder($answer))->build()->inline() - ])); // Returns HTML base64 encoded image of the captcha - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/Socialbox/Enums/Flags/SessionFlags.php b/src/Socialbox/Enums/Flags/SessionFlags.php index bf97b95..99a24ed 100644 --- a/src/Socialbox/Enums/Flags/SessionFlags.php +++ b/src/Socialbox/Enums/Flags/SessionFlags.php @@ -51,4 +51,43 @@ return array_map(fn(string $value) => SessionFlags::from(trim($value)), explode(',', $flagString)); } + + /** + * Determines if all required session flags for completion are satisfied based on the given array of flags. + * + * @param array $flags An array of session flags to evaluate. Accepts both enum values (strings) and enum objects. + * @return bool True if all required flags for completion are satisfied, false otherwise. + */ + public static function isComplete(array $flags): bool + { + $flags = array_map(function ($flag) { + return is_string($flag) ? SessionFlags::from($flag) : $flag; + }, $flags); + + $flags = array_map(fn(SessionFlags $flag) => $flag->value, $flags); + + if (in_array(SessionFlags::REGISTRATION_REQUIRED->value, $flags)) { + $flagsToComplete = [ + SessionFlags::SET_PASSWORD->value, + SessionFlags::SET_OTP->value, + SessionFlags::SET_DISPLAY_NAME->value, + SessionFlags::VER_PRIVACY_POLICY->value, + SessionFlags::VER_TERMS_OF_SERVICE->value, + SessionFlags::VER_EMAIL->value, + SessionFlags::VER_SMS->value, + SessionFlags::VER_PHONE_CALL->value, + SessionFlags::VER_IMAGE_CAPTCHA->value + ]; + return !array_intersect($flagsToComplete, $flags); // Check if the intersection is empty + } + if (in_array(SessionFlags::AUTHENTICATION_REQUIRED->value, $flags)) { + $flagsToComplete = [ + SessionFlags::VER_PASSWORD->value, + SessionFlags::VER_OTP->value + ]; + return !array_intersect($flagsToComplete, $flags); // Check if the intersection is empty + + } + return true; + } } diff --git a/src/Socialbox/Enums/StandardError.php b/src/Socialbox/Enums/StandardError.php index 7f80aaf..d634a79 100644 --- a/src/Socialbox/Enums/StandardError.php +++ b/src/Socialbox/Enums/StandardError.php @@ -36,6 +36,7 @@ enum StandardError : int case INVALID_USERNAME = -4001; case USERNAME_ALREADY_EXISTS = -4002; case NOT_REGISTERED = -4003; + case METHOD_NOT_ALLOWED = -4004; /** * Returns the default generic message for the error @@ -69,6 +70,9 @@ enum StandardError : int self::INVALID_USERNAME => 'The given username is invalid, it must be Alphanumeric with a minimum of 3 character but no greater than 255 characters', self::USERNAME_ALREADY_EXISTS => 'The given username already exists on the network', self::NOT_REGISTERED => 'The given username is not registered on the server', + self::METHOD_NOT_ALLOWED => 'The requested method is not allowed', + self::SESSION_EXPIRED => 'The session has expired', + self::SESSION_DHE_REQUIRED => 'The session requires DHE to be established', }; } diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index 65c83bd..eb98544 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -2,8 +2,16 @@ namespace Socialbox\Enums; + use Socialbox\Classes\StandardMethods\AcceptPrivacyPolicy; + use Socialbox\Classes\StandardMethods\AcceptTermsOfService; + use Socialbox\Classes\StandardMethods\GetPrivacyPolicy; use Socialbox\Classes\StandardMethods\GetSessionState; + use Socialbox\Classes\StandardMethods\GetTermsOfService; use Socialbox\Classes\StandardMethods\Ping; + use Socialbox\Classes\StandardMethods\SettingsSetPassword; + use Socialbox\Classes\StandardMethods\VerificationAnswerImageCaptcha; + use Socialbox\Classes\StandardMethods\VerificationGetImageCaptcha; + use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Exceptions\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\ClientRequest; @@ -13,12 +21,24 @@ { case PING = 'ping'; case GET_SESSION_STATE = 'getSessionState'; + + case GET_PRIVACY_POLICY = 'getPrivacyPolicy'; + case ACCEPT_PRIVACY_POLICY = 'acceptPrivacyPolicy'; + case GET_TERMS_OF_SERVICE = 'getTermsOfService'; + case ACCEPT_TERMS_OF_SERVICE = 'acceptTermsOfService'; + + case VERIFICATION_GET_IMAGE_CAPTCHA = 'verificationGetImageCaptcha'; + case VERIFICATION_ANSWER_IMAGE_CAPTCHA = 'verificationAnswerImageCaptcha'; + + case SETTINGS_SET_PASSWORD = 'settingsSetPassword'; /** - * @param ClientRequest $request - * @param RpcRequest $rpcRequest - * @return SerializableInterface|null - * @throws StandardException + * Executes the appropriate operation based on the current context and requests provided. + * + * @param ClientRequest $request The client request object containing necessary data for execution. + * @param RpcRequest $rpcRequest The RPC request object providing additional parameters for execution. + * @return SerializableInterface|null The result of the operation as a serializable interface or null if no operation matches. + * @throws StandardException If an error occurs during execution */ public function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { @@ -26,6 +46,74 @@ { self::PING => Ping::execute($request, $rpcRequest), self::GET_SESSION_STATE => GetSessionState::execute($request, $rpcRequest), + + self::GET_PRIVACY_POLICY => GetPrivacyPolicy::execute($request, $rpcRequest), + self::ACCEPT_PRIVACY_POLICY => AcceptPrivacyPolicy::execute($request, $rpcRequest), + self::GET_TERMS_OF_SERVICE => GetTermsOfService::execute($request, $rpcRequest), + self::ACCEPT_TERMS_OF_SERVICE => AcceptTermsOfService::execute($request, $rpcRequest), + + self::VERIFICATION_GET_IMAGE_CAPTCHA => VerificationGetImageCaptcha::execute($request, $rpcRequest), + self::VERIFICATION_ANSWER_IMAGE_CAPTCHA => VerificationAnswerImageCaptcha::execute($request, $rpcRequest), + + self::SETTINGS_SET_PASSWORD => SettingsSetPassword::execute($request, $rpcRequest), }; } + + /** + * Checks if the access method is allowed for the given client request. + * + * @param ClientRequest $clientRequest The client request instance to check access against. + * @return void + * @throws StandardException If the method is not allowed for the given client request. + */ + public function checkAccess(ClientRequest $clientRequest): void + { + if(in_array($this, self::getAllowedMethods($clientRequest))) + { + return; + } + + throw new StandardException(StandardError::METHOD_NOT_ALLOWED->getMessage(), StandardError::METHOD_NOT_ALLOWED); + } + + /** + * Determines the list of allowed methods for a given client request. + * + * @param ClientRequest $clientRequest The client request for which allowed methods are determined. + * @return array Returns an array of allowed methods for the provided client request. + */ + public static function getAllowedMethods(ClientRequest $clientRequest): array + { + $methods = [ + self::PING, + self::GET_SESSION_STATE, + self::GET_PRIVACY_POLICY, + self::GET_TERMS_OF_SERVICE, + ]; + + $session = $clientRequest->getSession(); + + if(in_array(SessionFlags::VER_PRIVACY_POLICY, $session->getFlags())) + { + $methods[] = self::ACCEPT_PRIVACY_POLICY; + } + + if(in_array(SessionFlags::VER_TERMS_OF_SERVICE, $session->getFlags())) + { + $methods[] = self::ACCEPT_TERMS_OF_SERVICE; + } + + if(in_array(SessionFlags::VER_IMAGE_CAPTCHA, $session->getFlags())) + { + $methods[] = self::VERIFICATION_GET_IMAGE_CAPTCHA; + $methods[] = self::VERIFICATION_ANSWER_IMAGE_CAPTCHA; + } + + if(in_array(SessionFlags::SET_PASSWORD, $session->getFlags())) + { + $methods[] = self::SETTINGS_SET_PASSWORD; + } + + return $methods; + } } \ No newline at end of file diff --git a/src/Socialbox/Managers/CaptchaManager.php b/src/Socialbox/Managers/CaptchaManager.php index a2407a4..8151456 100644 --- a/src/Socialbox/Managers/CaptchaManager.php +++ b/src/Socialbox/Managers/CaptchaManager.php @@ -135,10 +135,10 @@ class CaptchaManager * Retrieves the captcha record for the given peer UUID. * * @param string|RegisteredPeerRecord $peer_uuid The UUID of the peer to retrieve the captcha for. - * @return CaptchaRecord The captcha record. + * @return CaptchaRecord|null The captcha record. * @throws DatabaseOperationException If the operation fails. */ - public static function getCaptcha(string|RegisteredPeerRecord $peer_uuid): CaptchaRecord + public static function getCaptcha(string|RegisteredPeerRecord $peer_uuid): ?CaptchaRecord { // If the peer_uuid is a RegisteredPeerRecord, get the UUID if($peer_uuid instanceof RegisteredPeerRecord) @@ -162,7 +162,7 @@ class CaptchaManager if($result === false) { - throw new DatabaseOperationException('The requested captcha does not exist'); + return null; } return CaptchaRecord::fromArray($result); @@ -175,7 +175,7 @@ class CaptchaManager * @return bool True if a captcha exists, false otherwise. * @throws DatabaseOperationException If the operation fails. */ - private static function captchaExists(string|RegisteredPeerRecord $peer_uuid): bool + public static function captchaExists(string|RegisteredPeerRecord $peer_uuid): bool { // If the peer_uuid is a RegisteredPeerRecord, get the UUID if($peer_uuid instanceof RegisteredPeerRecord) diff --git a/src/Socialbox/Managers/PasswordManager.php b/src/Socialbox/Managers/PasswordManager.php new file mode 100644 index 0000000..6de3a9f --- /dev/null +++ b/src/Socialbox/Managers/PasswordManager.php @@ -0,0 +1,187 @@ +getUuid(); + } + + try + { + $stmt = Database::getConnection()->prepare('SELECT COUNT(*) FROM authentication_passwords WHERE peer_uuid=:uuid'); + $stmt->bindParam(':uuid', $peerUuid); + $stmt->execute(); + + return $stmt->fetchColumn() > 0; + } + catch (\PDOException $e) + { + throw new DatabaseOperationException('An error occurred while checking the password usage in the database', $e); + } + } + + /** + * Sets a password for a given user or peer record by securely encrypting it + * and storing it in the authentication_passwords database table. + * + * @param string|RegisteredPeerRecord $peerUuid The UUID of the peer or an instance of RegisteredPeerRecord. + * @param string $password The plaintext password to be securely stored. + * @throws CryptographyException If an error occurs while securing the password. + * @throws DatabaseOperationException If an error occurs while attempting to store the password in the database. + * @throws \DateMalformedStringException If the updated timestamp cannot be formatted. + * @return void + */ + public static function setPassword(string|RegisteredPeerRecord $peerUuid, string $password): void + { + if($peerUuid instanceof RegisteredPeerRecord) + { + $peerUuid = $peerUuid->getUuid(); + } + + $encryptionRecord = EncryptionRecordsManager::getRandomRecord(); + $securedPassword = SecuredPassword::securePassword($peerUuid, $password, $encryptionRecord); + + try + { + $stmt = Database::getConnection()->prepare("INSERT INTO authentication_passwords (peer_uuid, iv, encrypted_password, encrypted_tag) VALUES (:peer_uuid, :iv, :encrypted_password, :encrypted_tag)"); + $stmt->bindParam(":peer_uuid", $peerUuid); + + $iv = $securedPassword->getIv(); + $stmt->bindParam(':iv', $iv); + + $encryptedPassword = $securedPassword->getEncryptedPassword(); + $stmt->bindParam(':encrypted_password', $encryptedPassword); + + $encryptedTag = $securedPassword->getEncryptedTag(); + $stmt->bindParam(':encrypted_tag', $encryptedTag); + + $stmt->execute(); + } + catch(\PDOException $e) + { + throw new DatabaseOperationException(sprintf('Failed to set password for user %s', $peerUuid), $e); + } + } + + /** + * Updates the password for a given peer identified by their UUID or a RegisteredPeerRecord. + * + * @param string|RegisteredPeerRecord $peerUuid The UUID of the peer or an instance of RegisteredPeerRecord. + * @param string $newPassword The new password to be set for the peer. + * @throws CryptographyException If an error occurs while securing the new password. + * @throws DatabaseOperationException If the update operation fails due to a database error. + * @throws \DateMalformedStringException If the updated timestamp cannot be formatted. + * @returns void + */ + public static function updatePassword(string|RegisteredPeerRecord $peerUuid, string $newPassword): void + { + if($peerUuid instanceof RegisteredPeerRecord) + { + $peerUuid = $peerUuid->getUuid(); + } + + + $encryptionRecord = EncryptionRecordsManager::getRandomRecord(); + $securedPassword = SecuredPassword::securePassword($peerUuid, $newPassword, $encryptionRecord); + + try + { + $stmt = Database::getConnection()->prepare("UPDATE authentication_passwords SET iv=:iv, encrypted_password=:encrypted_password, encrypted_tag=:encrypted_tag, updated=:updated WHERE peer_uuid=:peer_uuid"); + $stmt->bindParam(":peer_uuid", $peerUuid); + + $iv = $securedPassword->getIv(); + $stmt->bindParam(':iv', $iv); + + $encryptedPassword = $securedPassword->getEncryptedPassword(); + $stmt->bindParam(':encrypted_password', $encryptedPassword); + + $encryptedTag = $securedPassword->getEncryptedTag(); + $stmt->bindParam(':encrypted_tag', $encryptedTag); + + $updated = $securedPassword->getUpdated()->format('Y-m-d H:i:s'); + $stmt->bindParam(':updated', $updated); + + $stmt->execute(); + } + catch(\PDOException $e) + { + throw new DatabaseOperationException(sprintf('Failed to update password for user %s', $peerUuid), $e); + } + } + + /** + * Retrieves the password record associated with the given peer UUID. + * + * @param string|RegisteredPeerRecord $peerUuid The UUID of the peer or an instance of RegisteredPeerRecord. + * @return SecurePasswordRecord|null Returns a SecurePasswordRecord if found, or null if no record is present. + * @throws DatabaseOperationException If a database operation error occurs during the retrieval process. + */ + private static function getPassword(string|RegisteredPeerRecord $peerUuid): ?SecurePasswordRecord + { + if($peerUuid instanceof RegisteredPeerRecord) + { + $peerUuid = $peerUuid->getUuid(); + } + + try + { + $statement = Database::getConnection()->prepare("SELECT * FROM authentication_passwords WHERE peer_uuid=:peer_uuid LIMIT 1"); + $statement->bindParam(':peer_uuid', $peerUuid); + + $statement->execute(); + $data = $statement->fetch(PDO::FETCH_ASSOC); + + if ($data === false) + { + return null; + } + + return SecurePasswordRecord::fromArray($data); + } + catch(\PDOException $e) + { + throw new DatabaseOperationException(sprintf('Failed to retrieve password record for user %s', $peerUuid), $e); + } + } + + /** + * Verifies if the provided password matches the secured password associated with the given peer UUID. + * + * @param string|RegisteredPeerRecord $peerUuid The unique identifier or registered peer record of the user. + * @param string $password The password to be verified. + * @return bool Returns true if the password is verified successfully; otherwise, false. + * @throws DatabaseOperationException If an error occurs while retrieving the password record from the database. + * @throws CryptographyException If an error occurs while verifying the password. + */ + public static function verifyPassword(string|RegisteredPeerRecord $peerUuid, string $password): bool + { + $securedPassword = self::getPassword($peerUuid); + if($securedPassword === null) + { + return false; + } + + $encryptionRecords = EncryptionRecordsManager::getAllRecords(); + return SecuredPassword::verifyPassword($password, $securedPassword, $encryptionRecords); + } + } \ No newline at end of file diff --git a/src/Socialbox/Managers/SessionManager.php b/src/Socialbox/Managers/SessionManager.php index 374ff5d..0b50151 100644 --- a/src/Socialbox/Managers/SessionManager.php +++ b/src/Socialbox/Managers/SessionManager.php @@ -361,7 +361,7 @@ throw new StandardException(sprintf("The requested session '%s' does not exist", $uuid), StandardError::SESSION_NOT_FOUND); } - return Utilities::unserializeList($data['flags']); + return SessionFlags::fromString($data['flags']); } catch (PDOException $e) { @@ -404,7 +404,7 @@ * Removes specified flags from the session associated with the given UUID. * * @param string $uuid The UUID of the session from which the flags will be removed. - * @param array $flags An array of flags to be removed from the session. + * @param SessionFlags[] $flags An array of flags to be removed from the session. * @return void * @throws DatabaseOperationException|StandardException If there is an error while updating the session in the database. */ @@ -412,16 +412,15 @@ { Logger::getLogger()->verbose(sprintf("Removing flags from session %s", $uuid)); - // First get the existing flags $existingFlags = self::getFlags($uuid); - - // Remove the specified flags - $flags = array_diff($existingFlags, $flags); + $flagsToRemove = array_map(fn($flag) => $flag->value, $flags); + $updatedFlags = array_filter($existingFlags, fn($flag) => !in_array($flag->value, $flagsToRemove)); + $flags = SessionFlags::toString($updatedFlags); try { $statement = Database::getConnection()->prepare("UPDATE sessions SET flags=? WHERE uuid=?"); - $statement->bindValue(1, Utilities::serializeList($flags)); + $statement->bindValue(1, $flags); // Directly use the toString() result $statement->bindParam(2, $uuid); $statement->execute(); } @@ -430,4 +429,29 @@ throw new DatabaseOperationException('Failed to remove flags from session', $e); } } + + /** + * Updates the authentication status for the specified session. + * + * @param string $uuid The unique identifier of the session to be updated. + * @param bool $authenticated The authentication status to set for the session. + * @return void + * @throws DatabaseOperationException If the database operation fails. + */ + public static function setAuthenticated(string $uuid, bool $authenticated): void + { + Logger::getLogger()->verbose(sprintf("Setting session %s as authenticated: %s", $uuid, $authenticated ? 'true' : 'false')); + + try + { + $statement = Database::getConnection()->prepare("UPDATE sessions SET authenticated=? WHERE uuid=?"); + $statement->bindParam(1, $authenticated); + $statement->bindParam(2, $uuid); + $statement->execute(); + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to update authenticated peer', $e); + } + } } \ No newline at end of file diff --git a/src/Socialbox/Objects/ClientRequest.php b/src/Socialbox/Objects/ClientRequest.php index 402c249..2bfb76c 100644 --- a/src/Socialbox/Objects/ClientRequest.php +++ b/src/Socialbox/Objects/ClientRequest.php @@ -10,7 +10,9 @@ use Socialbox\Enums\Types\RequestType; use Socialbox\Exceptions\CryptographyException; use Socialbox\Exceptions\RequestException; + use Socialbox\Managers\RegisteredPeerManager; use Socialbox\Managers\SessionManager; + use Socialbox\Objects\Database\RegisteredPeerRecord; use Socialbox\Objects\Database\SessionRecord; class ClientRequest @@ -113,6 +115,18 @@ return SessionManager::getSession($this->sessionUuid); } + public function getPeer(): ?RegisteredPeerRecord + { + $session = $this->getSession(); + + if($session === null) + { + return null; + } + + return RegisteredPeerManager::getPeer($session->getPeerUuid()); + } + public function getSignature(): ?string { return $this->signature; diff --git a/src/Socialbox/Objects/Database/SecurePasswordRecord.php b/src/Socialbox/Objects/Database/SecurePasswordRecord.php index fb12350..6f02999 100644 --- a/src/Socialbox/Objects/Database/SecurePasswordRecord.php +++ b/src/Socialbox/Objects/Database/SecurePasswordRecord.php @@ -81,4 +81,20 @@ { return $this->updated; } + + public function toArray(): array + { + return [ + 'peer_uuid' => $this->peerUuid, + 'iv' => $this->iv, + 'encrypted_password' => $this->encryptedPassword, + 'encrypted_tag' => $this->encryptedTag, + 'updated' => $this->updated->format('Y-m-d H:i:s') + ]; + } + + public static function fromArray(array $data): SecurePasswordRecord + { + return new SecurePasswordRecord($data); + } } \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/SessionRecord.php b/src/Socialbox/Objects/Database/SessionRecord.php index 03dea46..77d75df 100644 --- a/src/Socialbox/Objects/Database/SessionRecord.php +++ b/src/Socialbox/Objects/Database/SessionRecord.php @@ -144,6 +144,26 @@ return $this->flags; } + /** + * Checks if a given flag exists in the list of session flags. + * + * @param string|SessionFlags $flag The flag to check, either as a string or a SessionFlags object. + * @return bool True if the flag exists, false otherwise. + */ + public function flagExists(string|SessionFlags $flag): bool + { + if(is_string($flag)) + { + $flag = SessionFlags::tryFrom($flag); + if($flag === null) + { + return false; + } + } + + return in_array($flag, $this->flags); + } + /** * Retrieves the timestamp of the last request made. * diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index cee7473..c293d67 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -293,6 +293,17 @@ { $method = StandardMethods::tryFrom($rpcRequest->getMethod()); + try + { + $method->checkAccess($clientRequest); + } + catch (StandardException $e) + { + $response = $e->produceError($rpcRequest); + $results[] = $response->toArray(); + continue; + } + if($method === false) { Logger::getLogger()->warning('The requested method does not exist'); From 0b51b478597631689625a8d98796976c25ed87c3 Mon Sep 17 00:00:00 2001 From: netkas Date: Sat, 14 Dec 2024 00:43:49 -0500 Subject: [PATCH 059/420] Updated format --- .../VerificationAnswerImageCaptcha.php | 118 +++++++++--------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php b/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php index 88adeb3..d5234c4 100644 --- a/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php +++ b/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php @@ -1,77 +1,77 @@ containsParameter('answer')) - { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The answer parameter is required'); - } - $session = $request->getSession(); - - try + /** + * @inheritDoc + */ + public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { - if(CaptchaManager::getCaptcha($session->getPeerUuid())->isExpired()) + if(!$rpcRequest->containsParameter('answer')) { - return $rpcRequest->produceError(StandardError::CAPTCHA_EXPIRED, 'The captcha has expired'); + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The answer parameter is required'); } - } - catch(DatabaseOperationException $e) - { - throw new StandardException("There was an unexpected error while trying to get the captcha", StandardError::INTERNAL_SERVER_ERROR, $e); - } - try - { - $result = CaptchaManager::answerCaptcha($session->getPeerUuid(), $rpcRequest->getParameter('answer')); + $session = $request->getSession(); - if($result) - { - SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::VER_IMAGE_CAPTCHA]); - } - } - catch (DatabaseOperationException $e) - { - throw new StandardException("There was an unexpected error while trying to answer the captcha", StandardError::INTERNAL_SERVER_ERROR, $e); - } - - // Check if all registration flags are removed - if(SessionFlags::isComplete($request->getSession()->getFlags())) - { - // Set the session as authenticated try { - SessionManager::setAuthenticated($request->getSessionUuid(), true); - SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::REGISTRATION_REQUIRED, SessionFlags::AUTHENTICATION_REQUIRED]); + if(CaptchaManager::getCaptcha($session->getPeerUuid())->isExpired()) + { + return $rpcRequest->produceError(StandardError::CAPTCHA_EXPIRED, 'The captcha has expired'); + } + } + catch(DatabaseOperationException $e) + { + throw new StandardException("There was an unexpected error while trying to get the captcha", StandardError::INTERNAL_SERVER_ERROR, $e); + } + + try + { + $result = CaptchaManager::answerCaptcha($session->getPeerUuid(), $rpcRequest->getParameter('answer')); + + if($result) + { + SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::VER_IMAGE_CAPTCHA]); + } } catch (DatabaseOperationException $e) { - return $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardException("There was an unexpected error while trying to answer the captcha", StandardError::INTERNAL_SERVER_ERROR, $e); } - } - return $rpcRequest->produceResponse($result); - } -} \ No newline at end of file + // Check if all registration flags are removed + if(SessionFlags::isComplete($request->getSession()->getFlags())) + { + // Set the session as authenticated + try + { + SessionManager::setAuthenticated($request->getSessionUuid(), true); + SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::REGISTRATION_REQUIRED, SessionFlags::AUTHENTICATION_REQUIRED]); + } + catch (DatabaseOperationException $e) + { + return $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, $e); + } + } + + return $rpcRequest->produceResponse($result); + } + } \ No newline at end of file From 42ba7013f78e93c1d5018dfe22a4e5ced08a0b2f Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 19 Dec 2024 12:54:15 -0500 Subject: [PATCH 060/420] Add ExportedSession and improve RPC client handling. --- src/Socialbox/Classes/RpcClient.php | 466 ++++++++++-------- src/Socialbox/Classes/Utilities.php | 11 + src/Socialbox/Objects/ExportedSession.php | 142 ++++++ src/Socialbox/Objects/KeyPair.php | 56 ++- .../Socialbox/Classes/SecuredPasswordTest.php | 1 - tests/test.php | 29 +- 6 files changed, 469 insertions(+), 236 deletions(-) create mode 100644 src/Socialbox/Objects/ExportedSession.php diff --git a/src/Socialbox/Classes/RpcClient.php b/src/Socialbox/Classes/RpcClient.php index 269e679..4e4e824 100644 --- a/src/Socialbox/Classes/RpcClient.php +++ b/src/Socialbox/Classes/RpcClient.php @@ -3,11 +3,14 @@ namespace Socialbox\Classes; use Socialbox\Enums\StandardHeaders; + use Socialbox\Enums\Types\RequestType; use Socialbox\Exceptions\CryptographyException; use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\ResolutionException; use Socialbox\Exceptions\RpcException; - use Socialbox\Objects\RpcError; + use Socialbox\Objects\ExportedSession; + use Socialbox\Objects\KeyPair; + use Socialbox\Objects\PeerAddress; use Socialbox\Objects\RpcRequest; use Socialbox\Objects\RpcResponse; @@ -15,270 +18,333 @@ { private const string CLIENT_NAME = 'Socialbox PHP'; private const string CLIENT_VERSION = '1.0'; - private const string CONTENT_TYPE = 'application/json; charset=utf-8'; - private string $domain; - private string $endpoint; + private bool $bypassSignatureVerification; + private PeerAddress $peerAddress; + private KeyPair $keyPair; + private string $encryptionKey; private string $serverPublicKey; - private ?string $sessionUuid; - private ?string $privateKey; + private string $rpcEndpoint; + private string $sessionUuid; /** - * Constructor for initializing the server connection with a given domain. + * Constructs a new instance with the specified peer address. * - * @param string $domain The domain used to resolve the server's endpoint and public key. - * @throws ResolutionException - * @noinspection PhpUnhandledExceptionInspection + * @param string|PeerAddress $peerAddress The peer address to be used for the instance (eg; johndoe@example.com) + * @param ExportedSession|null $exportedSession Optional. An exported session to be used to re-connect. + * @throws CryptographyException If there is an error in the cryptographic operations. + * @throws RpcException If there is an error in the RPC request or if no response is received. + * @throws DatabaseOperationException If there is an error in the database operations. + * @throws ResolutionException If there is an error in the resolution process. */ - public function __construct(string $domain) + public function __construct(string|PeerAddress $peerAddress, ?ExportedSession $exportedSession=null) { - $resolved = ServerResolver::resolveDomain($domain); + $this->bypassSignatureVerification = false; - $this->domain = $domain; - $this->endpoint = $resolved->getEndpoint(); - $this->serverPublicKey = $resolved->getPublicKey(); - $this->sessionUuid = null; - $this->privateKey = null; + // If an exported session is provided, no need to re-connect. + if($exportedSession !== null) + { + $this->peerAddress = PeerAddress::fromAddress($exportedSession->getPeerAddress()); + $this->keyPair = new KeyPair($exportedSession->getPublicKey(), $exportedSession->getPrivateKey()); + $this->encryptionKey = $exportedSession->getEncryptionKey(); + $this->serverPublicKey = $exportedSession->getServerPublicKey(); + $this->rpcEndpoint = $exportedSession->getRpcEndpoint(); + $this->sessionUuid = $exportedSession->getSessionUuid(); + return; + } + + // If the peer address is a string, we need to convert it to a PeerAddress object + if(is_string($peerAddress)) + { + $peerAddress = PeerAddress::fromAddress($peerAddress); + } + + // Set the initial properties + $this->peerAddress = $peerAddress; + $this->keyPair = Cryptography::generateKeyPair(); + $this->encryptionKey = Cryptography::generateEncryptionKey(); + + // Resolve the domain and get the server's Public Key & RPC Endpoint + $resolvedServer = ServerResolver::resolveDomain($this->peerAddress->getDomain(), false); + $this->serverPublicKey = $resolvedServer->getPublicKey(); + $this->rpcEndpoint = $resolvedServer->getEndpoint(); + + // Attempt to create an encrypted session with the server + $this->sessionUuid = $this->createSession(); + $this->sendDheExchange(); } /** - * Retrieves the domain. + * Creates a new session by sending an HTTP GET request to the RPC endpoint. + * The request includes specific headers required for session initiation. * - * @return string The domain. + * @return string Returns the session UUID received from the server. + * @throws RpcException If the server response is invalid, the session creation fails, or no session UUID is returned. */ - public function getDomain(): string + private function createSession(): string { - return $this->domain; - } + $ch = curl_init(); - /** - * Retrieves the endpoint URL. - * - * @return string The endpoint URL. - */ - public function getEndpoint(): string - { - return $this->endpoint; - } - - /** - * Retrieves the server's public key. - * - * @return string The server's public key. - */ - public function getServerPublicKey(): string - { - return $this->serverPublicKey; - } - - /** - * Retrieves the session UUID. - * - * @return string|null The session UUID or null if not set. - */ - public function getSessionUuid(): ?string - { - return $this->sessionUuid; - } - - /** - * Sets the session UUID. - * - * @param string|null $sessionUuid The session UUID to set. Can be null. - * @return void - */ - public function setSessionUuid(?string $sessionUuid): void - { - $this->sessionUuid = $sessionUuid; - } - - /** - * Retrieves the private key. - * - * @return string|null The private key if available, or null if not set. - */ - public function getPrivateKey(): ?string - { - return $this->privateKey; - } - - /** - * Sets the private key. - * - * @param string|null $privateKey The private key to be set. Can be null. - * @return void - */ - public function setPrivateKey(?string $privateKey): void - { - $this->privateKey = $privateKey; - } - - /** - * Sends an RPC request to the specified endpoint. - * - * @param RpcRequest $request The RPC request to be sent. - * @return RpcResponse|RpcError|null The response from the RPC server, an error object, or null if no content. - * @throws CryptographyException If an error occurs during the signing of the content. - * @throws RpcException If an error occurs while sending the request or processing the response. - */ - public function sendRequest(RpcRequest $request): RpcResponse|RpcError|null - { - $curl = curl_init($this->endpoint); - $content = Utilities::jsonEncode($request->toArray()); - curl_setopt_array($curl, [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_POST => true, - CURLOPT_HTTPHEADER => $this->getHeaders($content), - CURLOPT_POSTFIELDS => $content, + curl_setopt($ch, CURLOPT_URL, $this->rpcEndpoint); + curl_setopt($ch, CURLOPT_HTTPGET, true); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + StandardHeaders::REQUEST_TYPE->value . ': ' . RequestType::INITIATE_SESSION->value, + StandardHeaders::CLIENT_NAME->value . ': ' . self::CLIENT_NAME, + StandardHeaders::CLIENT_VERSION->value . ': ' . self::CLIENT_VERSION, + StandardHeaders::PUBLIC_KEY->value . ': ' . $this->keyPair->getPublicKey(), + StandardHeaders::IDENTIFY_AS->value . ': ' . $this->peerAddress->getAddress(), ]); - $response = curl_exec($curl); - $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); + $response = curl_exec($ch); - if(curl_errno($curl)) + if($response === false) { - throw new RpcException(sprintf('Failed to send request: %s', curl_error($curl))); + curl_close($ch); + throw new RpcException('Failed to create the session, no response received'); } - curl_close($curl); - - // Return null if the response is empty - if($httpCode === 204) + $responseCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); + if($responseCode !== 201) { - return null; - } - - if(!$this->isSuccessful($httpCode)) - { - if(!empty($response)) - { - throw new RpcException($response); - } - - throw new RpcException(sprintf('Error occurred while processing request: %d', $httpCode)); + curl_close($ch); + throw new RpcException('Failed to create the session, server responded with ' . $responseCode . ': ' . $response); } if(empty($response)) { - throw new RpcException('Response was empty but status code was successful'); + curl_close($ch); + throw new RpcException('Failed to create the session, server did not return a session UUID'); } - return RpcResponse::fromArray(Utilities::jsonDecode($response)); + curl_close($ch); + return $response; } /** - * Sends multiple requests to the designated endpoint and returns their responses. + * Sends a Diffie-Hellman Ephemeral (DHE) exchange request to the server. * - * @param array $requests An array of request objects, each implementing the method toArray(). - * @return RpcResponse[]|RpcError[] An array of response objects, each implementing the method toArray(). - * @throws CryptographyException If an error occurs during the signing of the content. - * @throws RpcException If any errors occur during the request process or in case of unsuccessful HTTP codes. + * @throws RpcException If the encryption or the request fails. */ - public function sendRequests(array $requests): array + private function sendDheExchange(): void { - $curl = curl_init($this->endpoint); - $contents = null; - - foreach($requests as $request) + // Request body should contain the encrypted key, the client's public key, and the session UUID + // Upon success the server should return 204 without a body + try { - $contents[] = $request->toArray(); + $encryptedKey = Cryptography::encryptContent($this->encryptionKey, $this->serverPublicKey); + } + catch (CryptographyException $e) + { + throw new RpcException('Failed to encrypt DHE exchange data', 0, $e); } - $content = Utilities::jsonEncode($contents); - - curl_setopt_array($curl, [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_POST => true, - CURLOPT_HTTPHEADER => $this->getHeaders($content), - CURLOPT_POSTFIELDS => $content, + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $this->rpcEndpoint); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + StandardHeaders::REQUEST_TYPE->value . ': ' . RequestType::DHE_EXCHANGE->value, + StandardHeaders::SESSION_UUID->value . ': ' . $this->sessionUuid, ]); + curl_setopt($ch, CURLOPT_POSTFIELDS, $encryptedKey); - $response = curl_exec($curl); - $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); + $response = curl_exec($ch); - if(curl_errno($curl)) + if($response === false) { - throw new RpcException(sprintf('Failed to send request: %s', curl_error($curl))); + curl_close($ch); + throw new RpcException('Failed to send DHE exchange, no response received'); } - curl_close($curl); - - // Return null if the response is empty - if($httpCode === 204) + $responseCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); + if($responseCode !== 204) { + curl_close($ch); + throw new RpcException('Failed to send DHE exchange, server responded with ' . $responseCode . ': ' . $response); + } + + curl_close($ch); + } + + /** + * Sends an RPC request with the given JSON data. + * + * @param string $jsonData The JSON data to be sent in the request. + * @return array An array of RpcResult objects. + * @throws RpcException If the request fails, the response is invalid, or the decryption/signature verification fails. + */ + public function sendRawRequest(string $jsonData): array + { + try + { + $encryptedData = Cryptography::encryptTransport($jsonData, $this->encryptionKey); + $signature = Cryptography::signContent($jsonData, $this->keyPair->getPrivateKey()); + } + catch (CryptographyException $e) + { + throw new RpcException('Failed to encrypt request data: ' . $e->getMessage(), 0, $e); + } + + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, $this->rpcEndpoint); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + StandardHeaders::REQUEST_TYPE->value . ': ' . RequestType::RPC->value, + StandardHeaders::SESSION_UUID->value . ': ' . $this->sessionUuid, + StandardHeaders::SIGNATURE->value . ': ' . $signature, + 'Content-Type: application/encrypted-json', + ]); + curl_setopt($ch, CURLOPT_POSTFIELDS, $encryptedData); + + $response = curl_exec($ch); + + if ($response === false) + { + curl_close($ch); + throw new RpcException('Failed to send request, no response received'); + } + + $responseCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); + $responseString = $response; + + if (!Utilities::isSuccessCodes($responseCode)) + { + curl_close($ch); + if (!empty($responseString)) + { + throw new RpcException($responseString); + } + + throw new RpcException('Failed to send request (Empty Response): ' . $responseCode); + } + + if ($responseCode == 204) + { + curl_close($ch); return []; } - if(!$this->isSuccessful($httpCode)) + if (empty($responseString)) { - if(!empty($response)) + curl_close($ch); + throw new RpcException('The request was successful but the server did not indicate an empty response'); + } + + curl_close($ch); + + try + { + $decryptedResponse = Cryptography::decryptTransport($responseString, $this->encryptionKey); + } + catch (CryptographyException $e) + { + throw new RpcException('Failed to decrypt response: ' . $e->getMessage(), 0, $e); + } + + if (!$this->bypassSignatureVerification) + { + $signature = curl_getinfo($ch, CURLINFO_HEADER_OUT)['Signature'] ?? null; + if ($signature === null) { - throw new RpcException($response); + throw new RpcException('The server did not provide a signature for the response'); } - throw new RpcException(sprintf('Error occurred while processing request: %d', $httpCode)); + try + { + if (!Cryptography::verifyContent($decryptedResponse, $signature, $this->serverPublicKey)) + { + throw new RpcException('Failed to verify the response signature'); + } + } + catch (CryptographyException $e) + { + throw new RpcException('Failed to verify the response signature: ' . $e->getMessage(), 0, $e); + } } - if(empty($response)) + $decoded = json_decode($decryptedResponse, true); + + if (is_array($decoded)) { - throw new RpcException('Response was empty but status code was successful'); + $results = []; + foreach ($decoded as $responseMap) + { + $results[] = RpcResponse::fromArray($responseMap); + } + return $results; } - $results = Utilities::jsonDecode($response); - $responses = []; - - foreach($results as $result) + if (is_object($decoded)) { - $responses[] = RpcResponse::fromArray($result); + return [RpcResponse::fromArray((array)$decoded)]; + } + + throw new RpcException('Failed to decode response'); + } + + /** + * Sends an RPC request and retrieves the corresponding RPC response. + * + * @param RpcRequest $request The RPC request to be sent. + * @return RpcResponse The received RPC response. + * @throws RpcException If no response is received from the request. + */ + public function sendRequest(RpcRequest $request): RpcResponse + { + $response = $this->sendRawRequest(json_encode($request)); + + if (count($response) === 0) + { + throw new RpcException('Failed to send request, no response received'); + } + + return $response[0]; + } + + /** + * Sends a batch of requests to the server, processes them into an appropriate format, + * and handles the response. + * + * @param RpcRequest[] $requests An array of RpcRequest objects to be sent to the server. + * @return RpcResponse[] An array of RpcResponse objects received from the server. + * @throws RpcException If no response is received from the server. + */ + public function sendRequests(array $requests): array + { + $parsedRequests = []; + foreach ($requests as $request) + { + $parsedRequests[] = $request->toArray(); + } + + $responses = $this->sendRawRequest(json_encode($parsedRequests)); + + if (count($responses) === 0) + { + throw new RpcException('Failed to send requests, no response received'); } return $responses; } /** - * Determines if the provided HTTP status code indicates a successful response. + * Exports the current session details into an ExportedSession object. * - * @param int $code The HTTP status code to evaluate. - * @return bool True if the status code represents success (2xx), false otherwise. + * @return ExportedSession The exported session containing session-specific details. */ - private function isSuccessful(int $code): bool + public function exportSession(): ExportedSession { - return $code >= 200 && $code < 300; - } - - /** - * Generates an array of headers based on standard headers and instance-specific properties. - * - * @param string $content The content to be signed if a private key is available. - * @return array An array of headers to be included in an HTTP request. - * @throws CryptographyException If an error occurs during the signing of the content. - */ - private function getHeaders(string $content): array - { - $headers = [ - sprintf('%s: %s', StandardHeaders::CLIENT_NAME->value, self::CLIENT_NAME), - sprintf('%s: %s', StandardHeaders::CLIENT_VERSION->value, self::CLIENT_VERSION), - sprintf('%s: %s', StandardHeaders::CONTENT_TYPE->value, self::CONTENT_TYPE), - ]; - - if($this->sessionUuid !== null) - { - $headers[] = sprintf('%s: %s', StandardHeaders::SESSION_UUID->value, $this->sessionUuid); - } - - if($this->privateKey !== null) - { - try - { - $headers[] = sprintf('%s: %s', StandardHeaders::SIGNATURE->value, Cryptography::signContent($content, $this->privateKey, true)); - } - catch (CryptographyException $e) - { - Logger::getLogger()->error('Failed to sign content: ' . $e->getMessage()); - throw $e; - } - } - - return $headers; + return new ExportedSession([ + 'peer_address' => $this->peerAddress->getAddress(), + 'private_key' => $this->keyPair->getPrivateKey(), + 'public_key' => $this->keyPair->getPublicKey(), + 'encryption_key' => $this->encryptionKey, + 'server_public_key' => $this->serverPublicKey, + 'rpc_endpoint' => $this->rpcEndpoint, + 'session_uuid' => $this->sessionUuid + ]); } } \ No newline at end of file diff --git a/src/Socialbox/Classes/Utilities.php b/src/Socialbox/Classes/Utilities.php index efa786b..d12ad20 100644 --- a/src/Socialbox/Classes/Utilities.php +++ b/src/Socialbox/Classes/Utilities.php @@ -215,4 +215,15 @@ class Utilities { return explode(',', $list); } + + /** + * Checks if the given HTTP response code indicates success or failure. + * + * @param int $responseCode The HTTP response code to check. + * @return bool True if the response code indicates success, false otherwise. + */ + public static function isSuccessCodes(int $responseCode): bool + { + return $responseCode >= 200 && $responseCode < 300; + } } \ No newline at end of file diff --git a/src/Socialbox/Objects/ExportedSession.php b/src/Socialbox/Objects/ExportedSession.php new file mode 100644 index 0000000..e1ca7be --- /dev/null +++ b/src/Socialbox/Objects/ExportedSession.php @@ -0,0 +1,142 @@ +peerAddress = $data['peer_address']; + $this->privateKey = $data['private_key']; + $this->publicKey = $data['public_key']; + $this->encryptionKey = $data['encryption_key']; + $this->serverPublicKey = $data['server_public_key']; + $this->rpcEndpoint = $data['rpc_endpoint']; + $this->sessionUuid = $data['session_uuid']; + } + + /** + * Retrieves the address of the peer. + * + * @return string The peer's address as a string. + */ + public function getPeerAddress(): string + { + return $this->peerAddress; + } + + /** + * Retrieves the private key. + * + * @return string The private key. + */ + public function getPrivateKey(): string + { + return $this->privateKey; + } + + /** + * Retrieves the public key. + * + * @return string The public key. + */ + public function getPublicKey(): string + { + return $this->publicKey; + } + + /** + * Retrieves the encryption key. + * + * @return string The encryption key. + */ + public function getEncryptionKey(): string + { + return $this->encryptionKey; + } + + /** + * Retrieves the public key of the server. + * + * @return string The server's public key. + */ + public function getServerPublicKey(): string + { + return $this->serverPublicKey; + } + + /** + * Retrieves the RPC endpoint URL. + * + * @return string The RPC endpoint. + */ + public function getRpcEndpoint(): string + { + return $this->rpcEndpoint; + } + + /** + * Retrieves the unique identifier for the current session. + * + * @return string The session UUID. + */ + public function getSessionUuid(): string + { + return $this->sessionUuid; + } + + /** + * Converts the current instance into an array representation. + * + * @return array An associative array containing the instance properties and their respective values. + */ + public function toArray(): array + { + return [ + 'peer_address' => $this->peerAddress, + 'private_key' => $this->privateKey, + 'public_key' => $this->publicKey, + 'encryption_key' => $this->encryptionKey, + 'server_public_key' => $this->serverPublicKey, + 'rpc_endpoint' => $this->rpcEndpoint, + 'session_uuid' => $this->sessionUuid + ]; + } + + /** + * Creates an instance of ExportedSession from the provided array. + * + * @param array $data The input data used to construct the ExportedSession instance. + * @return ExportedSession The new ExportedSession instance created from the given data. + */ + public static function fromArray(array $data): ExportedSession + { + return new ExportedSession($data); + } + } \ No newline at end of file diff --git a/src/Socialbox/Objects/KeyPair.php b/src/Socialbox/Objects/KeyPair.php index df50c98..b3e91fa 100644 --- a/src/Socialbox/Objects/KeyPair.php +++ b/src/Socialbox/Objects/KeyPair.php @@ -1,25 +1,43 @@ publicKey = $publicKey; - $this->privateKey = $privateKey; - } + private string $publicKey; + private string $privateKey; - public function getPublicKey(): string - { - return $this->publicKey; - } + /** + * Constructor method for initializing the class with a public key and private key. + * + * @param string $publicKey The public key to be used. + * @param string $privateKey The private key to be used. + * + * @return void + */ + public function __construct(string $publicKey, string $privateKey) + { + $this->publicKey = $publicKey; + $this->privateKey = $privateKey; + } - public function getPrivateKey(): string - { - return $this->privateKey; - } -} \ No newline at end of file + /** + * Retrieves the public key associated with this instance. + * + * @return string The public key. + */ + public function getPublicKey(): string + { + return $this->publicKey; + } + + /** + * Retrieves the private key associated with the instance. + * + * @return string The private key as a string. + */ + public function getPrivateKey(): string + { + return $this->privateKey; + } + } \ No newline at end of file diff --git a/tests/Socialbox/Classes/SecuredPasswordTest.php b/tests/Socialbox/Classes/SecuredPasswordTest.php index 85de779..4778602 100644 --- a/tests/Socialbox/Classes/SecuredPasswordTest.php +++ b/tests/Socialbox/Classes/SecuredPasswordTest.php @@ -11,7 +11,6 @@ { print("Getting random encryption record\n"); $encryptionRecord = EncryptionRecordsManager::getRandomRecord(); - var_dump($encryptionRecord); print("Securing password\n"); $securedPassword = SecuredPassword::securePassword('123-123-123', 'password!', $encryptionRecord); diff --git a/tests/test.php b/tests/test.php index 3d3dd60..5e21a3a 100644 --- a/tests/test.php +++ b/tests/test.php @@ -1,24 +1,21 @@ exportSession()); - print("Securing password\n"); - $securedPassword = SecuredPassword::securePassword('123-123-123', 'password!', $encryptionRecord); + function generateRandomPeer() + { + $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $charactersLength = strlen($characters); + $randomString = ''; - print("Verifying password\n"); - if(SecuredPassword::verifyPassword('password!', $securedPassword, EncryptionRecordsManager::getAllRecords())) - { - print("Password verified\n"); - } - else - { - print("Password not verified\n"); + for ($i = 0; $i < 16; $i++) + { + $randomString .= $characters[rand(0, $charactersLength - 1)]; + } + + return 'userTest' . $randomString . '@intvo.id'; } \ No newline at end of file From ef3b10b2866c9d885453b09035f05b5c05be6e58 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 19 Dec 2024 15:09:22 -0500 Subject: [PATCH 061/420] Refactor RPC framework and enhance error handling. --- src/Socialbox/Classes/Cryptography.php | 20 +- src/Socialbox/Classes/RpcClient.php | 71 ++++--- .../Exceptions/StandardException.php | 54 ++--- src/Socialbox/Objects/ClientRequest.php | 3 +- src/Socialbox/Objects/RpcError.php | 186 ++++++++++-------- src/Socialbox/Objects/RpcRequest.php | 2 +- src/Socialbox/Objects/RpcResponse.php | 146 +++++++------- src/Socialbox/Objects/RpcResult.php | 95 +++++++++ src/Socialbox/SocialClient.php | 47 ++--- tests/test.php | 4 +- 10 files changed, 383 insertions(+), 245 deletions(-) create mode 100644 src/Socialbox/Objects/RpcResult.php diff --git a/src/Socialbox/Classes/Cryptography.php b/src/Socialbox/Classes/Cryptography.php index 19b17a0..d268984 100644 --- a/src/Socialbox/Classes/Cryptography.php +++ b/src/Socialbox/Classes/Cryptography.php @@ -2,6 +2,7 @@ namespace Socialbox\Classes; +use Exception; use InvalidArgumentException; use Random\RandomException; use Socialbox\Exceptions\CryptographyException; @@ -192,7 +193,15 @@ class Cryptography */ public static function encryptContent(string $content, string $publicKey): string { - $publicKey = openssl_pkey_get_public(self::derToPem(Utilities::base64decode($publicKey), self::PEM_PUBLIC_HEADER)); + try + { + $publicKey = openssl_pkey_get_public(self::derToPem(Utilities::base64decode($publicKey), self::PEM_PUBLIC_HEADER)); + } + catch(Exception $e) + { + throw new CryptographyException('Failed to decode public key: ' . $e->getMessage()); + } + if (!$publicKey) { throw new CryptographyException('Invalid public key: ' . openssl_error_string()); @@ -203,7 +212,14 @@ class Cryptography throw new CryptographyException('Failed to encrypt content: ' . openssl_error_string()); } - return base64_encode($encrypted); + try + { + return base64_encode($encrypted); + } + catch(Exception $e) + { + throw new CryptographyException('Failed to encode encrypted content: ' . $e->getMessage()); + } } /** diff --git a/src/Socialbox/Classes/RpcClient.php b/src/Socialbox/Classes/RpcClient.php index 4e4e824..97d9854 100644 --- a/src/Socialbox/Classes/RpcClient.php +++ b/src/Socialbox/Classes/RpcClient.php @@ -12,7 +12,7 @@ use Socialbox\Objects\KeyPair; use Socialbox\Objects\PeerAddress; use Socialbox\Objects\RpcRequest; - use Socialbox\Objects\RpcResponse; + use Socialbox\Objects\RpcResult; class RpcClient { @@ -34,7 +34,6 @@ * @param ExportedSession|null $exportedSession Optional. An exported session to be used to re-connect. * @throws CryptographyException If there is an error in the cryptographic operations. * @throws RpcException If there is an error in the RPC request or if no response is received. - * @throws DatabaseOperationException If there is an error in the database operations. * @throws ResolutionException If there is an error in the resolution process. */ public function __construct(string|PeerAddress $peerAddress, ?ExportedSession $exportedSession=null) @@ -65,7 +64,15 @@ $this->encryptionKey = Cryptography::generateEncryptionKey(); // Resolve the domain and get the server's Public Key & RPC Endpoint - $resolvedServer = ServerResolver::resolveDomain($this->peerAddress->getDomain(), false); + try + { + $resolvedServer = ServerResolver::resolveDomain($this->peerAddress->getDomain(), false); + } + catch (DatabaseOperationException $e) + { + throw new ResolutionException('Failed to resolve domain: ' . $e->getMessage(), 0, $e); + } + $this->serverPublicKey = $resolvedServer->getPublicKey(); $this->rpcEndpoint = $resolvedServer->getEndpoint(); @@ -171,7 +178,7 @@ * Sends an RPC request with the given JSON data. * * @param string $jsonData The JSON data to be sent in the request. - * @return array An array of RpcResult objects. + * @return RpcResult[] An array of RpcResult objects. * @throws RpcException If the request fails, the response is invalid, or the decryption/signature verification fails. */ public function sendRawRequest(string $jsonData): array @@ -179,7 +186,7 @@ try { $encryptedData = Cryptography::encryptTransport($jsonData, $this->encryptionKey); - $signature = Cryptography::signContent($jsonData, $this->keyPair->getPrivateKey()); + $signature = Cryptography::signContent($jsonData, $this->keyPair->getPrivateKey(), true); } catch (CryptographyException $e) { @@ -187,15 +194,27 @@ } $ch = curl_init(); + $headers = []; curl_setopt($ch, CURLOPT_URL, $this->rpcEndpoint); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($curl, $header) use (&$headers) + { + $len = strlen($header); + $header = explode(':', $header, 2); + if (count($header) < 2) // ignore invalid headers + { + return $len; + } + + $headers[strtolower(trim($header[0]))][] = trim($header[1]); + return $len; + }); curl_setopt($ch, CURLOPT_HTTPHEADER, [ StandardHeaders::REQUEST_TYPE->value . ': ' . RequestType::RPC->value, StandardHeaders::SESSION_UUID->value . ': ' . $this->sessionUuid, - StandardHeaders::SIGNATURE->value . ': ' . $signature, - 'Content-Type: application/encrypted-json', + StandardHeaders::SIGNATURE->value . ': ' . $signature ]); curl_setopt($ch, CURLOPT_POSTFIELDS, $encryptedData); @@ -246,7 +265,7 @@ if (!$this->bypassSignatureVerification) { - $signature = curl_getinfo($ch, CURLINFO_HEADER_OUT)['Signature'] ?? null; + $signature = $headers['signature'][0] ?? null; if ($signature === null) { throw new RpcException('The server did not provide a signature for the response'); @@ -254,7 +273,7 @@ try { - if (!Cryptography::verifyContent($decryptedResponse, $signature, $this->serverPublicKey)) + if (!Cryptography::verifyContent($decryptedResponse, $signature, $this->serverPublicKey, true)) { throw new RpcException('Failed to verify the response signature'); } @@ -266,41 +285,45 @@ } $decoded = json_decode($decryptedResponse, true); - - if (is_array($decoded)) + if(isset($decoded['id'])) + { + return [new RpcResult($decoded)]; + } + else { $results = []; foreach ($decoded as $responseMap) { - $results[] = RpcResponse::fromArray($responseMap); + $results[] = new RpcResult($responseMap); } return $results; } - - if (is_object($decoded)) - { - return [RpcResponse::fromArray((array)$decoded)]; - } - - throw new RpcException('Failed to decode response'); } /** * Sends an RPC request and retrieves the corresponding RPC response. * * @param RpcRequest $request The RPC request to be sent. - * @return RpcResponse The received RPC response. + * @return RpcResult The received RPC response. * @throws RpcException If no response is received from the request. */ - public function sendRequest(RpcRequest $request): RpcResponse + public function sendRequest(RpcRequest $request, bool $throwException=true): RpcResult { - $response = $this->sendRawRequest(json_encode($request)); + $response = $this->sendRawRequest(json_encode($request->toArray())); if (count($response) === 0) { throw new RpcException('Failed to send request, no response received'); } + if($throwException) + { + if($response[0]->getError() !== null) + { + throw $response[0]->getError()->toRpcException(); + } + } + return $response[0]; } @@ -309,7 +332,7 @@ * and handles the response. * * @param RpcRequest[] $requests An array of RpcRequest objects to be sent to the server. - * @return RpcResponse[] An array of RpcResponse objects received from the server. + * @return RpcResult[] An array of RpcResult objects received from the server. * @throws RpcException If no response is received from the server. */ public function sendRequests(array $requests): array @@ -324,7 +347,7 @@ if (count($responses) === 0) { - throw new RpcException('Failed to send requests, no response received'); + return []; } return $responses; diff --git a/src/Socialbox/Exceptions/StandardException.php b/src/Socialbox/Exceptions/StandardException.php index c163c82..f9f639c 100644 --- a/src/Socialbox/Exceptions/StandardException.php +++ b/src/Socialbox/Exceptions/StandardException.php @@ -1,34 +1,34 @@ value, $previous); - } + /** + * Thrown as a standard error, with a message and a code + * + * @param string $message + * @param StandardError $code + * @param Throwable|null $previous + */ + public function __construct(string $message, StandardError $code, ?Throwable $previous=null) + { + parent::__construct($message, $code->value, $previous); + } - public function getStandardError(): StandardError - { - return StandardError::from($this->code); - } + public function getStandardError(): StandardError + { + return StandardError::from($this->code); + } - public function produceError(RpcRequest $request): ?RpcError - { - return $request->produceError(StandardError::from($this->code), $this->message); - } -} \ No newline at end of file + public function produceError(RpcRequest $request): ?RpcError + { + return $request->produceError(StandardError::from($this->code), $this->message); + } + } \ No newline at end of file diff --git a/src/Socialbox/Objects/ClientRequest.php b/src/Socialbox/Objects/ClientRequest.php index 2bfb76c..5a368fb 100644 --- a/src/Socialbox/Objects/ClientRequest.php +++ b/src/Socialbox/Objects/ClientRequest.php @@ -4,6 +4,7 @@ use InvalidArgumentException; use Socialbox\Classes\Cryptography; + use Socialbox\Classes\Logger; use Socialbox\Classes\Utilities; use Socialbox\Enums\SessionState; use Socialbox\Enums\StandardHeaders; @@ -141,7 +142,7 @@ try { - return Cryptography::verifyContent(hash('sha1', $decryptedContent), $this->getSignature(), $this->getSession()->getPublicKey()); + return Cryptography::verifyContent($decryptedContent, $this->getSignature(), $this->getSession()->getPublicKey(), true); } catch(CryptographyException) { diff --git a/src/Socialbox/Objects/RpcError.php b/src/Socialbox/Objects/RpcError.php index 9b6453f..5e1bfc2 100644 --- a/src/Socialbox/Objects/RpcError.php +++ b/src/Socialbox/Objects/RpcError.php @@ -1,98 +1,112 @@ id = $id; - $this->code = $code; + private string $id; + private StandardError $code; + private string $error; - if($error === null) + /** + * Constructs the RPC error object. + * + * @param string $id The ID of the RPC request + * @param StandardError|int $code The error code + * @param string|null $error The error message + */ + public function __construct(string $id, StandardError|int $code, ?string $error) { - $this->error = $code->getMessage(); - } - else - { - $this->error = $error; + $this->id = $id; + + if(is_int($code)) + { + $code = StandardError::tryFrom($code); + if($code === null) + { + $code = StandardError::UNKNOWN; + } + } + + $this->code = $code; + + if($error === null) + { + $this->error = $code->getMessage(); + } + else + { + $this->error = $error; + } + } - } - - /** - * Returns the ID of the RPC request. - * - * @return string The ID of the RPC request. - */ - public function getId(): string - { - return $this->id; - } - - /** - * Returns the error message. - * - * @return string The error message. - */ - public function getError(): string - { - return $this->error; - } - - /** - * Returns the error code. - * - * @return StandardError The error code. - */ - public function getCode(): StandardError - { - return $this->code; - } - - /** - * Returns an array representation of the object. - * - * @return array The array representation of the object. - */ - public function toArray(): array - { - return [ - 'id' => $this->id, - 'error' => $this->error, - 'code' => $this->code->value - ]; - } - - /** - * Returns the RPC error object from an array of data. - * - * @param array $data The data to construct the RPC error from. - * @return RpcError The RPC error object. - */ - public static function fromArray(array $data): RpcError - { - $errorCode = StandardError::tryFrom($data['code']); - - if($errorCode == null) + /** + * Returns the ID of the RPC request. + * + * @return string The ID of the RPC request. + */ + public function getId(): string { - $errorCode = StandardError::UNKNOWN; + return $this->id; } - return new RpcError($data['id'], $data['error'], $errorCode); - } -} \ No newline at end of file + /** + * Returns the error code. + * + * @return StandardError The error code. + */ + public function getCode(): StandardError + { + return $this->code; + } + + /** + * Returns the error message. + * + * @return string The error message. + */ + public function getError(): string + { + return $this->error; + } + + /** + * Returns an array representation of the object. + * + * @return array The array representation of the object. + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'code' => $this->code->value, + 'error' => $this->error + ]; + } + + /** + * Converts the current object to an RpcException instance. + * + * @return RpcException The RpcException generated from the current object. + */ + public function toRpcException(): RpcException + { + return new RpcException($this->error, $this->code->value); + } + + /** + * Returns the RPC error object from an array of data. + * + * @param array $data The data to construct the RPC error from. + * @return RpcError The RPC error object. + */ + public static function fromArray(array $data): RpcError + { + return new RpcError($data['id'], $data['code'], $data['error']); + } + } \ No newline at end of file diff --git a/src/Socialbox/Objects/RpcRequest.php b/src/Socialbox/Objects/RpcRequest.php index b44089c..b0ffa88 100644 --- a/src/Socialbox/Objects/RpcRequest.php +++ b/src/Socialbox/Objects/RpcRequest.php @@ -23,7 +23,7 @@ * @param string|null $id The ID of the request. * @param array|null $parameters The parameters of the request. */ - public function __construct(string $method, ?string $id, ?array $parameters) + public function __construct(string $method, ?string $id, ?array $parameters=null) { $this->method = $method; $this->parameters = $parameters; diff --git a/src/Socialbox/Objects/RpcResponse.php b/src/Socialbox/Objects/RpcResponse.php index a109eab..2e56751 100644 --- a/src/Socialbox/Objects/RpcResponse.php +++ b/src/Socialbox/Objects/RpcResponse.php @@ -1,84 +1,84 @@ id = $id; - $this->result = $result; - } + private string $id; + private mixed $result; - /** - * Returns the ID of the response. - * - * @return string The ID of the response. - */ - public function getId(): string - { - return $this->id; - } - - /** - * Returns the result of the response. - * - * @return mixed|null The result of the response. - */ - public function getResult(): mixed - { - return $this->result; - } - - /** - * Converts the given data to an array. - * - * @param mixed $data The data to be converted. This can be an instance of SerializableInterface, an array, or a scalar value. - * @return mixed The converted data as an array if applicable, or the original data. - */ - private function convertToArray(mixed $data): mixed - { - // If the data is an instance of SerializableInterface, call toArray on it - if ($data instanceof SerializableInterface) + /** + * Constructs the response object. + * + * @param string $id The ID of the response. + * @param mixed|null $result The result of the response. + */ + public function __construct(string $id, mixed $result) { - return $data->toArray(); + $this->id = $id; + $this->result = $result; } - return $data; - } + /** + * Returns the ID of the response. + * + * @return string The ID of the response. + */ + public function getId(): string + { + return $this->id; + } - /** - * Returns an array representation of the object. - * - * @return array The array representation of the object. - */ - public function toArray(): array - { - return [ - 'id' => $this->id, - 'result' => $this->convertToArray($this->result) - ]; - } + /** + * Returns the result of the response. + * + * @return mixed|null The result of the response. + */ + public function getResult(): mixed + { + return $this->result; + } - /** - * Returns the response object from an array of data. - * - * @param array $data The data to construct the response from. - * @return RpcResponse The response object. - */ - public static function fromArray(array $data): RpcResponse - { - return new RpcResponse($data['id'], $data['result']); - } -} \ No newline at end of file + /** + * Converts the given data to an array. + * + * @param mixed $data The data to be converted. This can be an instance of SerializableInterface, an array, or a scalar value. + * @return mixed The converted data as an array if applicable, or the original data. + */ + private function convertToArray(mixed $data): mixed + { + // If the data is an instance of SerializableInterface, call toArray on it + if ($data instanceof SerializableInterface) + { + return $data->toArray(); + } + + return $data; + } + + /** + * Returns an array representation of the object. + * + * @return array The array representation of the object. + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'result' => $this->convertToArray($this->result) + ]; + } + + /** + * Returns the response object from an array of data. + * + * @param array $data The data to construct the response from. + * @return RpcResponse The response object. + */ + public static function fromArray(array $data): RpcResponse + { + return new RpcResponse($data['id'], $data['result']); + } + } \ No newline at end of file diff --git a/src/Socialbox/Objects/RpcResult.php b/src/Socialbox/Objects/RpcResult.php new file mode 100644 index 0000000..7ffd443 --- /dev/null +++ b/src/Socialbox/Objects/RpcResult.php @@ -0,0 +1,95 @@ +error = null; + $this->response = null; + return; + } + + if($response instanceof RpcResponse) + { + $this->response = $response; + $this->error = null; + return; + } + + if($response instanceof RpcError) + { + $this->error = $response; + $this->response = null; + } + } + + /** + * Checks whether the operation was successful. + * + * @return bool True if there is no error, otherwise false. + */ + public function isSuccess(): bool + { + return $this->error === null; + } + + /** + * Checks if the instance contains no error and no response. + * + * @return bool True if both error and response are null, otherwise false. + */ + public function isEmpty(): bool + { + return $this->error === null && $this->response === null; + } + + /** + * Retrieves the error associated with the instance, if any. + * + * @return RpcError|null The error object if an error exists, or null if no error is present. + */ + public function getError(): ?RpcError + { + return $this->error; + } + + /** + * Retrieves the RPC response if available. + * + * @return RpcResponse|null The response object if set, or null if no response is present. + */ + public function getResponse(): ?RpcResponse + { + return $this->response; + } + } \ No newline at end of file diff --git a/src/Socialbox/SocialClient.php b/src/Socialbox/SocialClient.php index c11c100..ec325b7 100644 --- a/src/Socialbox/SocialClient.php +++ b/src/Socialbox/SocialClient.php @@ -5,53 +5,42 @@ use Socialbox\Classes\RpcClient; use Socialbox\Classes\Utilities; use Socialbox\Exceptions\CryptographyException; + use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\ResolutionException; use Socialbox\Exceptions\RpcException; + use Socialbox\Objects\ExportedSession; use Socialbox\Objects\KeyPair; + use Socialbox\Objects\PeerAddress; use Socialbox\Objects\RpcError; use Socialbox\Objects\RpcRequest; class SocialClient extends RpcClient { /** - * Constructs a new instance with the specified domain. + * Constructs the object from an array of data. * - * @param string $domain The domain to be set for the instance. - * @throws ResolutionException + * @param string|PeerAddress $peerAddress The address of the peer to connect to. + * @param ExportedSession|null $exportedSession Optional. The exported session to use for communication. + * @throws CryptographyException If the public key is invalid. + * @throws ResolutionException If the domain cannot be resolved. + * @throws RpcException If the RPC request fails. */ - public function __construct(string $domain) + public function __construct(string|PeerAddress $peerAddress, ?ExportedSession $exportedSession=null) { - parent::__construct($domain); + parent::__construct($peerAddress, $exportedSession); } /** - * Creates a new session using the provided key pair. + * Sends a ping request to the server and checks the response. * - * @param KeyPair $keyPair The key pair to be used for creating the session. - * @return string The UUID of the created session. - * @throws CryptographyException if there is an error in the cryptographic operations. - * @throws RpcException if there is an error in the RPC request or if no response is received. + * @return true Returns true if the ping request succeeds. + * @throws RpcException Thrown if the RPC request fails. */ - public function createSession(KeyPair $keyPair): string + public function ping(): true { - $response = $this->sendRequest(new RpcRequest('createSession', Utilities::randomCrc32(), [ - 'public_key' => $keyPair->getPublicKey() - ])); - - if($response === null) - { - throw new RpcException('Failed to create the session, no response received'); - } - - if($response instanceof RpcError) - { - throw RpcException::fromRpcError($response); - } - - $this->setSessionUuid($response->getResult()); - $this->setPrivateKey($keyPair->getPrivateKey()); - - return $response->getResult(); + return (bool)$this->sendRequest( + new RpcRequest('ping', Utilities::randomCrc32()) + )->getResponse()->getResult(); } } \ No newline at end of file diff --git a/tests/test.php b/tests/test.php index 5e21a3a..585ec76 100644 --- a/tests/test.php +++ b/tests/test.php @@ -3,8 +3,8 @@ require 'ncc'; import('net.nosial.socialbox'); - $client = new \Socialbox\Classes\RpcClient(generateRandomPeer()); - var_dump($client->exportSession()); + $client = new \Socialbox\SocialClient(generateRandomPeer()); + var_dump($client->ping()); function generateRandomPeer() { From 014b63705bf7ad873268742a44a3b83e4a9ef05e Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 19 Dec 2024 17:59:50 -0500 Subject: [PATCH 062/420] Improve RPC handling and add terms/privacy methods. --- src/Socialbox/Classes/RpcClient.php | 5 +++ src/Socialbox/Classes/ServerResolver.php | 17 ++++---- src/Socialbox/Classes/Utilities.php | 1 + src/Socialbox/SocialClient.php | 50 ++++++++++++++++++++++++ tests/test.php | 4 ++ 5 files changed, 68 insertions(+), 9 deletions(-) diff --git a/src/Socialbox/Classes/RpcClient.php b/src/Socialbox/Classes/RpcClient.php index 97d9854..50ced1d 100644 --- a/src/Socialbox/Classes/RpcClient.php +++ b/src/Socialbox/Classes/RpcClient.php @@ -76,6 +76,11 @@ $this->serverPublicKey = $resolvedServer->getPublicKey(); $this->rpcEndpoint = $resolvedServer->getEndpoint(); + if(empty($this->serverPublicKey)) + { + throw new ResolutionException('Failed to resolve domain: No public key found for the server'); + } + // Attempt to create an encrypted session with the server $this->sessionUuid = $this->createSession(); $this->sendDheExchange(); diff --git a/src/Socialbox/Classes/ServerResolver.php b/src/Socialbox/Classes/ServerResolver.php index 9524b19..8d18f94 100644 --- a/src/Socialbox/Classes/ServerResolver.php +++ b/src/Socialbox/Classes/ServerResolver.php @@ -38,22 +38,18 @@ } $fullRecord = self::concatenateTxtRecords($txtRecords); - if (preg_match(self::PATTERN, $fullRecord, $matches)) { $endpoint = trim($matches[1]); $publicKey = trim(str_replace(' ', '', $matches[2])); - if (empty($endpoint)) { throw new ResolutionException(sprintf("Failed to resolve RPC endpoint for %s", $domain)); } - if (empty($publicKey)) { throw new ResolutionException(sprintf("Failed to resolve public key for %s", $domain)); } - return new ResolvedServer($endpoint, $publicKey); } else @@ -74,23 +70,26 @@ } /** - * Concatenates an array of TXT records into a single string. + * Concatenates an array of TXT records into a single string, filtering for SocialBox records. * * @param array $txtRecords An array of TXT records, where each record is expected to have a 'txt' key. - * @return string A concatenated string of all TXT records. + * @return string A concatenated string of all relevant TXT records. */ private static function concatenateTxtRecords(array $txtRecords): string { $fullRecordBuilder = ''; - foreach ($txtRecords as $txt) { if (isset($txt['txt'])) { - $fullRecordBuilder .= trim($txt['txt'], '" '); + $record = trim($txt['txt'], '" '); + // Only include records that start with v=socialbox + if (stripos($record, 'v=socialbox') === 0) + { + $fullRecordBuilder .= $record; + } } } - return $fullRecordBuilder; } } \ No newline at end of file diff --git a/src/Socialbox/Classes/Utilities.php b/src/Socialbox/Classes/Utilities.php index d12ad20..231fa85 100644 --- a/src/Socialbox/Classes/Utilities.php +++ b/src/Socialbox/Classes/Utilities.php @@ -83,6 +83,7 @@ class Utilities if ($decoded === false) { + var_dump($data); throw new InvalidArgumentException('Failed to decode data from Base64'); } diff --git a/src/Socialbox/SocialClient.php b/src/Socialbox/SocialClient.php index ec325b7..9dae1ec 100644 --- a/src/Socialbox/SocialClient.php +++ b/src/Socialbox/SocialClient.php @@ -43,4 +43,54 @@ )->getResponse()->getResult(); } + /** + * Retrieves the privacy policy from the server. + * + * @return string Returns the privacy policy as a string. + * @throws RpcException Thrown if the RPC request fails. + */ + public function getPrivacyPolicy(): string + { + return $this->sendRequest( + new RpcRequest('getPrivacyPolicy', Utilities::randomCrc32()) + )->getResponse()->getResult(); + } + + /** + * Accepts the privacy policy by sending a request to the server. + * + * @return true Returns true if the privacy policy is successfully accepted. + * @throws RpcException Thrown if the RPC request fails. + */ + public function acceptPrivacyPolicy(): true + { + return (bool)$this->sendRequest( + new RpcRequest('acceptPrivacyPolicy', Utilities::randomCrc32()) + )->getResponse()->getResult(); + } + + /** + * Retrieves the terms of service from the server. + * + * @return string Returns the terms of service as a string. + * @throws RpcException Thrown if the RPC request fails. + */ + public function getTermsOfService(): string + { + return $this->sendRequest(new RpcRequest('getTermsOfService', Utilities::randomCrc32()) + )->getResponse()->getResult(); + } + + /** + * Sends a request to accept the terms of service and verifies the response. + * + * @return true Returns true if the terms of service are successfully accepted. + * @throws RpcException Thrown if the RPC request fails. + */ + public function acceptTermsOfService(): true + { + return (bool)$this->sendRequest( + new RpcRequest('acceptTermsOfService', Utilities::randomCrc32()) + )->getResponse()->getResult(); + } } \ No newline at end of file diff --git a/tests/test.php b/tests/test.php index 585ec76..0b671b4 100644 --- a/tests/test.php +++ b/tests/test.php @@ -5,6 +5,10 @@ $client = new \Socialbox\SocialClient(generateRandomPeer()); var_dump($client->ping()); + var_dump($client->getPrivacyPolicy()); + var_dump($client->acceptPrivacyPolicy()); + var_dump($client->getTermsOfService()); + var_dump($client->acceptTermsOfService()); function generateRandomPeer() { From ea3de13cf880fae2da445a76aae50ce13290a9c9 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 19 Dec 2024 21:10:45 -0500 Subject: [PATCH 063/420] Refactor session handling and improve data reliability. --- src/Socialbox/Classes/Utilities.php | 1 - .../Managers/ResolvedServersManager.php | 13 +++- .../Objects/Standard/SessionState.php | 67 ++++++++++++++++--- src/Socialbox/SocialClient.php | 17 ++++- tests/test.php | 6 +- 5 files changed, 84 insertions(+), 20 deletions(-) diff --git a/src/Socialbox/Classes/Utilities.php b/src/Socialbox/Classes/Utilities.php index 231fa85..d12ad20 100644 --- a/src/Socialbox/Classes/Utilities.php +++ b/src/Socialbox/Classes/Utilities.php @@ -83,7 +83,6 @@ class Utilities if ($decoded === false) { - var_dump($data); throw new InvalidArgumentException('Failed to decode data from Base64'); } diff --git a/src/Socialbox/Managers/ResolvedServersManager.php b/src/Socialbox/Managers/ResolvedServersManager.php index 69ae2d1..781f6f4 100644 --- a/src/Socialbox/Managers/ResolvedServersManager.php +++ b/src/Socialbox/Managers/ResolvedServersManager.php @@ -81,10 +81,11 @@ class ResolvedServersManager * Retrieves the resolved server record from the database for a given domain. * * @param string $domain The domain name for which to retrieve the resolved server record. - * @return ResolvedServerRecord The resolved server record associated with the given domain. + * @return ResolvedServerRecord|null The resolved server record associated with the given domain. * @throws DatabaseOperationException If there is an error retrieving the resolved server record from the database. + * @throws \DateMalformedStringException If the date string is malformed. */ - public static function getResolvedServer(string $domain): ResolvedServerRecord + public static function getResolvedServer(string $domain): ?ResolvedServerRecord { try { @@ -92,7 +93,13 @@ class ResolvedServersManager $statement->bindParam(1, $domain); $statement->execute(); $result = $statement->fetch(); - return new ResolvedServerRecord($result); + + if($result === false) + { + return null; + } + + return ResolvedServerRecord::fromArray($result); } catch(PDOException $e) { diff --git a/src/Socialbox/Objects/Standard/SessionState.php b/src/Socialbox/Objects/Standard/SessionState.php index 5c81dd4..d4d4715 100644 --- a/src/Socialbox/Objects/Standard/SessionState.php +++ b/src/Socialbox/Objects/Standard/SessionState.php @@ -15,7 +15,7 @@ * @var SessionFlags[]|null */ private ?array $flags; - private DateTime $created; + private int $created; /** * Constructor for initializing the object with the provided data. @@ -25,7 +25,6 @@ * - 'identified_as': mixed, The identity information. * - 'authenticated': bool, Whether the object is authenticated. * - 'flags': string|null, Optional flags in - * @throws \DateMalformedStringException */ public function __construct(array $data) { @@ -48,49 +47,101 @@ if(is_int($data['created'])) { - $this->created = new DateTime(); - $this->created->setTimestamp($data['created']); + $this->created = $data['created']; } elseif($data['created'] instanceof DateTime) { - $this->created = $data['created']; + $this->created = $data['created']->getTimestamp(); } else { - $this->created = new DateTime($data['created']); + $this->created = time(); } } + /** + * Retrieves the UUID of the current instance. + * + * @return string The UUID as a string. + */ public function getUuid(): string { return $this->uuid; } + /** + * + * @return string The identifier associated with the entity. + */ public function getIdentifiedAs(): string { return $this->identifiedAs; } + /** + * Checks if the user is authenticated. + * + * @return bool Returns true if the user is authenticated, otherwise false. + */ public function isAuthenticated(): bool { return $this->authenticated; } + /** + * + * @return array|null + */ public function getFlags(): ?array { return $this->flags; } - public function getCreated(): DateTime + /** + * Checks if the provided flag exists within the current flags. + * + * @param string|SessionFlags $flag The flag to check, either as a string or an instance of SessionFlags. + * @return bool Returns true if the flag is found in the current flags, otherwise false. + */ + public function containsFlag(string|SessionFlags $flag): bool + { + if($this->flags === null || count($this->flags) === 0) + { + return false; + } + + if($flag instanceof SessionFlags) + { + $flag = $flag->value; + } + + return in_array($flag, $this->flags); + } + + /** + * + * @return int Returns the created timestamp as an integer. + */ + public function getCreated(): int { return $this->created; } + /** + * Creates a new instance of SessionState from the provided array. + * + * @param array $data The input array containing data to initialize the SessionState instance. + * @return SessionState A new instance of the SessionState class. + */ public static function fromArray(array $data): SessionState { return new self($data); } + /** + * + * @return array An associative array representation of the object's properties, including 'uuid', 'identified_as', 'authenticated', 'flags', and 'created'. + */ public function toArray(): array { return [ @@ -98,7 +149,7 @@ 'identified_as' => $this->identifiedAs, 'authenticated' => $this->authenticated, 'flags' => $this->flags, - 'created' => $this->created->getTimestamp() + 'created' => $this->created, ]; } } \ No newline at end of file diff --git a/src/Socialbox/SocialClient.php b/src/Socialbox/SocialClient.php index 9dae1ec..201d9f7 100644 --- a/src/Socialbox/SocialClient.php +++ b/src/Socialbox/SocialClient.php @@ -5,14 +5,12 @@ use Socialbox\Classes\RpcClient; use Socialbox\Classes\Utilities; use Socialbox\Exceptions\CryptographyException; - use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\ResolutionException; use Socialbox\Exceptions\RpcException; use Socialbox\Objects\ExportedSession; - use Socialbox\Objects\KeyPair; use Socialbox\Objects\PeerAddress; - use Socialbox\Objects\RpcError; use Socialbox\Objects\RpcRequest; + use Socialbox\Objects\Standard\SessionState; class SocialClient extends RpcClient { @@ -43,6 +41,19 @@ )->getResponse()->getResult(); } + /** + * Retrieves the current state of the session from the server. + * + * @return SessionState Returns an instance of SessionState representing the session's current state. + * @throws RpcException Thrown if the RPC request fails. + */ + public function getSessionState(): SessionState + { + return SessionState::fromArray($this->sendRequest( + new RpcRequest('getSessionState', Utilities::randomCrc32()) + )->getResponse()->getResult()); + } + /** * Retrieves the privacy policy from the server. * diff --git a/tests/test.php b/tests/test.php index 0b671b4..f176dbe 100644 --- a/tests/test.php +++ b/tests/test.php @@ -4,11 +4,7 @@ import('net.nosial.socialbox'); $client = new \Socialbox\SocialClient(generateRandomPeer()); - var_dump($client->ping()); - var_dump($client->getPrivacyPolicy()); - var_dump($client->acceptPrivacyPolicy()); - var_dump($client->getTermsOfService()); - var_dump($client->acceptTermsOfService()); + var_dump($client->getSessionState()); function generateRandomPeer() { From de0792868918e81fe6ce707037c6ee7b3d42796b Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 19 Dec 2024 21:11:40 -0500 Subject: [PATCH 064/420] Refine PHPDoc comments for better clarity. --- src/Socialbox/Objects/Standard/SessionState.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Socialbox/Objects/Standard/SessionState.php b/src/Socialbox/Objects/Standard/SessionState.php index d4d4715..d2352ae 100644 --- a/src/Socialbox/Objects/Standard/SessionState.php +++ b/src/Socialbox/Objects/Standard/SessionState.php @@ -70,8 +70,9 @@ } /** + * Retrieves the identifier of the current instance. * - * @return string The identifier associated with the entity. + * @return string The identifier as a string. */ public function getIdentifiedAs(): string { @@ -89,8 +90,9 @@ } /** + * Retrieves the flags associated with the current instance. * - * @return array|null + * @return array|null An array of flags or null if no flags are set. */ public function getFlags(): ?array { @@ -119,8 +121,9 @@ } /** + * Retrieves the creation timestamp of the current instance. * - * @return int Returns the created timestamp as an integer. + * @return int The creation timestamp as an integer. */ public function getCreated(): int { @@ -139,8 +142,9 @@ } /** + * Converts the current instance into an associative array. * - * @return array An associative array representation of the object's properties, including 'uuid', 'identified_as', 'authenticated', 'flags', and 'created'. + * @return array An associative array representation of the instance, including UUID, identification, authentication status, flags, and creation date. */ public function toArray(): array { From 01253d51153c6a76c34804dfaa803dc3766c16bf Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 20 Dec 2024 15:02:57 -0500 Subject: [PATCH 065/420] Refactor and enhance session flags, enums, and RPC methods. --- src/Socialbox/Enums/Flags/SessionFlags.php | 59 ++- src/Socialbox/Enums/StandardMethods.php | 23 ++ .../Objects/Database/SecurePasswordRecord.php | 10 +- src/Socialbox/Objects/PeerAddress.php | 170 ++++----- src/Socialbox/SocialClient.php | 341 +++++++++++++++++- tests/test.php | 1 + 6 files changed, 506 insertions(+), 98 deletions(-) diff --git a/src/Socialbox/Enums/Flags/SessionFlags.php b/src/Socialbox/Enums/Flags/SessionFlags.php index 99a24ed..0a678a6 100644 --- a/src/Socialbox/Enums/Flags/SessionFlags.php +++ b/src/Socialbox/Enums/Flags/SessionFlags.php @@ -12,18 +12,63 @@ case SET_PASSWORD = 'SET_PASSWORD'; // Peer has to set a password case SET_OTP = 'SET_OTP'; // Peer has to set an OTP case SET_DISPLAY_NAME = 'SET_DISPLAY_NAME'; // Peer has to set a display name + case SET_DISPLAY_PICTURE = 'SET_DISPLAY_PICTURE'; // Peer has to set a display picture + case SET_EMAIL = 'SET_EMAIL'; // Peer has to set an email + case SET_PHONE = 'SET_PHONE'; // Peer has to set a phone number + case SET_BIRTHDAY = 'SET_BIRTHDAY'; // Peer has to set a birthday // Verification, verification requirements case VER_PRIVACY_POLICY = 'VER_PRIVACY_POLICY'; // Peer has to accept the privacy policy case VER_TERMS_OF_SERVICE = 'VER_TERMS_OF_SERVICE'; // Peer has to accept the terms of service + case VER_COMMUNITY_GUIDELINES = 'VER_COMMUNITY_GUIDELINES'; // Peer has to acknowledge the community guidelines case VER_EMAIL = 'VER_EMAIL'; // Peer has to verify their email case VER_SMS = 'VER_SMS'; // Peer has to verify their phone number case VER_PHONE_CALL = 'VER_PHONE_CALL'; // Peer has to verify their phone number via a phone call case VER_IMAGE_CAPTCHA = 'VER_IMAGE_CAPTCHA'; // Peer has to solve an image captcha + case VER_TEXT_CAPTCHA = 'VER_TEXT_CAPTCHA'; // Peer has to solve a text captcha + case VER_EXTERNAL_URL = 'VER_EXTERNAL_URL'; // Peer has to visit an external URL // Login, require fields case VER_PASSWORD = 'VER_PASSWORD'; // Peer has to enter their password case VER_OTP = 'VER_OTP'; // Peer has to enter their OTP + case VER_AUTHENTICATION_CODE = 'VER_AUTHENTICATION_CODE'; // Peer has to enter their authentication code + + // Session Flags + case RATE_LIMITED = 'RATE_LIMITED'; // Peer is temporarily rate limited + + /** + * Determines whether the current value corresponds to a registration method flag. + * + * @return bool True if the value is a registration method flag, otherwise false. + */ + public function isRegistrationFlag(): bool + { + return in_array($this->value, [ + self::SET_PASSWORD->value, + self::SET_OTP->value, + self::SET_DISPLAY_NAME->value, + self::VER_PRIVACY_POLICY->value, + self::VER_TERMS_OF_SERVICE->value, + self::VER_EMAIL->value, + self::VER_SMS->value, + self::VER_PHONE_CALL->value, + self::VER_IMAGE_CAPTCHA->value + ]); + } + + /** + * Determines whether the current value corresponds to an authentication method flag. + * + * @return bool True if the value is an authentication method flag, otherwise false. + */ + public function isAuthenticationFlag(): bool + { + return in_array($this->value, [ + self::VER_IMAGE_CAPTCHA->value, + self::VER_PASSWORD->value, + self::VER_OTP->value + ]); + } /** * Converts an array of SessionFlags to a comma-separated string of their values. @@ -60,13 +105,12 @@ */ public static function isComplete(array $flags): bool { - $flags = array_map(function ($flag) { - return is_string($flag) ? SessionFlags::from($flag) : $flag; - }, $flags); - + // todo: refactor this to use the isRegistrationFlag & isAuthenticationFlag methods + $flags = array_map(function ($flag) {return is_string($flag) ? SessionFlags::from($flag) : $flag;}, $flags); $flags = array_map(fn(SessionFlags $flag) => $flag->value, $flags); - if (in_array(SessionFlags::REGISTRATION_REQUIRED->value, $flags)) { + if (in_array(SessionFlags::REGISTRATION_REQUIRED->value, $flags)) + { $flagsToComplete = [ SessionFlags::SET_PASSWORD->value, SessionFlags::SET_OTP->value, @@ -80,7 +124,9 @@ ]; return !array_intersect($flagsToComplete, $flags); // Check if the intersection is empty } - if (in_array(SessionFlags::AUTHENTICATION_REQUIRED->value, $flags)) { + + if (in_array(SessionFlags::AUTHENTICATION_REQUIRED->value, $flags)) + { $flagsToComplete = [ SessionFlags::VER_PASSWORD->value, SessionFlags::VER_OTP->value @@ -88,6 +134,7 @@ return !array_intersect($flagsToComplete, $flags); // Check if the intersection is empty } + return true; } } diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index eb98544..e921744 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -26,11 +26,34 @@ case ACCEPT_PRIVACY_POLICY = 'acceptPrivacyPolicy'; case GET_TERMS_OF_SERVICE = 'getTermsOfService'; case ACCEPT_TERMS_OF_SERVICE = 'acceptTermsOfService'; + case GET_COMMUNITY_GUIDELINES = 'getCommunityGuidelines'; + case ACCEPT_COMMUNITY_GUIDELINES = 'acceptCommunityGuidelines'; + + case VERIFICATION_EMAIL = 'verificationEmail'; + case VERIFICATION_ANSWER_EMAIL = 'verificationAnswerEmail'; + + case VERIFICATION_SMS = 'verificationSms'; + case VERIFICATION_ANSWER_SMS = 'verificationAnswerSms'; + + case VERIFICATION_PHONE_CALL = 'verificationPhoneCall'; + case VERIFICATION_ANSWER_PHONE_CALL = 'verificationAnswerPhoneCall'; case VERIFICATION_GET_IMAGE_CAPTCHA = 'verificationGetImageCaptcha'; case VERIFICATION_ANSWER_IMAGE_CAPTCHA = 'verificationAnswerImageCaptcha'; + case VERIFICATION_GET_TEXT_CAPTCHA = 'verificationGetTextCaptcha'; + case VERIFICATION_ANSWER_TEXT_CAPTCHA = 'verificationAnswerTextCaptcha'; + + case VERIFICATION_GET_EXTERNAL_URL = 'verificationGetExternalUrl'; + case VERIFICATION_ANSWER_EXTERNAL_URL = 'verificationAnswerExternalUrl'; + case SETTINGS_SET_PASSWORD = 'settingsSetPassword'; + case SETTINGS_SET_OTP = 'settingsSetOtp'; + case SETTINGS_SET_DISPLAY_NAME = 'settingsSetDisplayName'; + case SETTINGS_SET_DISPLAY_PICTURE = 'settingsSetDisplayPicture'; + case SETTINGS_SET_EMAIL = 'settingsSetEmail'; + case SETTINGS_SET_PHONE = 'settingsSetPhone'; + case SETTINGS_SET_BIRTHDAY = 'settingsSetBirthday'; /** * Executes the appropriate operation based on the current context and requests provided. diff --git a/src/Socialbox/Objects/Database/SecurePasswordRecord.php b/src/Socialbox/Objects/Database/SecurePasswordRecord.php index 6f02999..239fafa 100644 --- a/src/Socialbox/Objects/Database/SecurePasswordRecord.php +++ b/src/Socialbox/Objects/Database/SecurePasswordRecord.php @@ -29,7 +29,15 @@ $this->iv = $data['iv']; $this->encryptedPassword = $data['encrypted_password']; $this->encryptedTag = $data['encrypted_tag']; - $this->updated = new DateTime($data['updated']); + + if($data['updated'] instanceof DateTime) + { + $this->updated = $data['updated']; + } + else + { + $this->updated = new DateTime($data['updated']); + } } /** diff --git a/src/Socialbox/Objects/PeerAddress.php b/src/Socialbox/Objects/PeerAddress.php index 42af2be..3a07416 100644 --- a/src/Socialbox/Objects/PeerAddress.php +++ b/src/Socialbox/Objects/PeerAddress.php @@ -1,96 +1,96 @@ username = $username; - $this->domain = $domain; - } + private string $username; + private string $domain; - /** - * Constructs a PeerAddress from a full peer address (eg; john@example.com) - * - * @param string $address The full address of the peer - * @return PeerAddress The constructed PeerAddress object - */ - public static function fromAddress(string $address): PeerAddress - { - if(!Validator::validatePeerAddress($address)) + /** + * Constructs a PeerAddress object from a given username and domain + * + * @param string $username The username of the peer + * @param string $domain The domain of the peer + */ + public function __construct(string $username, string $domain) { - throw new InvalidArgumentException("Invalid peer address: $address"); + $this->username = $username; + $this->domain = $domain; } - $parts = explode('@', $address); - return new PeerAddress($parts[0], $parts[1]); - } - - /** - * Returns the username of the peer - * - * @return string - */ - public function getUsername(): string - { - return $this->username; - } - - /** - * Returns the domain of the peer - * - * @return string - */ - public function getDomain(): string - { - return $this->domain; - } - - /** - * Returns whether the peer is the host - * - * @return bool True if the peer is the host, false otherwise - */ - public function isHost(): bool - { - return $this->username === ReservedUsernames::HOST->value; - } - - /** - * Returns whether the peer requires authentication, for example, the anonymous user does not require authentication - * - * @return bool True if authentication is required, false otherwise - */ - public function authenticationRequired(): bool - { - return match($this->username) + /** + * Constructs a PeerAddress from a full peer address (eg; john@example.com) + * + * @param string $address The full address of the peer + * @return PeerAddress The constructed PeerAddress object + */ + public static function fromAddress(string $address): PeerAddress { - ReservedUsernames::ANONYMOUS->value => false, - default => true - }; - } + if(!Validator::validatePeerAddress($address)) + { + throw new InvalidArgumentException("Invalid peer address: $address"); + } - /** - * Returns the full address of the peer - * - * @return string The full address of the peer - */ - public function getAddress(): string - { - return sprintf("%s@%s", $this->username, $this->domain); - } -} \ No newline at end of file + $parts = explode('@', $address); + return new PeerAddress($parts[0], $parts[1]); + } + + /** + * Returns the username of the peer + * + * @return string + */ + public function getUsername(): string + { + return $this->username; + } + + /** + * Returns the domain of the peer + * + * @return string + */ + public function getDomain(): string + { + return $this->domain; + } + + /** + * Returns whether the peer is the host + * + * @return bool True if the peer is the host, false otherwise + */ + public function isHost(): bool + { + return $this->username === ReservedUsernames::HOST->value; + } + + /** + * Returns whether the peer requires authentication, for example, the anonymous user does not require authentication + * + * @return bool True if authentication is required, false otherwise + */ + public function authenticationRequired(): bool + { + return match($this->username) + { + ReservedUsernames::ANONYMOUS->value => false, + default => true + }; + } + + /** + * Returns the full address of the peer + * + * @return string The full address of the peer + */ + public function getAddress(): string + { + return sprintf("%s@%s", $this->username, $this->domain); + } + } \ No newline at end of file diff --git a/src/Socialbox/SocialClient.php b/src/Socialbox/SocialClient.php index 201d9f7..669508d 100644 --- a/src/Socialbox/SocialClient.php +++ b/src/Socialbox/SocialClient.php @@ -1,9 +1,12 @@ sendRequest( - new RpcRequest('ping', Utilities::randomCrc32()) + new RpcRequest(StandardMethods::PING->value, Utilities::randomCrc32()) )->getResponse()->getResult(); } @@ -50,7 +53,7 @@ public function getSessionState(): SessionState { return SessionState::fromArray($this->sendRequest( - new RpcRequest('getSessionState', Utilities::randomCrc32()) + new RpcRequest(StandardMethods::GET_SESSION_STATE->value, Utilities::randomCrc32()) )->getResponse()->getResult()); } @@ -63,7 +66,7 @@ public function getPrivacyPolicy(): string { return $this->sendRequest( - new RpcRequest('getPrivacyPolicy', Utilities::randomCrc32()) + new RpcRequest(StandardMethods::GET_PRIVACY_POLICY->value, Utilities::randomCrc32()) )->getResponse()->getResult(); } @@ -76,7 +79,7 @@ public function acceptPrivacyPolicy(): true { return (bool)$this->sendRequest( - new RpcRequest('acceptPrivacyPolicy', Utilities::randomCrc32()) + new RpcRequest(StandardMethods::ACCEPT_PRIVACY_POLICY->value, Utilities::randomCrc32()) )->getResponse()->getResult(); } @@ -88,7 +91,8 @@ */ public function getTermsOfService(): string { - return $this->sendRequest(new RpcRequest('getTermsOfService', Utilities::randomCrc32()) + return $this->sendRequest( + new RpcRequest(StandardMethods::GET_TERMS_OF_SERVICE->value, Utilities::randomCrc32()) )->getResponse()->getResult(); } @@ -101,7 +105,332 @@ public function acceptTermsOfService(): true { return (bool)$this->sendRequest( - new RpcRequest('acceptTermsOfService', Utilities::randomCrc32()) + new RpcRequest(StandardMethods::ACCEPT_TERMS_OF_SERVICE->value, Utilities::randomCrc32()) + )->getResponse()->getResult(); + } + + /** + * Fetches the community guidelines by performing a remote procedure call request. + * + * @return string The content of the community guidelines. + * @throws RpcException Thrown if the RPC request encounters an error. + */ + public function getCommunityGuidelines(): string + { + return $this->sendRequest( + new RpcRequest(StandardMethods::GET_COMMUNITY_GUIDELINES->value, Utilities::randomCrc32()) + )->getResponse()->getResult(); + } + + /** + * Sends a request to accept the community guidelines via a remote procedure call. + * + * @return true Indicates that the community guidelines have been successfully accepted. + * @throws RpcException Thrown if the RPC request encounters an error. + */ + public function acceptCommunityGuidelines(): true + { + return $this->sendRequest( + new RpcRequest(StandardMethods::ACCEPT_COMMUNITY_GUIDELINES->value, Utilities::randomCrc32()) + )->getResponse()->getResult(); + } + + /** + * Sends a verification email to the specified email address by making a remote procedure call request. + * + * @param string $emailAddress The email address to which the verification email will be sent. + * @return true Indicates the successful initiation of the verification process. + * @throws RpcException Thrown if the RPC request fails. + */ + public function verificationEmail(string $emailAddress): true + { + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::VERIFICATION_EMAIL->value, Utilities::randomCrc32(), [ + 'email_address' => $emailAddress + ]) + )->getResponse()->getResult(); + } + + /** + * Confirms a verification process using an email verification code by sending a remote procedure call request. + * + * @param string $verificationCode The verification code to validate the email. + * @return true The result indicating the successful processing of the verification. + * @throws RpcException Thrown if the RPC request fails. + */ + public function verificationAnswerEmail(string $verificationCode): true + { + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::VERIFICATION_ANSWER_EMAIL->value, Utilities::randomCrc32(), [ + 'verification_code' => $verificationCode + ]) + )->getResponse()->getResult(); + } + + /** + * Sends a verification SMS to the specified phone number by initiating a remote procedure call. + * + * @param string $phoneNumber The phone number to which the verification SMS should be sent. + * @return true True if the SMS was sent successfully. + * @throws RpcException Thrown if the RPC request fails. + */ + public function verificationSms(string $phoneNumber): true + { + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::VERIFICATION_SMS->value, Utilities::randomCrc32(), [ + 'phone_number' => $phoneNumber + ]) + )->getResponse()->getResult(); + } + + /** + * Sends a verification SMS answer by providing the verification code through a remote procedure call request. + * + * @param string $verificationCode The verification code to be sent for completing the SMS verification process. + * @return true Returns true if the verification is successfully processed. + * @throws RpcException Thrown if the RPC request fails. + */ + public function verificationAnswerSms(string $verificationCode): true + { + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::VERIFICATION_ANSWER_SMS->value, Utilities::randomCrc32(), [ + 'verification_code' => $verificationCode + ]) + )->getResponse()->getResult(); + } + + /** + * Initiates a phone verification process by sending a remote procedure call request. + * + * @param string $phoneNumber The phone number to be verified. + * @return bool True if the phone verification request was successful. + * @throws RpcException Thrown if the RPC request fails. + */ + public function verificationPhone(string $phoneNumber): true + { + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::VERIFICATION_PHONE_CALL->value, Utilities::randomCrc32(), [ + 'phone_number' => $phoneNumber + ]) + )->getResponse()->getResult(); + } + + /** + * Answers a verification phone call by sending a remote procedure call request with the provided verification code. + * + * @param string $verificationCode The verification code to authenticate the phone call. + * @return true Returns true if the verification phone call was successfully answered. + * @throws RpcException Thrown if the RPC request fails. + */ + public function verificationAnswerPhone(string $verificationCode): true + { + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::VERIFICATION_ANSWER_PHONE_CALL->value, Utilities::randomCrc32(), [ + 'verification_code' => $verificationCode + ]) + )->getResponse()->getResult(); + } + + /** + * Retrieves the image captcha for verification purposes by sending a remote procedure call request. + * + * @return string The result of the image captcha request. + * @throws RpcException Thrown if the RPC request fails. + */ + public function verificationGetImageCaptcha(): string + { + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::VERIFICATION_GET_IMAGE_CAPTCHA->value, Utilities::randomCrc32()) + )->getResponse()->getResult(); + } + + /** + * Submits the answer for an image captcha verification by sending a remote procedure call request. + * + * @param string $verificationCode The code provided as the answer to the image captcha. + * @return true Returns true if the captcha answer is successfully verified. + * @throws RpcException Thrown if the RPC request fails. + */ + public function verificationAnswerImageCaptcha(string $verificationCode): true + { + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::VERIFICATION_ANSWER_IMAGE_CAPTCHA->value, Utilities::randomCrc32(), [ + 'verification_code' => $verificationCode + ]) + )->getResponse()->getResult(); + } + + /** + * Retrieves the text captcha verification response. + * + * @return string The result of the text captcha verification request. + * @throws RpcException Thrown if the RPC request fails. + */ + public function verificationGetTextCaptcha(): string + { + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::VERIFICATION_GET_TEXT_CAPTCHA->value, Utilities::randomCrc32()) + )->getResponse()->getResult(); + } + + /** + * Sends a request to answer a text-based captcha for verification purposes. + * + * @param string $verificationCode The code provided to answer the captcha. + * @return true Returns true if the captcha answer was successfully processed. + * @throws RpcException Thrown if the RPC request fails. + */ + public function verificationAnswerTextCaptcha(string $verificationCode): true + { + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::VERIFICATION_ANSWER_TEXT_CAPTCHA->value, Utilities::randomCrc32(), [ + 'verification_code' => $verificationCode + ]) + )->getResponse()->getResult(); + } + + /** + * Retrieves the external URL for verification purposes by sending a remote procedure call request. + * + * @return string The result of the verification URL request. + * @throws RpcException Thrown if the RPC request fails. + */ + public function verificationGetExternalUrl(): string + { + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::VERIFICATION_GET_EXTERNAL_URL->value, Utilities::randomCrc32()) + )->getResponse()->getResult(); + } + + /** + * Sends a verification code to answer an external URL for verification purposes. + * + * @param string $verificationCode The verification code to be sent. + * @return true The result of the verification operation. + * @throws RpcException Thrown if the RPC request fails. + */ + public function verificationAnswerExternalUrl(string $verificationCode): true + { + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::VERIFICATION_ANSWER_EXTERNAL_URL->value, Utilities::randomCrc32(), [ + 'verification_code' => $verificationCode + ]) + )->getResponse()->getResult(); + } + + /** + * Sets a new password for settings with optional hashing. + * + * @param string $password The password to be set. If hashing is enabled, the password will be hashed before being sent. + * @param bool $hash Optional. Determines whether the password should be hashed. Default is true. If false, the input is expected to be hashed using sha512. + * @return true Returns true if the password is successfully set. + * @throws RpcException Thrown if the RPC request fails. + */ + public function settingsSetPassword(string $password, bool $hash=true): true + { + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_SET_PASSWORD->value, Utilities::randomCrc32(), [ + 'password' => $hash ? hash('sha512', $password) : $password + ]) + )->getResponse()->getResult(); + } + + /** + * Updates the OTP setting by sending a remote procedure call request with the provided OTP. + * + * @param string $otp The OTP to be set. If hashing is enabled, it will be hashed using SHA-512. + * @throws RpcException Thrown if the RPC request fails. + */ + public function settingsSetOtp(string $otp, bool $hash=true): true + { + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_SET_OTP->value, Utilities::randomCrc32(), [ + 'otp' => $hash ? hash('sha512', $otp) : $otp + ]) + )->getResponse()->getResult(); + } + + /** + * Sets the display name in the settings by sending a remote procedure call request. + * + * @param string $displayName The new display name to be set. + * @return true Returns true upon successful update of the display name. + * @throws RpcException Thrown if the RPC request fails. + */ + public function settingsSetDisplayName(string $displayName): true + { + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_SET_DISPLAY_NAME->value, Utilities::randomCrc32(), [ + 'display_name' => $displayName + ]) + )->getResponse()->getResult(); + } + + /** + * Updates the display picture by sending a remote procedure call request with the specified file identifier. + * + * @param string $fileId The identifier of the file to be set as the display picture. + * @return true Returns true upon successful update of the + * @throws RpcException Thrown if the RPC request fails. + */ + public function settingsSetDisplayPicture(string $fileId): true + { + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_SET_DISPLAY_PICTURE->value, Utilities::randomCrc32(), [ + 'file_id' => $fileId + ]) + )->getResponse()->getResult(); + } + + /** + * Updates the email address for the settings by sending a remote procedure call request. + * + * @param string $emailAddress The new email address to set. + * @return true Returns true if the email address was successfully updated. + * @throws RpcException Thrown if the RPC request fails. + */ + public function settingsSetEmail(string $emailAddress): true + { + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_SET_EMAIL->value, Utilities::randomCrc32(), [ + 'email_address' => $emailAddress + ]) + )->getResponse()->getResult(); + } + + /** + * Updates the phone number in the settings by sending a remote procedure call request. + * + * @param string $phoneNumber The phone number to be set in the settings. + * @return true Returns true if the operation was successful + * @throws RpcException Thrown if the RPC request fails. + */ + public function settingsSetPhone(string $phoneNumber): true + { + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_SET_DISPLAY_NAME->value, Utilities::randomCrc32(), [ + 'phone_number' => $phoneNumber + ]) + )->getResponse()->getResult(); + } + + /** + * Updates the user's birthday by sending a remote procedure call request with the specified date. + * + * @param int $year The year of the user's birthday. + * @param int $month The month of the user's birthday. + * @param int $day The day of the user's birthday. + * @return true Returns true if the birthday was successfully updated. + * @throws RpcException Thrown if the RPC request fails. + */ + public function settingsSetBirthday(int $year, int $month, int $day): true + { + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_SET_BIRTHDAY->value, Utilities::randomCrc32(), [ + 'year' => $year, + 'month' => $month, + 'day' => $day + ]) )->getResponse()->getResult(); } } \ No newline at end of file diff --git a/tests/test.php b/tests/test.php index f176dbe..9963e5f 100644 --- a/tests/test.php +++ b/tests/test.php @@ -6,6 +6,7 @@ $client = new \Socialbox\SocialClient(generateRandomPeer()); var_dump($client->getSessionState()); + function generateRandomPeer() { $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; From 395e6b95ff9ff7259de440605552e72af71c8b95 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 23 Dec 2024 19:02:37 -0500 Subject: [PATCH 066/420] Refactor and extend configuration classes. --- .idea/php.xml | 7 +- .idea/socialbox-php.iml | 2 - composer.json | 3 +- .../ClientCommands/StorageConfiguration.php | 52 +++ src/Socialbox/Classes/Configuration.php | 438 ++++++++++-------- .../Configuration/CacheConfiguration.php | 204 +++++--- .../Configuration/DatabaseConfiguration.php | 111 +++-- .../Configuration/LoggingConfiguration.php | 222 ++++----- .../RegistrationConfiguration.php | 339 ++++++++------ .../Configuration/SecurityConfiguration.php | 94 ++-- src/Socialbox/Classes/Resources.php | 18 + .../documents/community_guidelines.html | 1 + .../AcceptCommunityGuidelines.php | 37 ++ .../StandardMethods/AcceptPrivacyPolicy.php | 19 +- .../StandardMethods/AcceptTermsOfService.php | 19 +- .../GetCommunityGuidelines.php | 28 ++ .../StandardMethods/GetPrivacyPolicy.php | 8 +- .../StandardMethods/GetTermsOfService.php | 8 +- .../VerificationGetImageCaptcha.php | 1 + src/Socialbox/Classes/Utilities.php | 60 +++ src/Socialbox/Enums/StandardMethods.php | 43 +- src/Socialbox/Managers/SessionManager.php | 17 + .../Objects/Standard/ServerDocument.php | 76 +++ src/Socialbox/SocialClient.php | 33 +- 24 files changed, 1158 insertions(+), 682 deletions(-) create mode 100644 src/Socialbox/Classes/ClientCommands/StorageConfiguration.php create mode 100644 src/Socialbox/Classes/Resources/documents/community_guidelines.html create mode 100644 src/Socialbox/Classes/StandardMethods/AcceptCommunityGuidelines.php create mode 100644 src/Socialbox/Classes/StandardMethods/GetCommunityGuidelines.php create mode 100644 src/Socialbox/Objects/Standard/ServerDocument.php diff --git a/.idea/php.xml b/.idea/php.xml index 26fbabf..acc3054 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -58,7 +58,7 @@ - + @@ -77,7 +77,7 @@ - + @@ -94,9 +94,6 @@ - - - diff --git a/.idea/socialbox-php.iml b/.idea/socialbox-php.iml index 47ac8d8..9ac0b9d 100644 --- a/.idea/socialbox-php.iml +++ b/.idea/socialbox-php.iml @@ -2,8 +2,6 @@ - - diff --git a/composer.json b/composer.json index 05de501..2386ce4 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ "ext-openssl": "*", "ext-redis": "*", "ext-memcached": "*", - "ext-curl": "*" + "ext-curl": "*", + "ext-gd": "*" } } \ No newline at end of file diff --git a/src/Socialbox/Classes/ClientCommands/StorageConfiguration.php b/src/Socialbox/Classes/ClientCommands/StorageConfiguration.php new file mode 100644 index 0000000..913c3d4 --- /dev/null +++ b/src/Socialbox/Classes/ClientCommands/StorageConfiguration.php @@ -0,0 +1,52 @@ +path = $data['path']; + $this->userDisplayImagesPath = $data['user_display_images_path']; + $this->userDisplayImagesMaxSize = $data['user_display_images_max_size']; + } + + /** + * Retrieves the base path value. + * + * @return string The base path. + */ + public function getPath(): string + { + return $this->path; + } + + /** + * Retrieves the path for user display images. + * + * @return string The path where user display images are stored. + */ + public function getUserDisplayImagesPath(): string + { + return $this->userDisplayImagesPath; + } + + /** + * Retrieves the maximum size allowed for user display images. + * + * @return int The maximum size in bytes. + */ + public function getUserDisplayImagesMaxSize(): int + { + return $this->userDisplayImagesMaxSize; + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/Configuration.php b/src/Socialbox/Classes/Configuration.php index 07fc492..b1aa90b 100644 --- a/src/Socialbox/Classes/Configuration.php +++ b/src/Socialbox/Classes/Configuration.php @@ -1,236 +1,264 @@ setDefault('instance.enabled', false); // False by default, requires the user to enable it. - $config->setDefault('instance.domain', null); - $config->setDefault('instance.rpc_endpoint', null); - $config->setDefault('instance.encryption_keys_count', 5); - $config->setDefault('instance.encryption_records_count', 5); - $config->setDefault('instance.private_key', null); - $config->setDefault('instance.public_key', null); - $config->setDefault('instance.encryption_keys', null); - - // Security Configuration - $config->setDefault('security.display_internal_exceptions', false); - $config->setDefault('security.resolved_servers_ttl', 600); - $config->setDefault('security.captcha_ttl', 200); - - // Database configuration - $config->setDefault('database.host', '127.0.0.1'); - $config->setDefault('database.port', 3306); - $config->setDefault('database.username', 'root'); - $config->setDefault('database.password', 'root'); - $config->setDefault('database.name', 'test'); - - // Logging configuration - $config->setDefault('logging.console_logging_enabled', true); - $config->setDefault('logging.console_logging_level', 'info'); - $config->setDefault('logging.file_logging_enabled', true); - $config->setDefault('logging.file_logging_level', 'error'); - - // Cache layer configuration - $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.sessions.enabled', true); - $config->setDefault('cache.sessions.ttl', 3600); - $config->setDefault('cache.sessions.max', 1000); - - // Registration configuration - $config->setDefault('registration.enabled', true); - $config->setDefault('registration.privacy_policy_document', null); - $config->setDefault('registration.accept_privacy_policy', true); - $config->setDefault('registration.terms_of_service_document', null); - $config->setDefault('registration.accept_terms_of_service', null); - $config->setDefault('registration.password_required', true); - $config->setDefault('registration.otp_required', false); - $config->setDefault('registration.display_name_required', false); - $config->setDefault('registration.email_verification_required', false); - $config->setDefault('registration.sms_verification_required', false); - $config->setDefault('registration.phone_call_verification_required', false); - $config->setDefault('registration.image_captcha_verification_required', true); - - $config->save(); - - self::$configuration = $config; - self::$instanceConfiguration = new InstanceConfiguration(self::$configuration->getConfiguration()['instance']); - self::$securityConfiguration = new SecurityConfiguration(self::$configuration->getConfiguration()['security']); - self::$databaseConfiguration = new DatabaseConfiguration(self::$configuration->getConfiguration()['database']); - self::$loggingConfiguration = new LoggingConfiguration(self::$configuration->getConfiguration()['logging']); - self::$cacheConfiguration = new CacheConfiguration(self::$configuration->getConfiguration()['cache']); - self::$registrationConfiguration = new RegistrationConfiguration(self::$configuration->getConfiguration()['registration']); - } - - /** - * Resets all configuration instances by setting them to null and then - * reinitializes the configurations. - * - * @return void - */ - public static function reload(): void - { - self::$configuration = null; - self::$instanceConfiguration = null; - self::$securityConfiguration = null; - self::$databaseConfiguration = null; - self::$loggingConfiguration = null; - self::$cacheConfiguration = null; - self::$registrationConfiguration = null; - - self::initializeConfiguration(); - } - - /** - * Retrieves the current configuration array. If the configuration is not initialized, - * it triggers the initialization process. - * - * @return array The current configuration array. - */ - public static function getConfiguration(): array - { - if(self::$configuration === null) + /** + * Initializes the configuration settings for the application. This includes + * settings for the instance, security, database, cache layer, and registration. + * + * @return void + */ + private static function initializeConfiguration(): void { + $config = new \ConfigLib\Configuration('socialbox'); + + // Instance configuration + $config->setDefault('instance.enabled', false); // False by default, requires the user to enable it. + $config->setDefault('instance.domain', null); + $config->setDefault('instance.rpc_endpoint', null); + $config->setDefault('instance.encryption_keys_count', 5); + $config->setDefault('instance.encryption_records_count', 5); + $config->setDefault('instance.private_key', null); + $config->setDefault('instance.public_key', null); + $config->setDefault('instance.encryption_keys', null); + + // Security Configuration + $config->setDefault('security.display_internal_exceptions', false); + $config->setDefault('security.resolved_servers_ttl', 600); + $config->setDefault('security.captcha_ttl', 200); + + // Database configuration + $config->setDefault('database.host', '127.0.0.1'); + $config->setDefault('database.port', 3306); + $config->setDefault('database.username', 'root'); + $config->setDefault('database.password', 'root'); + $config->setDefault('database.name', 'test'); + + // Logging configuration + $config->setDefault('logging.console_logging_enabled', true); + $config->setDefault('logging.console_logging_level', 'info'); + $config->setDefault('logging.file_logging_enabled', true); + $config->setDefault('logging.file_logging_level', 'error'); + + // Cache layer configuration + $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.sessions.enabled', true); + $config->setDefault('cache.sessions.ttl', 3600); + $config->setDefault('cache.sessions.max', 1000); + + // Registration configuration + $config->setDefault('registration.enabled', true); + $config->setDefault('registration.privacy_policy_document', null); + $config->setDefault('registration.privacy_policy_date', 1734985525); + $config->setDefault('registration.accept_privacy_policy', true); + $config->setDefault('registration.terms_of_service_document', null); + $config->setDefault('registration.terms_of_service_date', 1734985525); + $config->setDefault('registration.accept_terms_of_service', true); + $config->setDefault('registration.community_guidelines_document', null); + $config->setDefault('registration.community_guidelines_date', 1734985525); + $config->setDefault('registration.accept_community_guidelines', true); + $config->setDefault('registration.password_required', true); + $config->setDefault('registration.otp_required', false); + $config->setDefault('registration.display_name_required', true); + $config->setDefault('registration.display_picture_required', false); + $config->setDefault('registration.image_captcha_verification_required', true); + + // Storage configuration + $config->setDefault('storage.path', '/etc/socialbox'); // The main path for file storage + $config->setDefault('storage.user_display_images_path', 'user_profiles'); // eg; `/etc/socialbox/user_profiles` + $config->setDefault('storage.user_display_images_max_size', 3145728); // 3MB + + $config->save(); + + self::$configuration = $config; + self::$instanceConfiguration = new InstanceConfiguration(self::$configuration->getConfiguration()['instance']); + self::$securityConfiguration = new SecurityConfiguration(self::$configuration->getConfiguration()['security']); + self::$databaseConfiguration = new DatabaseConfiguration(self::$configuration->getConfiguration()['database']); + self::$loggingConfiguration = new LoggingConfiguration(self::$configuration->getConfiguration()['logging']); + self::$cacheConfiguration = new CacheConfiguration(self::$configuration->getConfiguration()['cache']); + self::$registrationConfiguration = new RegistrationConfiguration(self::$configuration->getConfiguration()['registration']); + self::$storageConfiguration = new StorageConfiguration(self::$configuration->getConfiguration()['storage']); + } + + /** + * Resets all configuration instances by setting them to null and then + * reinitializes the configurations. + * + * @return void + */ + public static function reload(): void + { + self::$configuration = null; + self::$instanceConfiguration = null; + self::$securityConfiguration = null; + self::$databaseConfiguration = null; + self::$loggingConfiguration = null; + self::$cacheConfiguration = null; + self::$registrationConfiguration = null; + self::initializeConfiguration(); } - return self::$configuration->getConfiguration(); - } - - public static function getConfigurationLib(): \ConfigLib\Configuration - { - if(self::$configuration === null) + /** + * Retrieves the current configuration array. If the configuration is not initialized, + * it triggers the initialization process. + * + * @return array The current configuration array. + */ + public static function getConfiguration(): array { - self::initializeConfiguration(); + if(self::$configuration === null) + { + self::initializeConfiguration(); + } + + return self::$configuration->getConfiguration(); } - return self::$configuration; - } - - /** - * Retrieves the current instance configuration. - * - * @return InstanceConfiguration The current instance configuration instance. - */ - public static function getInstanceConfiguration(): InstanceConfiguration - { - if(self::$instanceConfiguration === null) + public static function getConfigurationLib(): \ConfigLib\Configuration { - self::initializeConfiguration(); + if(self::$configuration === null) + { + self::initializeConfiguration(); + } + + return self::$configuration; } - return self::$instanceConfiguration; - } - - /** - * Retrieves the current security configuration. - * - * @return SecurityConfiguration The current security configuration instance. - */ - public static function getSecurityConfiguration(): SecurityConfiguration - { - if(self::$securityConfiguration === null) + /** + * Retrieves the current instance configuration. + * + * @return InstanceConfiguration The current instance configuration instance. + */ + public static function getInstanceConfiguration(): InstanceConfiguration { - self::initializeConfiguration(); + if(self::$instanceConfiguration === null) + { + self::initializeConfiguration(); + } + + return self::$instanceConfiguration; } - return self::$securityConfiguration; - } - - /** - * Retrieves the current database configuration. - * - * @return DatabaseConfiguration The configuration settings for the database. - */ - public static function getDatabaseConfiguration(): DatabaseConfiguration - { - if(self::$databaseConfiguration === null) + /** + * Retrieves the current security configuration. + * + * @return SecurityConfiguration The current security configuration instance. + */ + public static function getSecurityConfiguration(): SecurityConfiguration { - self::initializeConfiguration(); + if(self::$securityConfiguration === null) + { + self::initializeConfiguration(); + } + + return self::$securityConfiguration; } - return self::$databaseConfiguration; - } - - /** - * Retrieves the current logging configuration. - * - * @return LoggingConfiguration The current logging configuration instance. - */ - public static function getLoggingConfiguration(): LoggingConfiguration - { - if(self::$loggingConfiguration === null) + /** + * Retrieves the current database configuration. + * + * @return DatabaseConfiguration The configuration settings for the database. + */ + public static function getDatabaseConfiguration(): DatabaseConfiguration { - self::initializeConfiguration(); + if(self::$databaseConfiguration === null) + { + self::initializeConfiguration(); + } + + return self::$databaseConfiguration; } - return self::$loggingConfiguration; - } - - /** - * Retrieves the current cache configuration. If the cache configuration - * has not been initialized, it will initialize it first. - * - * @return CacheConfiguration The current cache configuration instance. - */ - public static function getCacheConfiguration(): CacheConfiguration - { - if(self::$cacheConfiguration === null) + /** + * Retrieves the current logging configuration. + * + * @return LoggingConfiguration The current logging configuration instance. + */ + public static function getLoggingConfiguration(): LoggingConfiguration { - self::initializeConfiguration(); + if(self::$loggingConfiguration === null) + { + self::initializeConfiguration(); + } + + return self::$loggingConfiguration; } - return self::$cacheConfiguration; - } - - /** - * Retrieves the registration configuration. - * - * This method returns the current RegistrationConfiguration instance. - * If the configuration has not been initialized yet, it initializes it first. - * - * @return RegistrationConfiguration The registration configuration instance. - */ - public static function getRegistrationConfiguration(): RegistrationConfiguration - { - if(self::$registrationConfiguration === null) + /** + * Retrieves the current cache configuration. If the cache configuration + * has not been initialized, it will initialize it first. + * + * @return CacheConfiguration The current cache configuration instance. + */ + public static function getCacheConfiguration(): CacheConfiguration { - self::initializeConfiguration(); + if(self::$cacheConfiguration === null) + { + self::initializeConfiguration(); + } + + return self::$cacheConfiguration; } - return self::$registrationConfiguration; - } -} \ No newline at end of file + /** + * Retrieves the registration configuration. + * + * This method returns the current RegistrationConfiguration instance. + * If the configuration has not been initialized yet, it initializes it first. + * + * @return RegistrationConfiguration The registration configuration instance. + */ + public static function getRegistrationConfiguration(): RegistrationConfiguration + { + if(self::$registrationConfiguration === null) + { + self::initializeConfiguration(); + } + + return self::$registrationConfiguration; + } + + /** + * Retrieves the storage configuration. + * + * This method returns the current StorageConfiguration instance. + * If the configuration has not been initialized yet, it initializes it first. + * + * @return StorageConfiguration The storage configuration instance. + */ + public static function getStorageConfiguration(): StorageConfiguration + { + if(self::$storageConfiguration === null) + { + self::initializeConfiguration(); + } + + return self::$storageConfiguration; + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/Configuration/CacheConfiguration.php b/src/Socialbox/Classes/Configuration/CacheConfiguration.php index 5f5d8f4..05ba6bb 100644 --- a/src/Socialbox/Classes/Configuration/CacheConfiguration.php +++ b/src/Socialbox/Classes/Configuration/CacheConfiguration.php @@ -1,83 +1,151 @@ enabled = (bool)$data['enabled']; - $this->engine = (string)$data['engine']; - $this->host = (string)$data['host']; - $this->port = (int)$data['port']; - $this->username = $data['username'] ? (string)$data['username'] : null; - $this->password = $data['password'] ? (string)$data['password'] : null; - $this->database = $data['database'] ? (int)$data['database'] : null; + private bool $enabled; + private string $engine; + private string $host; + private int $port; + private ?string $username; + private ?string $password; + private ?int $database; - $this->sessionsEnabled = (bool)$data['sessions']['enabled']; - $this->sessionsTtl = (int)$data['sessions']['ttl']; - $this->sessionsMax = (int)$data['sessions']['max']; - } + private bool $sessionsEnabled; + private int $sessionsTtl; + private int $sessionsMax; - public function isEnabled(): bool - { - return $this->enabled; - } + /** + * Constructor to initialize configuration values. + * + * @param array $data An associative array containing configuration data. + * Keys include: + * - enabled (bool): Whether the feature is enabled. + * - engine (string): The engine type. + * - host (string): The host address. + * - port (int): The port number. + * - username (string|null): The username for authentication. + * - password (string|null): The password for authentication. + * - database (int|null): The database ID. + * - sessions (array): Session-specific settings. Keys include: + * - enabled (bool): Whether sessions are enabled. + * - ttl (int): Session time-to-live in seconds. + * - max (int): Maximum number of concurrent sessions. + * + * @return void + */ + public function __construct(array $data) + { + $this->enabled = (bool)$data['enabled']; + $this->engine = (string)$data['engine']; + $this->host = (string)$data['host']; + $this->port = (int)$data['port']; + $this->username = $data['username'] ? (string)$data['username'] : null; + $this->password = $data['password'] ? (string)$data['password'] : null; + $this->database = $data['database'] ? (int)$data['database'] : null; + $this->sessionsEnabled = (bool)$data['sessions']['enabled']; + $this->sessionsTtl = (int)$data['sessions']['ttl']; + $this->sessionsMax = (int)$data['sessions']['max']; + } - public function getEngine(): string - { - return $this->engine; - } + /** + * Checks whether the feature is enabled. + * + * @return bool Returns true if the feature is enabled, false otherwise. + */ + public function isEnabled(): bool + { + return $this->enabled; + } - public function getHost(): string - { - return $this->host; - } + /** + * Retrieves the engine name. + * + * @return string Returns the name of the engine. + */ + public function getEngine(): string + { + return $this->engine; + } - public function getPort(): int - { - return $this->port; - } + /** + * Retrieves the host value. + * + * @return string The host as a string. + */ + public function getHost(): string + { + return $this->host; + } - public function getUsername(): ?string - { - return $this->username; - } + /** + * Retrieves the port value. + * + * @return int The port number. + */ + public function getPort(): int + { + return $this->port; + } - public function getPassword(): ?string - { - return $this->password; - } + /** + * Retrieves the username value. + * + * @return string|null The username, or null if not set. + */ + public function getUsername(): ?string + { + return $this->username; + } - public function getDatabase(): ?int - { - return $this->database; - } + /** + * Retrieves the password value. + * + * @return string|null The password as a string or null if not set. + */ + public function getPassword(): ?string + { + return $this->password; + } - public function isSessionsEnabled(): bool - { - return $this->sessionsEnabled; - } + /** + * Retrieves the database identifier. + * + * @return int|null The database identifier or null if not set. + */ + public function getDatabase(): ?int + { + return $this->database; + } - public function getSessionsTtl(): int - { - return $this->sessionsTtl; - } + /** + * Checks whether sessions are enabled. + * + * @return bool Returns true if sessions are enabled, otherwise false. + */ + public function isSessionsEnabled(): bool + { + return $this->sessionsEnabled; + } - public function getSessionsMax(): int - { - return $this->sessionsMax; - } -} \ No newline at end of file + /** + * Retrieves the time-to-live (TTL) value for sessions. + * + * @return int The TTL value for sessions. + */ + public function getSessionsTtl(): int + { + return $this->sessionsTtl; + } + + /** + * Retrieves the maximum number of sessions allowed. + * + * @return int Returns the maximum number of sessions. + */ + public function getSessionsMax(): int + { + return $this->sessionsMax; + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/Configuration/DatabaseConfiguration.php b/src/Socialbox/Classes/Configuration/DatabaseConfiguration.php index 2eb427c..1093f68 100644 --- a/src/Socialbox/Classes/Configuration/DatabaseConfiguration.php +++ b/src/Socialbox/Classes/Configuration/DatabaseConfiguration.php @@ -1,46 +1,83 @@ host = (string)$data['host']; - $this->port = (int)$data['port']; - $this->username = (string)$data['username']; - $this->password = $data['password'] ? (string)$data['password'] : null; - $this->name = (string)$data['name']; - } + private string $host; + private int $port; + private string $username; + private ?string $password; + private string $name; - public function getHost(): string - { - return $this->host; - } + /** + * Constructor method to initialize properties from the provided data array. + * + * @param array $data Associative array containing the keys 'host', 'port', 'username', 'password', and 'name'. + * - 'host' (string): The host of the server. + * - 'port' (int): The port number. + * - 'username' (string): The username for authentication. + * - 'password' (string|null): The password for authentication, optional. + * - 'name' (string): The name associated with the connection or resource. + * + * @return void + */ + public function __construct(array $data) + { + $this->host = (string)$data['host']; + $this->port = (int)$data['port']; + $this->username = (string)$data['username']; + $this->password = $data['password'] ? (string)$data['password'] : null; + $this->name = (string)$data['name']; + } - public function getPort(): int - { - return $this->port; - } + /** + * Retrieves the host value. + * + * @return string The value of the host. + */ + public function getHost(): string + { + return $this->host; + } - public function getUsername(): string - { - return $this->username; - } + /** + * Retrieves the port value. + * + * @return int The value of the port. + */ + public function getPort(): int + { + return $this->port; + } - public function getPassword(): ?string - { - return $this->password; - } + /** + * Retrieves the username value. + * + * @return string The value of the username. + */ + public function getUsername(): string + { + return $this->username; + } - public function getName(): string - { - return $this->name; - } -} \ No newline at end of file + /** + * Retrieves the password value. + * + * @return string|null The value of the password, or null if not set. + */ + public function getPassword(): ?string + { + return $this->password; + } + + /** + * Retrieves the name value. + * + * @return string The value of the name + */ + public function getName(): string + { + return $this->name; + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/Configuration/LoggingConfiguration.php b/src/Socialbox/Classes/Configuration/LoggingConfiguration.php index a499d53..2830648 100644 --- a/src/Socialbox/Classes/Configuration/LoggingConfiguration.php +++ b/src/Socialbox/Classes/Configuration/LoggingConfiguration.php @@ -1,115 +1,121 @@ consoleLoggingEnabled = (bool) $data['console_logging_enabled']; - $this->consoleLoggingLevel = $data['console_logging_level']; - $this->fileLoggingEnabled = (bool) $data['file_logging_enabled']; - $this->fileLoggingLevel = $data['file_logging_level']; - } + private bool $consoleLoggingEnabled; + private string $consoleLoggingLevel; + private bool $fileLoggingEnabled; + private string $fileLoggingLevel; - /** - * Checks if console logging is enabled. - * - * @return bool True if console logging is enabled, otherwise false. - */ - public function isConsoleLoggingEnabled(): bool - { - return $this->consoleLoggingEnabled; - } - - /** - * Retrieves the logging level for console output. - * - * @return LogLevel The logging level configured for console output. - */ - public function getConsoleLoggingLevel(): LogLevel - { - return $this->parseLogLevel($this->consoleLoggingLevel); - } - - /** - * Checks if file logging is enabled. - * - * @return bool True if file logging is enabled, false otherwise. - */ - public function isFileLoggingEnabled(): bool - { - return $this->fileLoggingEnabled; - } - - /** - * Retrieves the logging level for file logging. - * - * @return LogLevel The logging level set for file logging. - */ - public function getFileLoggingLevel(): LogLevel - { - return $this->parseLogLevel($this->fileLoggingLevel); - } - - /** - * Parses the given log level from string format to a LogLevel enumeration. - * - * @param string $logLevel The log level as a string. - * @return LogLevel The corresponding LogLevel enumeration. - */ - private function parseLogLevel(string $logLevel): LogLevel - { - switch (strtolower($logLevel)) { - case LogLevel::DEBUG: - case 'debug': - case '6': - case 'dbg': - return LogLevel::DEBUG; - case LogLevel::VERBOSE: - case 'verbose': - case '5': - case 'vrb': - return LogLevel::VERBOSE; - default: - case LogLevel::INFO: - case 'info': - case '4': - case 'inf': - return LogLevel::INFO; - case LogLevel::WARNING: - case 'warning': - case '3': - case 'wrn': - return LogLevel::WARNING; - case LogLevel::ERROR: - case 'error': - case '2': - case 'err': - return LogLevel::ERROR; - case LogLevel::FATAL: - case 'fatal': - case '1': - case 'crt': - return LogLevel::FATAL; - case LogLevel::SILENT: - case 'silent': - case '0': - case 'sil': - return LogLevel::SILENT; + /** + * Initializes a new instance of the class with the given configuration data. + * + * @param array $data An associative array containing logging configuration settings. + * @return void + */ + public function __construct(array $data) + { + $this->consoleLoggingEnabled = (bool) $data['console_logging_enabled']; + $this->consoleLoggingLevel = $data['console_logging_level']; + $this->fileLoggingEnabled = (bool) $data['file_logging_enabled']; + $this->fileLoggingLevel = $data['file_logging_level']; } - } -} \ No newline at end of file + + /** + * Checks if console logging is enabled. + * + * @return bool True if console logging is enabled, otherwise false. + */ + public function isConsoleLoggingEnabled(): bool + { + return $this->consoleLoggingEnabled; + } + + /** + * Retrieves the logging level for console output. + * + * @return LogLevel The logging level configured for console output. + */ + public function getConsoleLoggingLevel(): LogLevel + { + return $this->parseLogLevel($this->consoleLoggingLevel); + } + + /** + * Checks if file logging is enabled. + * + * @return bool True if file logging is enabled, false otherwise. + */ + public function isFileLoggingEnabled(): bool + { + return $this->fileLoggingEnabled; + } + + /** + * Retrieves the logging level for file logging. + * + * @return LogLevel The logging level set for file logging. + */ + public function getFileLoggingLevel(): LogLevel + { + return $this->parseLogLevel($this->fileLoggingLevel); + } + + /** + * Parses the given log level from string format to a LogLevel enumeration. + * + * @param string $logLevel The log level as a string. + * @return LogLevel The corresponding LogLevel enumeration. + */ + private function parseLogLevel(string $logLevel): LogLevel + { + switch (strtolower($logLevel)) + { + case LogLevel::DEBUG: + case 'debug': + case '6': + case 'dbg': + return LogLevel::DEBUG; + + case LogLevel::VERBOSE: + case 'verbose': + case '5': + case 'vrb': + return LogLevel::VERBOSE; + + default: + case LogLevel::INFO: + case 'info': + case '4': + case 'inf': + return LogLevel::INFO; + + case LogLevel::WARNING: + case 'warning': + case '3': + case 'wrn': + return LogLevel::WARNING; + case LogLevel::ERROR: + case 'error': + case '2': + case 'err': + return LogLevel::ERROR; + + case LogLevel::FATAL: + case 'fatal': + case '1': + case 'crt': + return LogLevel::FATAL; + + case LogLevel::SILENT: + case 'silent': + case '0': + case 'sil': + return LogLevel::SILENT; + } + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/Configuration/RegistrationConfiguration.php b/src/Socialbox/Classes/Configuration/RegistrationConfiguration.php index 29ebcea..b909c9f 100644 --- a/src/Socialbox/Classes/Configuration/RegistrationConfiguration.php +++ b/src/Socialbox/Classes/Configuration/RegistrationConfiguration.php @@ -1,171 +1,202 @@ registrationEnabled = (bool)$data['enabled']; - $this->privacyPolicyDocument = $data['privacy_policy_document'] ?? null; - $this->acceptPrivacyPolicy = $data['accept_privacy_policy'] ?? true; - $this->termsOfServiceDocument = $data['terms_of_service_document'] ?? null; - $this->acceptTermsOfService = $data['accept_terms_of_service'] ?? true; - $this->passwordRequired = (bool)$data['password_required']; - $this->otpRequired = (bool)$data['otp_required']; - $this->displayNameRequired = (bool)$data['display_name_required']; - $this->emailVerificationRequired = (bool)$data['email_verification_required']; - $this->smsVerificationRequired = (bool)$data['sms_verification_required']; - $this->phoneCallVerificationRequired = (bool)$data['phone_call_verification_required']; - $this->imageCaptchaVerificationRequired = (bool)$data['image_captcha_verification_required']; - } + private bool $registrationEnabled; + private ?string $privacyPolicyDocument; + private int $privacyPolicyDate; + private bool $acceptPrivacyPolicy; + private ?string $termsOfServiceDocument; + private int $termsOfServiceDate; + private bool $acceptTermsOfService; + private ?string $communityGuidelinesDocument; + private int $communityGuidelinesDate; + private bool $acceptCommunityGuidelines; + private bool $passwordRequired; + private bool $otpRequired; + private bool $displayNameRequired; + private bool $displayPictureRequired; + private bool $imageCaptchaVerificationRequired; - /** - * Checks if the registration is enabled. - * - * @return bool True if registration is enabled, false otherwise. - */ - public function isRegistrationEnabled(): bool - { - return $this->registrationEnabled; - } + /** + * Constructor method for initializing verification requirements. + * + * @param array $data An associative array containing the following keys: + * 'registration_enabled', 'password_required', + * 'otp_required', 'display_name_required', + * 'email_verification_required', 'sms_verification_required', + * 'phone_call_verification_required', 'image_captcha_verification_required'. + * + * @return void + */ + public function __construct(array $data) + { + $this->registrationEnabled = (bool)$data['enabled']; + $this->privacyPolicyDocument = $data['privacy_policy_document'] ?? null; + $this->privacyPolicyDate = $data['privacy_policy_date'] ?? 0; + $this->acceptPrivacyPolicy = $data['accept_privacy_policy'] ?? true; + $this->termsOfServiceDocument = $data['terms_of_service_document'] ?? null; + $this->termsOfServiceDate = $data['terms_of_service_date'] ?? 0; + $this->acceptTermsOfService = $data['accept_terms_of_service'] ?? true; + $this->communityGuidelinesDocument = $data['community_guidelines_document'] ?? null; + $this->communityGuidelinesDate = $data['community_guidelines_date'] ?? 0; + $this->acceptCommunityGuidelines = $data['accept_community_guidelines'] ?? true; + $this->passwordRequired = (bool)$data['password_required']; + $this->otpRequired = (bool)$data['otp_required']; + $this->displayNameRequired = (bool)$data['display_name_required']; + $this->displayPictureRequired = (bool)$data['display_picture_required']; + $this->imageCaptchaVerificationRequired = (bool)$data['image_captcha_verification_required']; + } + /** + * Checks if the registration is enabled. + * + * @return bool True if registration is enabled, false otherwise. + */ + public function isRegistrationEnabled(): bool + { + return $this->registrationEnabled; + } - /** - * Retrieves the privacy policy document. - * - * @return ?string Returns the privacy policy document or null if not set. - */ - public function getPrivacyPolicyDocument(): ?string - { - return $this->privacyPolicyDocument; - } + /** + * Retrieves the privacy policy document. + * + * @return ?string Returns the privacy policy document or null if not set. + */ + public function getPrivacyPolicyDocument(): ?string + { + return $this->privacyPolicyDocument; + } - /** - * Checks if accepting the privacy policy is required. - * - * @return bool Returns true if the privacy policy must be accepted, false otherwise. - */ - public function isAcceptPrivacyPolicyRequired(): bool - { - return $this->acceptPrivacyPolicy; - } + /** + * Retrieves the date of the privacy policy. + * + * @return int Returns the date of the privacy policy. + */ + public function getPrivacyPolicyDate(): int + { + return $this->privacyPolicyDate; + } - /** - * Retrieves the terms of service document. - * - * @return ?string Returns the terms of service document or null if not set. - */ - public function getTermsOfServiceDocument(): ?string - { - return $this->termsOfServiceDocument; - } + /** + * Checks if accepting the privacy policy is required. + * + * @return bool Returns true if the privacy policy must be accepted, false otherwise. + */ + public function isAcceptPrivacyPolicyRequired(): bool + { + return $this->acceptPrivacyPolicy; + } - /** - * Checks if accepting the terms of service is required. - * - * @return bool Returns true if the terms of service must be accepted, false otherwise. - */ - public function isAcceptTermsOfServiceRequired(): bool - { - return $this->acceptTermsOfService; - } + /** + * Retrieves the terms of service document. + * + * @return ?string Returns the terms of service document or null if not set. + */ + public function getTermsOfServiceDocument(): ?string + { + return $this->termsOfServiceDocument; + } - /** - * Determines if a password is required. - * - * @return bool True if a password is required, false otherwise. - */ - public function isPasswordRequired(): bool - { - return $this->passwordRequired; - } + /** + * Retrieves the date of the terms of service. + * + * @return int Returns the date of the terms of service. + */ + public function getTermsOfServiceDate(): int + { + return $this->termsOfServiceDate; + } - /** - * Determines if OTP (One-Time Password) is required. - * - * @return bool True if OTP is required, false otherwise. - */ - public function isOtpRequired(): bool - { - return $this->otpRequired; - } + /** + * Checks if accepting the terms of service is required. + * + * @return bool Returns true if the terms of service must be accepted, false otherwise. + */ + public function isAcceptTermsOfServiceRequired(): bool + { + return $this->acceptTermsOfService; + } - /** - * Checks if a display name is required. - * - * @return bool Returns true if a display name is required, false otherwise. - */ - public function isDisplayNameRequired(): bool - { - return $this->displayNameRequired; - } + /** + * Retrieves the community guidelines document. + * + * @return ?string Returns the community guidelines document or null if not set. + */ + public function getCommunityGuidelinesDocument(): ?string + { + return $this->communityGuidelinesDocument; + } - /** - * Checks if email verification is required. - * - * @return bool Returns true if email verification is required, false otherwise. - */ - public function isEmailVerificationRequired(): bool - { - return $this->emailVerificationRequired; - } + /** + * Retrieves the date of the community guidelines. + * + * @return int Returns the date of the community guidelines. + */ + public function getCommunityGuidelinesDate(): int + { + return $this->communityGuidelinesDate; + } - /** - * Checks if SMS verification is required. - * - * @return bool Returns true if SMS verification is required, false otherwise. - */ - public function isSmsVerificationRequired(): bool - { - return $this->smsVerificationRequired; - } + /** + * Checks if accepting the community guidelines is required. + * + * @return bool Returns true if the community guidelines must be accepted, false otherwise. + */ + public function isAcceptCommunityGuidelinesRequired(): bool + { + return $this->acceptCommunityGuidelines; + } - /** - * Checks if phone call verification is required. - * - * @return bool Returns true if phone call verification is required, false otherwise. - */ - public function isPhoneCallVerificationRequired(): bool - { - return $this->phoneCallVerificationRequired; - } + /** + * Determines if a password is required. + * + * @return bool True if a password is required, false otherwise. + */ + public function isPasswordRequired(): bool + { + return $this->passwordRequired; + } - /** - * Determines if image CAPTCHA verification is required. - * - * @return bool Returns true if image CAPTCHA verification is required, false otherwise. - */ - public function isImageCaptchaVerificationRequired(): bool - { - return $this->imageCaptchaVerificationRequired; - } -} \ No newline at end of file + /** + * Determines if OTP (One-Time Password) is required. + * + * @return bool True if OTP is required, false otherwise. + */ + public function isOtpRequired(): bool + { + return $this->otpRequired; + } + + /** + * Checks if a display name is required. + * + * @return bool Returns true if a display name is required, false otherwise. + */ + public function isDisplayNameRequired(): bool + { + return $this->displayNameRequired; + } + + /** + * Checks if a display picture is required. + * + * @return bool Returns true if a display picture is required, false otherwise. + */ + public function isDisplayPictureRequired(): bool + { + return $this->displayPictureRequired; + } + + /** + * Determines if image CAPTCHA verification is required. + * + * @return bool Returns true if image CAPTCHA verification is required, false otherwise. + */ + public function isImageCaptchaVerificationRequired(): bool + { + return $this->imageCaptchaVerificationRequired; + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/Configuration/SecurityConfiguration.php b/src/Socialbox/Classes/Configuration/SecurityConfiguration.php index 805437c..72854c2 100644 --- a/src/Socialbox/Classes/Configuration/SecurityConfiguration.php +++ b/src/Socialbox/Classes/Configuration/SecurityConfiguration.php @@ -1,55 +1,55 @@ displayInternalExceptions = $data['display_internal_exceptions']; - $this->resolvedServersTtl = $data['resolved_servers_ttl']; - $this->captchaTtl = $data['captcha_ttl']; - } + private bool $displayInternalExceptions; + private int $resolvedServersTtl; + private int $captchaTtl; - /** - * Determines if the display of internal errors is enabled. - * - * @return bool True if the display of internal errors is enabled, false otherwise. - */ - public function isDisplayInternalExceptions(): bool - { - return $this->displayInternalExceptions; - } + /** + * Constructor method for initializing class properties. + * + * @param array $data An associative array containing values for initializing the properties. + * + * @return void + */ + public function __construct(array $data) + { + $this->displayInternalExceptions = $data['display_internal_exceptions']; + $this->resolvedServersTtl = $data['resolved_servers_ttl']; + $this->captchaTtl = $data['captcha_ttl']; + } - /** - * Retrieves the time-to-live (TTL) value for resolved servers. - * - * @return int The TTL value for resolved servers. - */ - public function getResolvedServersTtl(): int - { - return $this->resolvedServersTtl; - } + /** + * Determines if the display of internal errors is enabled. + * + * @return bool True if the display of internal errors is enabled, false otherwise. + */ + public function isDisplayInternalExceptions(): bool + { + return $this->displayInternalExceptions; + } - /** - * Retrieves the time-to-live (TTL) value for captchas. - * - * @return int The TTL value for captchas. - */ - public function getCaptchaTtl(): int - { - return $this->captchaTtl; - } + /** + * Retrieves the time-to-live (TTL) value for resolved servers. + * + * @return int The TTL value for resolved servers. + */ + public function getResolvedServersTtl(): int + { + return $this->resolvedServersTtl; + } -} \ No newline at end of file + /** + * Retrieves the time-to-live (TTL) value for captchas. + * + * @return int The TTL value for captchas. + */ + public function getCaptchaTtl(): int + { + return $this->captchaTtl; + } + + } \ No newline at end of file diff --git a/src/Socialbox/Classes/Resources.php b/src/Socialbox/Classes/Resources.php index a7c32cb..ca23c34 100644 --- a/src/Socialbox/Classes/Resources.php +++ b/src/Socialbox/Classes/Resources.php @@ -64,4 +64,22 @@ return file_get_contents(self::getDocumentResource('tos')); } + + /** + * Retrieves the community guidelines document content. + * + * @return string The content of the community guidelines document, either from a configured location + * or a default resource if the configured location is unavailable. + */ + public static function getCommunityGuidelines(): string + { + $configuredLocation = Configuration::getRegistrationConfiguration()->getCommunityGuidelinesDocument(); + if($configuredLocation !== null && file_exists($configuredLocation)) + { + return file_get_contents($configuredLocation); + } + + return file_get_contents(self::getDocumentResource('community')); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/Resources/documents/community_guidelines.html b/src/Socialbox/Classes/Resources/documents/community_guidelines.html new file mode 100644 index 0000000..2e08c36 --- /dev/null +++ b/src/Socialbox/Classes/Resources/documents/community_guidelines.html @@ -0,0 +1 @@ +

Community Guidelines

\ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/AcceptCommunityGuidelines.php b/src/Socialbox/Classes/StandardMethods/AcceptCommunityGuidelines.php new file mode 100644 index 0000000..16ff746 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/AcceptCommunityGuidelines.php @@ -0,0 +1,37 @@ +getSessionUuid(), [SessionFlags::VER_COMMUNITY_GUIDELINES]); + + // Check & update the session flow + SessionManager::updateFlow($request->getSession()); + } + catch (DatabaseOperationException $e) + { + return $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/AcceptPrivacyPolicy.php b/src/Socialbox/Classes/StandardMethods/AcceptPrivacyPolicy.php index d87ada5..a09c5cd 100644 --- a/src/Socialbox/Classes/StandardMethods/AcceptPrivacyPolicy.php +++ b/src/Socialbox/Classes/StandardMethods/AcceptPrivacyPolicy.php @@ -21,28 +21,17 @@ { try { + // Remove the verification flag SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::VER_PRIVACY_POLICY]); + + // Check & update the session flow + SessionManager::updateFlow($request->getSession()); } catch (DatabaseOperationException $e) { return $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, $e); } - // Check if all registration flags are removed - if(SessionFlags::isComplete($request->getSession()->getFlags())) - { - // Set the session as authenticated - try - { - SessionManager::setAuthenticated($request->getSessionUuid(), true); - SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::REGISTRATION_REQUIRED, SessionFlags::AUTHENTICATION_REQUIRED]); - } - catch (DatabaseOperationException $e) - { - return $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, $e); - } - } - return $rpcRequest->produceResponse(true); } } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/AcceptTermsOfService.php b/src/Socialbox/Classes/StandardMethods/AcceptTermsOfService.php index bd5fb8c..2732c2e 100644 --- a/src/Socialbox/Classes/StandardMethods/AcceptTermsOfService.php +++ b/src/Socialbox/Classes/StandardMethods/AcceptTermsOfService.php @@ -20,28 +20,17 @@ { try { + // Remove the verification flag SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::VER_TERMS_OF_SERVICE]); + + // Check & update the session flow + SessionManager::updateFlow($request->getSession()); } catch (DatabaseOperationException $e) { return $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, $e); } - // Check if all registration flags are removed - if(SessionFlags::isComplete($request->getSession()->getFlags())) - { - // Set the session as authenticated - try - { - SessionManager::setAuthenticated($request->getSessionUuid(), true); - SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::REGISTRATION_REQUIRED, SessionFlags::AUTHENTICATION_REQUIRED]); - } - catch (DatabaseOperationException $e) - { - return $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, $e); - } - } - return $rpcRequest->produceResponse(true); } } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/GetCommunityGuidelines.php b/src/Socialbox/Classes/StandardMethods/GetCommunityGuidelines.php new file mode 100644 index 0000000..4021f57 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/GetCommunityGuidelines.php @@ -0,0 +1,28 @@ +produceResponse(new ServerDocument([ + 'last_updated' => Configuration::getRegistrationConfiguration()->getCommunityGuidelinesDate(), + 'title' => 'Community Guidelines', + 'content' => Resources::getCommunityGuidelines() + ])); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/GetPrivacyPolicy.php b/src/Socialbox/Classes/StandardMethods/GetPrivacyPolicy.php index 20ab6c5..db83fd0 100644 --- a/src/Socialbox/Classes/StandardMethods/GetPrivacyPolicy.php +++ b/src/Socialbox/Classes/StandardMethods/GetPrivacyPolicy.php @@ -3,11 +3,13 @@ namespace Socialbox\Classes\StandardMethods; use Socialbox\Abstracts\Method; + use Socialbox\Classes\Configuration; use Socialbox\Classes\Resources; use Socialbox\Enums\StandardError; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\ClientRequest; use Socialbox\Objects\RpcRequest; + use Socialbox\Objects\Standard\ServerDocument; class GetPrivacyPolicy extends Method { @@ -17,6 +19,10 @@ */ public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { - return $rpcRequest->produceResponse(Resources::getPrivacyPolicy()); + return $rpcRequest->produceResponse(new ServerDocument([ + 'last_updated' => Configuration::getRegistrationConfiguration()->getPrivacyPolicyDate(), + 'title' => 'Privacy Policy', + 'content' => Resources::getPrivacyPolicy() + ])); } } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/GetTermsOfService.php b/src/Socialbox/Classes/StandardMethods/GetTermsOfService.php index c92a38f..96c1d51 100644 --- a/src/Socialbox/Classes/StandardMethods/GetTermsOfService.php +++ b/src/Socialbox/Classes/StandardMethods/GetTermsOfService.php @@ -3,11 +3,13 @@ namespace Socialbox\Classes\StandardMethods; use Socialbox\Abstracts\Method; + use Socialbox\Classes\Configuration; use Socialbox\Classes\Resources; use Socialbox\Enums\StandardError; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\ClientRequest; use Socialbox\Objects\RpcRequest; + use Socialbox\Objects\Standard\ServerDocument; class GetTermsOfService extends Method { @@ -17,6 +19,10 @@ */ public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { - return $rpcRequest->produceResponse(Resources::getTermsOfService()); + return $rpcRequest->produceResponse(new ServerDocument([ + 'last_updated' => Configuration::getRegistrationConfiguration()->getTermsOfServiceDate(), + 'title' => 'Terms of Service', + 'content' => Resources::getTermsOfService() + ])); } } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php b/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php index f935346..515938b 100644 --- a/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php +++ b/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php @@ -60,6 +60,7 @@ // Build the captcha // Returns HTML base64 encoded image of the captcha + // Important note: Must always be HTML-BASE64 which means it must be prefixed with `data:image/jpeg;base64,` return $rpcRequest->produceResponse(new ImageCaptcha([ 'expires' => $captchaRecord->getExpires(), 'content' => (new CaptchaBuilder($answer))->build()->inline() diff --git a/src/Socialbox/Classes/Utilities.php b/src/Socialbox/Classes/Utilities.php index d12ad20..ff38b98 100644 --- a/src/Socialbox/Classes/Utilities.php +++ b/src/Socialbox/Classes/Utilities.php @@ -194,6 +194,66 @@ class Utilities return preg_replace('/[^a-zA-Z0-9-_]/', '', $name); } + + /** + * Sanitizes a Base64-encoded JPEG image by validating its data, decoding it, + * and re-encoding it to ensure it conforms to the JPEG format. + * + * @param string $data The Base64-encoded string potentially containing a JPEG image, + * optionally prefixed with "data:image/...;base64,". + * @return string A sanitized and re-encoded JPEG image as a binary string. + * @throws InvalidArgumentException If the input data is not valid Base64, + * does not represent an image, or is not in the JPEG format. + */ + public static function sanitizeBase64Jpeg(string $data): string + { + // Detect and strip the potential "data:image/...;base64," prefix, if present + if (str_contains($data, ',')) + { + [, $data] = explode(',', $data, 2); + } + + // Decode the Base64 string + $decodedData = base64_decode($data, true); + + // Check if decoding succeeded + if ($decodedData === false) + { + throw new InvalidArgumentException("Invalid Base64 data."); + } + + // Temporarily load the decoded data as an image + $tempResource = imagecreatefromstring($decodedData); + + // Validate that the decoded data is indeed an image + if ($tempResource === false) + { + throw new InvalidArgumentException("The Base64 data does not represent a valid image."); + } + + // Validate MIME type using getimagesizefromstring + $imageInfo = getimagesizefromstring($decodedData); + if ($imageInfo === false || $imageInfo['mime'] !== 'image/jpeg') + { + imagedestroy($tempResource); // Cleanup resources + throw new InvalidArgumentException("The image is not a valid JPEG format."); + } + + // Capture the re-encoded image in memory and return it as a string + ob_start(); // Start output buffering + $saveResult = imagejpeg($tempResource, null, 100); // Max quality, save to output buffer + imagedestroy($tempResource); // Free up memory resources + + if (!$saveResult) + { + ob_end_clean(); // Clean the output buffer if encoding failed + throw new InvalidArgumentException("Failed to encode the sanitized image."); + } + + // Return the sanitized jpeg image as the result + return ob_get_clean(); + } + /** * Converts an array into a serialized string by joining the elements with a comma. * diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index e921744..0afad9c 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -2,8 +2,10 @@ namespace Socialbox\Enums; + use Socialbox\Classes\StandardMethods\AcceptCommunityGuidelines; use Socialbox\Classes\StandardMethods\AcceptPrivacyPolicy; use Socialbox\Classes\StandardMethods\AcceptTermsOfService; + use Socialbox\Classes\StandardMethods\GetCommunityGuidelines; use Socialbox\Classes\StandardMethods\GetPrivacyPolicy; use Socialbox\Classes\StandardMethods\GetSessionState; use Socialbox\Classes\StandardMethods\GetTermsOfService; @@ -74,11 +76,15 @@ self::ACCEPT_PRIVACY_POLICY => AcceptPrivacyPolicy::execute($request, $rpcRequest), self::GET_TERMS_OF_SERVICE => GetTermsOfService::execute($request, $rpcRequest), self::ACCEPT_TERMS_OF_SERVICE => AcceptTermsOfService::execute($request, $rpcRequest), + self::GET_COMMUNITY_GUIDELINES => GetCommunityGuidelines::execute($request, $rpcRequest), + self::ACCEPT_COMMUNITY_GUIDELINES => AcceptCommunityGuidelines::execute($request, $rpcRequest), self::VERIFICATION_GET_IMAGE_CAPTCHA => VerificationGetImageCaptcha::execute($request, $rpcRequest), self::VERIFICATION_ANSWER_IMAGE_CAPTCHA => VerificationAnswerImageCaptcha::execute($request, $rpcRequest), self::SETTINGS_SET_PASSWORD => SettingsSetPassword::execute($request, $rpcRequest), + + default => $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, sprintf("The method %s is not supported by the server", $rpcRequest->getMethod())) }; } @@ -107,36 +113,59 @@ */ public static function getAllowedMethods(ClientRequest $clientRequest): array { + // These methods should always accessible $methods = [ - self::PING, - self::GET_SESSION_STATE, - self::GET_PRIVACY_POLICY, - self::GET_TERMS_OF_SERVICE, + // Important methods + self::PING, // Always allow the ping method + self::GET_SESSION_STATE, // The session state should always be accessible + self::GET_PRIVACY_POLICY, // The user should always be able to get the privacy policy + self::GET_TERMS_OF_SERVICE, // The user should always be able to get the terms of service + self::GET_COMMUNITY_GUIDELINES, // The user should always be able to get the community guidelines ]; $session = $clientRequest->getSession(); - if(in_array(SessionFlags::VER_PRIVACY_POLICY, $session->getFlags())) + // If the flag `VER_PRIVACY_POLICY` is set, then the user can accept the privacy policy + if($session->flagExists(SessionFlags::VER_PRIVACY_POLICY)) { $methods[] = self::ACCEPT_PRIVACY_POLICY; } - if(in_array(SessionFlags::VER_TERMS_OF_SERVICE, $session->getFlags())) + // If the flag `VER_TERMS_OF_SERVICE` is set, then the user can accept the terms of service + if($session->flagExists(SessionFlags::VER_TERMS_OF_SERVICE)) { $methods[] = self::ACCEPT_TERMS_OF_SERVICE; } - if(in_array(SessionFlags::VER_IMAGE_CAPTCHA, $session->getFlags())) + // If the flag `VER_COMMUNITY_GUIDELINES` is set, then the user can accept the community guidelines + if($session->flagExists(SessionFlags::VER_COMMUNITY_GUIDELINES)) + { + $methods[] = self::ACCEPT_COMMUNITY_GUIDELINES; + } + + // If the flag `VER_IMAGE_CAPTCHA` is set, then the user has to get and answer an image captcha + if($session->flagExists(SessionFlags::VER_IMAGE_CAPTCHA)) { $methods[] = self::VERIFICATION_GET_IMAGE_CAPTCHA; $methods[] = self::VERIFICATION_ANSWER_IMAGE_CAPTCHA; } + // If the flag `SET_PASSWORD` is set, then the user has to set a password if(in_array(SessionFlags::SET_PASSWORD, $session->getFlags())) { $methods[] = self::SETTINGS_SET_PASSWORD; } + // If the user is authenticated, then preform additional method calls + if($session->isAuthenticated()) + { + // Always allow the authenticated user to change their password + if(!in_array(SessionFlags::SET_PASSWORD, $session->getFlags())) + { + $methods[] = self::SETTINGS_SET_PASSWORD; + } + } + return $methods; } } \ No newline at end of file diff --git a/src/Socialbox/Managers/SessionManager.php b/src/Socialbox/Managers/SessionManager.php index 0b50151..982a66b 100644 --- a/src/Socialbox/Managers/SessionManager.php +++ b/src/Socialbox/Managers/SessionManager.php @@ -454,4 +454,21 @@ throw new DatabaseOperationException('Failed to update authenticated peer', $e); } } + + /** + * Marks the session as complete if all necessary conditions are met. + * + * @param SessionRecord $session The session record to evaluate and potentially mark as complete. + * @throws DatabaseOperationException If there is an error while updating the session in the database. + * @throws StandardException If the session record cannot be found or if there is an error during retrieval. + * @return void + */ + public static function updateFlow(SessionRecord $session): void + { + if(SessionFlags::isComplete($session->getFlags())) + { + SessionManager::setAuthenticated($session->getUuid(), true); + SessionManager::removeFlags($session->getUuid(), [SessionFlags::REGISTRATION_REQUIRED, SessionFlags::AUTHENTICATION_REQUIRED]); + } + } } \ No newline at end of file diff --git a/src/Socialbox/Objects/Standard/ServerDocument.php b/src/Socialbox/Objects/Standard/ServerDocument.php new file mode 100644 index 0000000..2143eb2 --- /dev/null +++ b/src/Socialbox/Objects/Standard/ServerDocument.php @@ -0,0 +1,76 @@ +lastUpdated = $data['last_updated']; + $this->title = $data['title']; + $this->content = $data['content']; + } + + /** + * Retrieves the timestamp of the last update. + * + * @return int The last updated timestamp. + */ + public function getLastUpdated(): int + { + return $this->lastUpdated; + } + + /** + * Retrieves the title property. + * + * @return string The title value. + */ + public function getTitle(): string + { + return $this->title; + } + + /** + * Retrieves the content stored in the instance. + * + * @return string The content as a string. + */ + public function getContent(): string + { + return $this->content; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): object + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'last_updated' => $this->lastUpdated, + 'content_type' => $this->contentType, + 'title' => $this->title, + 'content' => $this->content + ]; + } + } \ No newline at end of file diff --git a/src/Socialbox/SocialClient.php b/src/Socialbox/SocialClient.php index 669508d..f23e79f 100644 --- a/src/Socialbox/SocialClient.php +++ b/src/Socialbox/SocialClient.php @@ -13,6 +13,7 @@ use Socialbox\Objects\ExportedSession; use Socialbox\Objects\PeerAddress; use Socialbox\Objects\RpcRequest; + use Socialbox\Objects\Standard\ServerDocument; use Socialbox\Objects\Standard\SessionState; class SocialClient extends RpcClient @@ -58,16 +59,16 @@ } /** - * Retrieves the privacy policy from the server. + * Fetches the privacy policy document by sending a remote procedure call request. * - * @return string Returns the privacy policy as a string. + * @return ServerDocument The privacy policy document retrieved from the server. * @throws RpcException Thrown if the RPC request fails. */ - public function getPrivacyPolicy(): string + public function getPrivacyPolicy(): ServerDocument { - return $this->sendRequest( + return ServerDocument::fromArray($this->sendRequest( new RpcRequest(StandardMethods::GET_PRIVACY_POLICY->value, Utilities::randomCrc32()) - )->getResponse()->getResult(); + )->getResponse()->getResult()); } /** @@ -84,16 +85,16 @@ } /** - * Retrieves the terms of service from the server. + * Retrieves the terms of service document by sending a remote procedure call request. * - * @return string Returns the terms of service as a string. + * @return ServerDocument The terms of service document retrieved from the server. * @throws RpcException Thrown if the RPC request fails. */ - public function getTermsOfService(): string + public function getTermsOfService(): ServerDocument { - return $this->sendRequest( + return ServerDocument::fromArray($this->sendRequest( new RpcRequest(StandardMethods::GET_TERMS_OF_SERVICE->value, Utilities::randomCrc32()) - )->getResponse()->getResult(); + )->getResponse()->getResult()); } /** @@ -110,16 +111,16 @@ } /** - * Fetches the community guidelines by performing a remote procedure call request. + * Fetches the community guidelines document from the server by sending a remote procedure call request. * - * @return string The content of the community guidelines. - * @throws RpcException Thrown if the RPC request encounters an error. + * @return ServerDocument The community guidelines document retrieved from the server. + * @throws RpcException Thrown if the RPC request fails. */ - public function getCommunityGuidelines(): string + public function getCommunityGuidelines(): ServerDocument { - return $this->sendRequest( + return ServerDocument::fromArray($this->sendRequest( new RpcRequest(StandardMethods::GET_COMMUNITY_GUIDELINES->value, Utilities::randomCrc32()) - )->getResponse()->getResult(); + )->getResponse()->getResult()); } /** From 23262bc0a69d837ee99ad8a1c9f8b662c9926599 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 23 Dec 2024 19:03:43 -0500 Subject: [PATCH 067/420] Remove obsolete ClientRequestOld and RpcHandler classes --- src/Socialbox/Abstracts/Method.php | 39 ++-- src/Socialbox/Classes/RpcHandler.php | 198 ------------------ .../Classes/StandardMethods/Ping.php | 1 - .../VerificationAnswerImageCaptcha.php | 3 - src/Socialbox/Objects/ClientRequestOld.php | 162 -------------- 5 files changed, 17 insertions(+), 386 deletions(-) delete mode 100644 src/Socialbox/Classes/RpcHandler.php delete mode 100644 src/Socialbox/Objects/ClientRequestOld.php diff --git a/src/Socialbox/Abstracts/Method.php b/src/Socialbox/Abstracts/Method.php index beac6a0..39d9012 100644 --- a/src/Socialbox/Abstracts/Method.php +++ b/src/Socialbox/Abstracts/Method.php @@ -1,26 +1,21 @@ getMessage(), 400, $e); - } - - $clientRequest = new ClientRequestOld($headers, self::getRpcRequests(), self::getRequestHash()); - - // Verify the session & request signature - if($clientRequest->getSessionUuid() !== null) - { - // If no signature is provided, it must be required if the client is providing a Session UUID - if($clientRequest->getSignature() === null) - { - throw new RpcException(sprintf('Unauthorized request, signature required for session based requests'), 401); - } - - try - { - $session = SessionManager::getSession($clientRequest->getSessionUuid()); - } - catch(StandardException $e) - { - throw new RpcException($e->getMessage(), 400); - } - catch(DatabaseOperationException $e) - { - throw new RpcException('Failed to verify session', 500, $e); - } - - try - { - if(!Cryptography::verifyContent($clientRequest->getHash(), $clientRequest->getSignature(), $session->getPublicKey())) - { - throw new RpcException('Request signature check failed', 400); - } - } - catch(RpcException $e) - { - throw $e; - } - catch(Exception $e) - { - throw new RpcException('Request signature check failed (Cryptography Error): ' . $e->getMessage(), 400, $e); - } - } - - return $clientRequest; - } - - /** - * Returns the request hash by hashing the request body using SHA256 - * - * @return string Returns the request hash in SHA256 representation - */ - private static function getRequestHash(): string - { - return hash('sha1', file_get_contents('php://input')); - } - - /** - * Handles a POST request, returning an array of RpcRequest objects - * expects a JSON encoded body with either a single RpcRequest object or an array of RpcRequest objects - * - * @return RpcRequest[] The parsed RpcRequest objects - * @throws RpcException Thrown if the request is invalid - */ - private static function getRpcRequests(): array - { - try - { - // Decode the request body - $body = Utilities::jsonDecode(file_get_contents('php://input')); - } - catch(InvalidArgumentException $e) - { - throw new RpcException("Invalid JSON in request body: " . $e->getMessage(), 400, $e); - } - - if(isset($body['method'])) - { - // If it only contains a method, we assume it's a single request - return [self::parseRequest($body)]; - } - - // Otherwise, we assume it's an array of requests - return array_map(fn($request) => self::parseRequest($request), $body); - } - - /** - * Parses the raw request data into an RpcRequest object - * - * @param array $data The raw request data - * @return RpcRequest The parsed RpcRequest object - * @throws RpcException If the request is invalid - */ - private static function parseRequest(array $data): RpcRequest - { - if(!isset($data['method'])) - { - throw new RpcException("Missing 'method' key in request", 400); - } - - if(isset($data['id'])) - { - if(!is_string($data['id'])) - { - throw new RpcException("Invalid 'id' key in request: Expected string", 400); - } - - if(strlen($data['id']) === 0) - { - throw new RpcException("Invalid 'id' key in request: Expected non-empty string", 400); - } - - if(strlen($data['id']) > 8) - { - throw new RpcException("Invalid 'id' key in request: Expected string of length <= 8", 400); - } - } - - if(isset($data['parameters'])) - { - if(!is_array($data['parameters'])) - { - throw new RpcException("Invalid 'parameters' key in request: Expected array", 400); - } - } - - return new RpcRequest($data['method'], $data['id'] ?? null, $data['parameters'] ?? null); - } -} \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/Ping.php b/src/Socialbox/Classes/StandardMethods/Ping.php index 93724a9..4fe2094 100644 --- a/src/Socialbox/Classes/StandardMethods/Ping.php +++ b/src/Socialbox/Classes/StandardMethods/Ping.php @@ -5,7 +5,6 @@ use Socialbox\Abstracts\Method; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\ClientRequest; - use Socialbox\Objects\ClientRequestOld; use Socialbox\Objects\RpcRequest; class Ping extends Method diff --git a/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php b/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php index d5234c4..790dee9 100644 --- a/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php +++ b/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php @@ -3,17 +3,14 @@ namespace Socialbox\Classes\StandardMethods; use Socialbox\Abstracts\Method; - use Socialbox\Enums\Flags\PeerFlags; use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\CaptchaManager; - use Socialbox\Managers\RegisteredPeerManager; use Socialbox\Managers\SessionManager; use Socialbox\Objects\ClientRequest; - use Socialbox\Objects\ClientRequestOld; use Socialbox\Objects\RpcRequest; class VerificationAnswerImageCaptcha extends Method diff --git a/src/Socialbox/Objects/ClientRequestOld.php b/src/Socialbox/Objects/ClientRequestOld.php deleted file mode 100644 index 036a26b..0000000 --- a/src/Socialbox/Objects/ClientRequestOld.php +++ /dev/null @@ -1,162 +0,0 @@ -headers = $headers; - $this->requests = $requests; - $this->requestHash = $requestHash; - } - - /** - * @return array - */ - public function getHeaders(): array - { - return $this->headers; - } - - /** - * @return RpcRequest[] - */ - public function getRequests(): array - { - return $this->requests; - } - - public function getHash(): string - { - return $this->requestHash; - } - - public function getClientName(): string - { - return $this->headers[StandardHeaders::CLIENT_NAME->value]; - } - - public function getClientVersion(): string - { - return $this->headers[StandardHeaders::CLIENT_VERSION->value]; - } - - public function getSessionUuid(): ?string - { - if(!isset($this->headers[StandardHeaders::SESSION_UUID->value])) - { - return null; - } - - return $this->headers[StandardHeaders::SESSION_UUID->value]; - } - - public function getFromPeer(): ?PeerAddress - { - if(!isset($this->headers[StandardHeaders::FROM_PEER->value])) - { - return null; - } - - return PeerAddress::fromAddress($this->headers[StandardHeaders::FROM_PEER->value]); - } - - public function getSignature(): ?string - { - if(!isset($this->headers[StandardHeaders::SIGNATURE->value])) - { - return null; - } - - return $this->headers[StandardHeaders::SIGNATURE->value]; - } - - public function validateSession(): void - { - if($this->getSessionUuid() == null) - { - throw new StandardException(StandardError::SESSION_REQUIRED->getMessage(), StandardError::SESSION_REQUIRED); - } - - $session = SessionManager::getSession($this->getSessionUuid()); - - switch($session->getState()) - { - case SessionState::AWAITING_DHE: - throw new StandardException(StandardError::SESSION_DHE_REQUIRED->getMessage(), StandardError::SESSION_DHE_REQUIRED); - - case SessionState::EXPIRED: - throw new StandardException(StandardError::SESSION_EXPIRED->getMessage(), StandardError::SESSION_EXPIRED); - } - } - - /** - * @return bool - * @throws DatabaseOperationException - */ - public function verifySignature(): bool - { - $signature = $this->getSignature(); - $sessionUuid = $this->getSessionUuid(); - - if($signature == null || $sessionUuid == null) - { - return false; - } - - try - { - $session = SessionManager::getSession($sessionUuid); - } - catch(StandardException $e) - { - if($e->getStandardError() == StandardError::SESSION_NOT_FOUND) - { - return false; - } - - throw new RuntimeException($e); - } - - try - { - return Cryptography::verifyContent($this->getHash(), $signature, $session->getPublicKey()); - } - catch(CryptographyException $e) - { - return false; - } - } - } \ No newline at end of file From 1108711eb730a35c7b9f3720a4bfc808994f9438 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 23 Dec 2024 19:07:01 -0500 Subject: [PATCH 068/420] Refactor session flow management for captcha and password. --- .../StandardMethods/SettingsSetPassword.php | 18 +++--------------- .../VerificationAnswerImageCaptcha.php | 16 +--------------- 2 files changed, 4 insertions(+), 30 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/SettingsSetPassword.php b/src/Socialbox/Classes/StandardMethods/SettingsSetPassword.php index 7269f15..8241343 100644 --- a/src/Socialbox/Classes/StandardMethods/SettingsSetPassword.php +++ b/src/Socialbox/Classes/StandardMethods/SettingsSetPassword.php @@ -50,27 +50,15 @@ // Remove the SET_PASSWORD flag SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::SET_PASSWORD]); + + // Check & update the session flow + SessionManager::updateFlow($request->getSession()); } catch(Exception $e) { throw new StandardException('Failed to set password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } - // Check if all registration flags are removed - if(SessionFlags::isComplete($request->getSession()->getFlags())) - { - // Set the session as authenticated - try - { - SessionManager::setAuthenticated($request->getSessionUuid(), true); - SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::REGISTRATION_REQUIRED, SessionFlags::AUTHENTICATION_REQUIRED]); - } - catch (DatabaseOperationException $e) - { - throw new StandardException('Failed to update session due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); - } - } - return $rpcRequest->produceResponse(true); } } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php b/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php index 790dee9..67b5027 100644 --- a/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php +++ b/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php @@ -47,6 +47,7 @@ if($result) { SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::VER_IMAGE_CAPTCHA]); + SessionManager::updateFlow($session); } } catch (DatabaseOperationException $e) @@ -54,21 +55,6 @@ throw new StandardException("There was an unexpected error while trying to answer the captcha", StandardError::INTERNAL_SERVER_ERROR, $e); } - // Check if all registration flags are removed - if(SessionFlags::isComplete($request->getSession()->getFlags())) - { - // Set the session as authenticated - try - { - SessionManager::setAuthenticated($request->getSessionUuid(), true); - SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::REGISTRATION_REQUIRED, SessionFlags::AUTHENTICATION_REQUIRED]); - } - catch (DatabaseOperationException $e) - { - return $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, $e); - } - } - return $rpcRequest->produceResponse($result); } } \ No newline at end of file From 7e4b02bc0442258b93234cfd69708421f9d45e00 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 23 Dec 2024 19:25:21 -0500 Subject: [PATCH 069/420] Remove outdated test files and update peer registration logic --- src/Socialbox/Classes/RpcClient.php | 12 +- .../Managers/RegisteredPeerManager.php | 652 +++++++++--------- src/Socialbox/Managers/SessionManager.php | 19 +- src/Socialbox/Socialbox.php | 16 +- tests/Socialbox/Classes/CryptographyTest.php | 219 ------ .../Socialbox/Classes/SecuredPasswordTest.php | 21 - .../Socialbox/Classes/ServerResolverTest.php | 21 - tests/Socialbox/Classes/client_private.der | 1 - tests/Socialbox/Classes/client_public.der | 1 - tests/Socialbox/Classes/content.txt | 4 - tests/Socialbox/Classes/server_private.der | 1 - tests/Socialbox/Classes/server_public.der | 1 - tests/Socialbox/Classes/server_secret.txt | 1 - .../Managers/RegisteredPeerManagerTest.php | 78 --- .../Managers/ResolvedServersManagerTest.php | 43 -- .../Socialbox/Managers/SessionManagerTest.php | 40 -- .../Managers/VariableManagerTest.php | 34 - tests/Socialbox/Other/CaptchaTest.php | 17 - 18 files changed, 362 insertions(+), 819 deletions(-) delete mode 100644 tests/Socialbox/Classes/CryptographyTest.php delete mode 100644 tests/Socialbox/Classes/SecuredPasswordTest.php delete mode 100644 tests/Socialbox/Classes/ServerResolverTest.php delete mode 100644 tests/Socialbox/Classes/client_private.der delete mode 100644 tests/Socialbox/Classes/client_public.der delete mode 100644 tests/Socialbox/Classes/content.txt delete mode 100644 tests/Socialbox/Classes/server_private.der delete mode 100644 tests/Socialbox/Classes/server_public.der delete mode 100644 tests/Socialbox/Classes/server_secret.txt delete mode 100644 tests/Socialbox/Managers/RegisteredPeerManagerTest.php delete mode 100644 tests/Socialbox/Managers/ResolvedServersManagerTest.php delete mode 100644 tests/Socialbox/Managers/SessionManagerTest.php delete mode 100644 tests/Socialbox/Managers/VariableManagerTest.php delete mode 100644 tests/Socialbox/Other/CaptchaTest.php diff --git a/src/Socialbox/Classes/RpcClient.php b/src/Socialbox/Classes/RpcClient.php index 50ced1d..1be63d1 100644 --- a/src/Socialbox/Classes/RpcClient.php +++ b/src/Socialbox/Classes/RpcClient.php @@ -113,20 +113,26 @@ if($response === false) { curl_close($ch); - throw new RpcException('Failed to create the session, no response received'); + throw new RpcException(sprintf('Failed to create the session at %s, no response received', $this->rpcEndpoint)); } $responseCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); if($responseCode !== 201) { curl_close($ch); - throw new RpcException('Failed to create the session, server responded with ' . $responseCode . ': ' . $response); + + if(empty($response)) + { + throw new RpcException(sprintf('Failed to create the session at %s, server responded with ' . $responseCode, $this->rpcEndpoint)); + } + + throw new RpcException(sprintf('Failed to create the session at %s, server responded with ' . $responseCode . ': ' . $response, $this->rpcEndpoint)); } if(empty($response)) { curl_close($ch); - throw new RpcException('Failed to create the session, server did not return a session UUID'); + throw new RpcException(sprintf('Failed to create the session at %s, server did not return a session UUID', $this->rpcEndpoint)); } curl_close($ch); diff --git a/src/Socialbox/Managers/RegisteredPeerManager.php b/src/Socialbox/Managers/RegisteredPeerManager.php index 16ca7c0..e70c4cc 100644 --- a/src/Socialbox/Managers/RegisteredPeerManager.php +++ b/src/Socialbox/Managers/RegisteredPeerManager.php @@ -1,340 +1,354 @@ verbose(sprintf("Checking if username %s already exists", $username)); - - try + /** + * Checks if a username already exists in the database. + * + * @param string $username The username to check. + * @return bool True if the username exists, false otherwise. + * @throws DatabaseOperationException If the operation fails. + */ + public static function usernameExists(string $username): bool { - $statement = Database::getConnection()->prepare('SELECT COUNT(*) FROM `registered_peers` WHERE username=?'); - $statement->bindParam(1, $username); - $statement->execute(); + Logger::getLogger()->verbose(sprintf("Checking if username %s already exists", $username)); - $result = $statement->fetchColumn(); - return $result > 0; - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to check if the username exists', $e); - } - } - - /** - * Creates a new peer with the given username. - * - * @param string $username The username to associate with the new peer. - * @param bool $enabled True if the peer should be enabled, false otherwise. - * @return string The UUID of the newly created peer. - * @throws DatabaseOperationException If the operation fails. - */ - public static function createPeer(string $username, bool $enabled=false): string - { - Logger::getLogger()->verbose(sprintf("Creating a new peer with username %s", $username)); - $uuid = Uuid::v4()->toRfc4122(); - - try - { - $statement = Database::getConnection()->prepare('INSERT INTO `registered_peers` (uuid, username, enabled) VALUES (?, ?, ?)'); - $statement->bindParam(1, $uuid); - $statement->bindParam(2, $username); - $statement->bindParam(3, $enabled, PDO::PARAM_BOOL); - $statement->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to create the peer in the database', $e); - } - - return $uuid; - } - - /** - * Deletes a peer from the database based on the given UUID or RegisteredPeerRecord. - * WARNING: This operation is cascading and will delete all associated data. - * - * @param string|RegisteredPeerRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer to be deleted. - * @return void - * @throws DatabaseOperationException If the operation fails. - */ - public static function deletePeer(string|RegisteredPeerRecord $uuid): void - { - if($uuid instanceof RegisteredPeerRecord) - { - $uuid = $uuid->getUuid(); - } - - Logger::getLogger()->verbose(sprintf("Deleting peer %s", $uuid)); - - try - { - $statement = Database::getConnection()->prepare('DELETE FROM `registered_peers` WHERE uuid=?'); - $statement->bindParam(1, $uuid); - $statement->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to delete the peer from the database', $e); - } - } - - /** - * Retrieves a registered peer record based on the given unique identifier or RegisteredPeerRecord object. - * - * @param string|RegisteredPeerRecord $uuid The unique identifier of the registered peer, or an instance of RegisteredPeerRecord. - * @return RegisteredPeerRecord Returns a RegisteredPeerRecord object containing the peer's information. - * @throws DatabaseOperationException If there is an error during the database operation. - */ - public static function getPeer(string|RegisteredPeerRecord $uuid): RegisteredPeerRecord - { - if($uuid instanceof RegisteredPeerRecord) - { - $uuid = $uuid->getUuid(); - } - - Logger::getLogger()->verbose(sprintf("Retrieving peer %s from the database", $uuid)); - - try - { - $statement = Database::getConnection()->prepare('SELECT * FROM `registered_peers` WHERE uuid=?'); - $statement->bindParam(1, $uuid); - $statement->execute(); - - $result = $statement->fetch(PDO::FETCH_ASSOC); - - if($result === false) + try { - throw new DatabaseOperationException(sprintf("The requested peer '%s' does not exist", $uuid)); + $statement = Database::getConnection()->prepare('SELECT COUNT(*) FROM `registered_peers` WHERE username=?'); + $statement->bindParam(1, $username); + $statement->execute(); + + $result = $statement->fetchColumn(); + return $result > 0; } - - return new RegisteredPeerRecord($result); - } - catch(PDOException | \DateMalformedStringException $e) - { - throw new DatabaseOperationException('Failed to get the peer from the database', $e); - } - } - - /** - * Retrieves a peer record by the given username. - * - * @param string $username The username of the peer to be retrieved. - * @return RegisteredPeerRecord|null The record of the peer associated with the given username. - * @throws DatabaseOperationException If there is an error while querying the database. - */ - public static function getPeerByUsername(string $username): ?RegisteredPeerRecord - { - Logger::getLogger()->verbose(sprintf("Retrieving peer %s from the database", $username)); - - try - { - $statement = Database::getConnection()->prepare('SELECT * FROM `registered_peers` WHERE username=?'); - $statement->bindParam(1, $username); - $statement->execute(); - - $result = $statement->fetch(PDO::FETCH_ASSOC); - - if($result === false) + catch(PDOException $e) { - return null; - } - - return new RegisteredPeerRecord($result); - } - catch(PDOException | \DateMalformedStringException $e) - { - throw new DatabaseOperationException('Failed to get the peer from the database', $e); - } - } - - /** - * Enables a peer identified by the given UUID or RegisteredPeerRecord. - * - * @param string|RegisteredPeerRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer to be enabled. - * @return void - * @throws DatabaseOperationException If there is an error while updating the database. - */ - public static function enablePeer(string|RegisteredPeerRecord $uuid): void - { - if($uuid instanceof RegisteredPeerRecord) - { - $uuid = $uuid->getUuid(); - } - - Logger::getLogger()->verbose(sprintf("Enabling peer %s", $uuid)); - - try - { - $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET enabled=1 WHERE uuid=?'); - $statement->bindParam(1, $uuid); - $statement->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to enable the peer in the database', $e); - } - } - - /** - * Disables the peer identified by the given UUID or RegisteredPeerRecord. - * - * @param string|RegisteredPeerRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer. - * @return void - * @throws DatabaseOperationException If there is an error while updating the peer's status in the database. - */ - public static function disablePeer(string|RegisteredPeerRecord $uuid): void - { - if($uuid instanceof RegisteredPeerRecord) - { - $uuid = $uuid->getUuid(); - } - - Logger::getLogger()->verbose(sprintf("Disabling peer %s", $uuid)); - - try - { - $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET enabled=0 WHERE uuid=?'); - $statement->bindParam(1, $uuid); - $statement->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to disable the peer in the database', $e); - } - } - - /** - * Adds a specific flag to the peer identified by the given UUID or RegisteredPeerRecord. - * - * @param string|RegisteredPeerRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer. - * @param PeerFlags|array $flags The flag or array of flags to be added to the peer. - * @return void - * @throws DatabaseOperationException If there is an error while updating the database. - */ - public static function addFlag(string|RegisteredPeerRecord $uuid, PeerFlags|array $flags): void - { - if($uuid instanceof RegisteredPeerRecord) - { - $uuid = $uuid->getUuid(); - } - - Logger::getLogger()->verbose(sprintf("Adding flag(s) %s to peer %s", implode(',', $flags), $uuid)); - - $peer = self::getPeer($uuid); - $existingFlags = $peer->getFlags(); - $flags = is_array($flags) ? $flags : [$flags]; - - foreach($flags as $flag) - { - if(!in_array($flag, $existingFlags)) - { - $existingFlags[] = $flag; + throw new DatabaseOperationException('Failed to check if the username exists', $e); } } - try + /** + * Creates a new peer with the given username. + * + * @param PeerAddress $peerAddress The address of the peer to be created. + * @param bool $enabled True if the peer should be enabled, false otherwise. + * @return string The UUID of the newly created peer. + * @throws DatabaseOperationException If the operation fails. + */ + public static function createPeer(PeerAddress $peerAddress, bool $enabled=false): string { - $implodedFlags = implode(',', array_map(fn($flag) => $flag->name, $existingFlags)); - $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET flags=? WHERE uuid=?'); - $statement->bindParam(1, $implodedFlags); - $statement->bindParam(2, $uuid); - $statement->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to add the flag to the peer in the database', $e); - } - } + Logger::getLogger()->verbose(sprintf("Registering peer %s", $peerAddress->getAddress())); + $uuid = Uuid::v4()->toRfc4122(); + $server = $peerAddress->getDomain(); - /** - * Removes a specific flag from the peer identified by the given UUID or RegisteredPeerRecord. - * - * @param string|RegisteredPeerRecord $peer - * @param PeerFlags $flag The flag to be removed from the peer. - * @return void - * @throws DatabaseOperationException If there is an error while updating the database. - */ - public static function removeFlag(string|RegisteredPeerRecord $peer, PeerFlags $flag): void - { - if(is_string($peer)) - { - $peer = self::getPeer($peer); - } - - Logger::getLogger()->verbose(sprintf("Removing flag %s from peer %s", $flag->value, $peer->getUuid())); - - if(!$peer->flagExists($flag)) - { - return; - } - - $peer->removeFlag($flag); - - try - { - $implodedFlags = PeerFlags::toString($peer->getFlags()); - $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET flags=? WHERE uuid=?'); - $statement->bindParam(1, $implodedFlags); - $statement->bindParam(2, $registeredPeer); - $statement->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to remove the flag from the peer in the database', $e); - } - } - - /** - * - */ - public static function getPasswordAuthentication(string|RegisteredPeerRecord $peerUuid): ?SecurePasswordRecord - { - if($peerUuid instanceof RegisteredPeerRecord) - { - $peerUuid = $peerUuid->getUuid(); - } - - try - { - $statement = Database::getConnection()->prepare('SELECT * FROM `authentication_passwords` WHERE peer_uuid=?'); - $statement->bindParam(1, $peerUuid); - $statement->execute(); - - $result = $statement->fetch(PDO::FETCH_ASSOC); - - if($result === false) + if($server === Configuration::getInstanceConfiguration()->getDomain()) { - return null; + $server = 'host'; } - return new SecurePasswordRecord($result); + try + { + $statement = Database::getConnection()->prepare('INSERT INTO `registered_peers` (uuid, username, server, enabled) VALUES (?, ?, ?, ?)'); + $statement->bindParam(1, $uuid); + $username = $peerAddress->getUsername(); + $statement->bindParam(2, $username); + $statement->bindParam(3, $server); + $statement->bindParam(4, $enabled, PDO::PARAM_BOOL); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to create the peer in the database', $e); + } + + return $uuid; } - catch(PDOException | \DateMalformedStringException $e) + + /** + * Deletes a peer from the database based on the given UUID or RegisteredPeerRecord. + * WARNING: This operation is cascading and will delete all associated data. + * + * @param string|RegisteredPeerRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer to be deleted. + * @return void + * @throws DatabaseOperationException If the operation fails. + */ + public static function deletePeer(string|RegisteredPeerRecord $uuid): void { - throw new DatabaseOperationException('Failed to get the secure password record from the database', $e); + if($uuid instanceof RegisteredPeerRecord) + { + $uuid = $uuid->getUuid(); + } + + Logger::getLogger()->verbose(sprintf("Deleting peer %s", $uuid)); + + try + { + $statement = Database::getConnection()->prepare('DELETE FROM `registered_peers` WHERE uuid=?'); + $statement->bindParam(1, $uuid); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to delete the peer from the database', $e); + } } - } -} \ No newline at end of file + + /** + * Retrieves a registered peer record based on the given unique identifier or RegisteredPeerRecord object. + * + * @param string|RegisteredPeerRecord $uuid The unique identifier of the registered peer, or an instance of RegisteredPeerRecord. + * @return RegisteredPeerRecord Returns a RegisteredPeerRecord object containing the peer's information. + * @throws DatabaseOperationException If there is an error during the database operation. + */ + public static function getPeer(string|RegisteredPeerRecord $uuid): RegisteredPeerRecord + { + if($uuid instanceof RegisteredPeerRecord) + { + $uuid = $uuid->getUuid(); + } + + Logger::getLogger()->verbose(sprintf("Retrieving peer %s from the database", $uuid)); + + try + { + $statement = Database::getConnection()->prepare('SELECT * FROM `registered_peers` WHERE uuid=?'); + $statement->bindParam(1, $uuid); + $statement->execute(); + + $result = $statement->fetch(PDO::FETCH_ASSOC); + + if($result === false) + { + throw new DatabaseOperationException(sprintf("The requested peer '%s' does not exist", $uuid)); + } + + return new RegisteredPeerRecord($result); + } + catch(PDOException | \DateMalformedStringException $e) + { + throw new DatabaseOperationException('Failed to get the peer from the database', $e); + } + } + + /** + * Retrieves a peer record by the given username. + * + * @param PeerAddress $address The address of the peer to be retrieved. + * @return RegisteredPeerRecord|null The record of the peer associated with the given username. + * @throws DatabaseOperationException If there is an error while querying the database. + */ + public static function getPeerByAddress(PeerAddress $address): ?RegisteredPeerRecord + { + Logger::getLogger()->verbose(sprintf("Retrieving peer %s from the database", $address->getAddress())); + + try + { + $statement = Database::getConnection()->prepare('SELECT * FROM `registered_peers` WHERE username=? AND server=?'); + $username = $address->getUsername(); + $statement->bindParam(1, $username); + $server = $address->getDomain(); + $statement->bindParam(2, $server); + $statement->execute(); + + $result = $statement->fetch(PDO::FETCH_ASSOC); + + if($result === false) + { + return null; + } + + return new RegisteredPeerRecord($result); + } + catch(PDOException | \DateMalformedStringException $e) + { + throw new DatabaseOperationException('Failed to get the peer from the database', $e); + } + } + + /** + * Enables a peer identified by the given UUID or RegisteredPeerRecord. + * + * @param string|RegisteredPeerRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer to be enabled. + * @return void + * @throws DatabaseOperationException If there is an error while updating the database. + */ + public static function enablePeer(string|RegisteredPeerRecord $uuid): void + { + if($uuid instanceof RegisteredPeerRecord) + { + $uuid = $uuid->getUuid(); + } + + Logger::getLogger()->verbose(sprintf("Enabling peer %s", $uuid)); + + try + { + $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET enabled=1 WHERE uuid=?'); + $statement->bindParam(1, $uuid); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to enable the peer in the database', $e); + } + } + + /** + * Disables the peer identified by the given UUID or RegisteredPeerRecord. + * + * @param string|RegisteredPeerRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer. + * @return void + * @throws DatabaseOperationException If there is an error while updating the peer's status in the database. + */ + public static function disablePeer(string|RegisteredPeerRecord $uuid): void + { + if($uuid instanceof RegisteredPeerRecord) + { + $uuid = $uuid->getUuid(); + } + + Logger::getLogger()->verbose(sprintf("Disabling peer %s", $uuid)); + + try + { + $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET enabled=0 WHERE uuid=?'); + $statement->bindParam(1, $uuid); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to disable the peer in the database', $e); + } + } + + /** + * Adds a specific flag to the peer identified by the given UUID or RegisteredPeerRecord. + * + * @param string|RegisteredPeerRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer. + * @param PeerFlags|array $flags The flag or array of flags to be added to the peer. + * @return void + * @throws DatabaseOperationException If there is an error while updating the database. + */ + public static function addFlag(string|RegisteredPeerRecord $uuid, PeerFlags|array $flags): void + { + if($uuid instanceof RegisteredPeerRecord) + { + $uuid = $uuid->getUuid(); + } + + Logger::getLogger()->verbose(sprintf("Adding flag(s) %s to peer %s", implode(',', $flags), $uuid)); + + $peer = self::getPeer($uuid); + $existingFlags = $peer->getFlags(); + $flags = is_array($flags) ? $flags : [$flags]; + + foreach($flags as $flag) + { + if(!in_array($flag, $existingFlags)) + { + $existingFlags[] = $flag; + } + } + + try + { + $implodedFlags = implode(',', array_map(fn($flag) => $flag->name, $existingFlags)); + $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET flags=? WHERE uuid=?'); + $statement->bindParam(1, $implodedFlags); + $statement->bindParam(2, $uuid); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to add the flag to the peer in the database', $e); + } + } + + /** + * Removes a specific flag from the peer identified by the given UUID or RegisteredPeerRecord. + * + * @param string|RegisteredPeerRecord $peer + * @param PeerFlags $flag The flag to be removed from the peer. + * @return void + * @throws DatabaseOperationException If there is an error while updating the database. + */ + public static function removeFlag(string|RegisteredPeerRecord $peer, PeerFlags $flag): void + { + if(is_string($peer)) + { + $peer = self::getPeer($peer); + } + + Logger::getLogger()->verbose(sprintf("Removing flag %s from peer %s", $flag->value, $peer->getUuid())); + + if(!$peer->flagExists($flag)) + { + return; + } + + $peer->removeFlag($flag); + + try + { + $implodedFlags = PeerFlags::toString($peer->getFlags()); + $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET flags=? WHERE uuid=?'); + $statement->bindParam(1, $implodedFlags); + $statement->bindParam(2, $registeredPeer); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to remove the flag from the peer in the database', $e); + } + } + + /** + * Retrieves the password authentication record associated with the given unique peer identifier or a RegisteredPeerRecord object. + * + * @param string|RegisteredPeerRecord $peerUuid The unique identifier of the peer, or an instance of RegisteredPeerRecord. + * @return SecurePasswordRecord|null Returns a SecurePasswordRecord object if a password authentication record exists, otherwise null. + * @throws DatabaseOperationException If there is an error during the database operation. + */ + public static function getPasswordAuthentication(string|RegisteredPeerRecord $peerUuid): ?SecurePasswordRecord + { + if($peerUuid instanceof RegisteredPeerRecord) + { + $peerUuid = $peerUuid->getUuid(); + } + + try + { + $statement = Database::getConnection()->prepare('SELECT * FROM `authentication_passwords` WHERE peer_uuid=?'); + $statement->bindParam(1, $peerUuid); + $statement->execute(); + + $result = $statement->fetch(PDO::FETCH_ASSOC); + + if($result === false) + { + return null; + } + + return new SecurePasswordRecord($result); + } + catch(PDOException | \DateMalformedStringException $e) + { + throw new DatabaseOperationException('Failed to get the secure password record from the database', $e); + } + } + } \ No newline at end of file diff --git a/src/Socialbox/Managers/SessionManager.php b/src/Socialbox/Managers/SessionManager.php index 982a66b..c96bc2a 100644 --- a/src/Socialbox/Managers/SessionManager.php +++ b/src/Socialbox/Managers/SessionManager.php @@ -70,19 +70,9 @@ $flags[] = SessionFlags::SET_DISPLAY_NAME; } - if(Configuration::getRegistrationConfiguration()->isEmailVerificationRequired()) + if(Configuration::getRegistrationConfiguration()->isDisplayPictureRequired()) { - $flags[] = SessionFlags::VER_EMAIL; - } - - if(Configuration::getRegistrationConfiguration()->isSmsVerificationRequired()) - { - $flags[] = SessionFlags::VER_SMS; - } - - if(Configuration::getRegistrationConfiguration()->isPhoneCallVerificationRequired()) - { - $flags[] = SessionFlags::VER_PHONE_CALL; + $flags[] = SessionFlags::SET_DISPLAY_PICTURE; } if(Configuration::getRegistrationConfiguration()->isImageCaptchaVerificationRequired()) @@ -109,6 +99,11 @@ { $flags[] = SessionFlags::VER_TERMS_OF_SERVICE; } + + if(Configuration::getRegistrationConfiguration()->isAcceptCommunityGuidelinesRequired()) + { + $flags[] = SessionFlags::VER_COMMUNITY_GUIDELINES; + } } if(count($flags) > 0) diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index c293d67..4d4b0c6 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -116,11 +116,21 @@ return; } - // TODO: Check if the peer address points to the domain of this server, if not we can't accept the request + // If the peer is identifying as the same domain + if($clientRequest->getIdentifyAs()->getDomain() === Configuration::getInstanceConfiguration()->getDomain()) + { + // Prevent the peer from identifying as the host unless it's coming from an external domain + if($clientRequest->getIdentifyAs()->getUsername() === 'host') + { + http_response_code(403); + print('Unauthorized: The requested peer is not allowed to identify as the host'); + return; + } + } try { - $registeredPeer = RegisteredPeerManager::getPeerByUsername($clientRequest->getIdentifyAs()->getUsername()); + $registeredPeer = RegisteredPeerManager::getPeerByAddress($clientRequest->getIdentifyAs()); // If the peer is registered, check if it is enabled if($registeredPeer !== null && !$registeredPeer->isEnabled()) @@ -143,7 +153,7 @@ } // Register the peer if it is not already registered - $peerUuid = RegisteredPeerManager::createPeer(PeerAddress::fromAddress($clientRequest->getHeader(StandardHeaders::IDENTIFY_AS))->getUsername()); + $peerUuid = RegisteredPeerManager::createPeer(PeerAddress::fromAddress($clientRequest->getHeader(StandardHeaders::IDENTIFY_AS))); // Retrieve the peer object $registeredPeer = RegisteredPeerManager::getPeer($peerUuid); } diff --git a/tests/Socialbox/Classes/CryptographyTest.php b/tests/Socialbox/Classes/CryptographyTest.php deleted file mode 100644 index fa26a49..0000000 --- a/tests/Socialbox/Classes/CryptographyTest.php +++ /dev/null @@ -1,219 +0,0 @@ -assertIsObject($keyPair); - $this->assertObjectHasProperty('publicKey', $keyPair); - $this->assertObjectHasProperty('privateKey', $keyPair); - $this->assertIsString($keyPair->getPublicKey()); - $this->assertIsString($keyPair->getPrivateKey()); - - print_r($keyPair); - } - - /** - * Testing `Cryptography::signContent` method - * @throws CryptographyException - */ - public function testSignContent() - { - $content = "My secret content"; - $keyPair = Cryptography::generateKeyPair(); - - $signature = Cryptography::signContent($content, $keyPair->getPrivateKey()); - - $this->assertIsString($signature); - } - - /** - * Testing `Cryptography::verifyContent` method - * @throws CryptographyException - */ - public function testVerifyContent() - { - $content = "My secret content"; - $keyPair = Cryptography::generateKeyPair(); - - // Sign the content - $signature = Cryptography::signContent($content, $keyPair->getPrivateKey()); - - // Verify the content - $result = Cryptography::verifyContent($content, $signature, $keyPair->getPublicKey()); - - $this->assertTrue($result); - } - - /** - * Testing `Cryptography::temporarySignature` method - * @throws CryptographyException - */ - public function testTemporarySignature() - { - $content = "Test Content"; - $keyPair = Cryptography::generateKeyPair(); - - $tempSignature = Cryptography::temporarySignContent($content, $keyPair->getPrivateKey()); - - $this->assertIsString($tempSignature); - } - - /** - * Testing `Cryptography::verifyTemporarySignature` method - * @throws CryptographyException - */ - public - function testVerifyTemporarySignature() - { - $content = "Test Content"; - $keyPair = Cryptography::generateKeyPair(); - $frames = 2; - - // Generate a temporary signature - $tempSignature = Cryptography::temporarySignContent($content, $keyPair->getPrivateKey()); - - // Verify the temporary signature - $result = Cryptography::verifyTemporarySignature($content, $tempSignature, $keyPair->getPublicKey(), $frames); - - $this->assertTrue($result); - } - - /** - * Testing `Cryptography::encrypt` method - * @throws CryptographyException - */ - public function testEncrypt() - { - $content = "Test Content"; - $keyPair = Cryptography::generateKeyPair(); - - // Encrypt the content - $encryptedContent = Cryptography::encryptContent($content, $keyPair->getPublicKey()); - - $this->assertIsString($encryptedContent); - } - - /** - * Testing `Cryptography::decrypt` method - * @throws CryptographyException - */ - public function testDecrypt() - { - $content = "Test Content"; - $keyPair = Cryptography::generateKeyPair(); - - // Encrypt the content - $encryptedContent = Cryptography::encryptContent($content, $keyPair->getPublicKey()); - - // Decrypt the content - $decryptedContent = Cryptography::decryptContent($encryptedContent, $keyPair->getPrivateKey()); - - $this->assertIsString($decryptedContent); - $this->assertEquals($content, $decryptedContent); - } - - public function testEncryptFromFile() - { - $file_path = __DIR__ . DIRECTORY_SEPARATOR . 'server_public.der'; - $content = "Test Content"; - - $encryptedContent = Cryptography::encryptContent($content, file_get_contents($file_path)); - - $this->assertIsString($encryptedContent); - $this->assertNotEquals($content, $encryptedContent); - - print_r($encryptedContent); - } - - public function testDecryptFromFile() - { - $private_key_file = __DIR__ . DIRECTORY_SEPARATOR . 'server_private.der'; - $content = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'server_secret.txt'); - - try - { - $decryptedContent = Cryptography::decryptContent($content, file_get_contents($private_key_file)); - } - catch(CryptographyException $e) - { - $this->fail($e->getMessage()); - } - - $this->assertIsString($decryptedContent); - $this->assertEquals($decryptedContent, 'Test Content'); - } - - /** - * Testing `Cryptography::validatePublicKey` method - */ - public function testValidatePublicKey() - { - $keyPair = Cryptography::generateKeyPair(); - - $result = Cryptography::validatePublicKey($keyPair->getPublicKey()); - $this->assertTrue($result); - - $resultWithInValidKey = Cryptography::validatePublicKey('invalidKey'); - - $this->assertFalse($resultWithInValidKey); - } - - public function testValidateInvalidPublicKey() - { - $result = Cryptography::validatePublicKey('Bogus Key'); - $this->assertFalse($result); - - $result = Cryptography::validatePublicKey(Utilities::base64encode('Bogus Key')); - $this->assertFalse($result); - } - - public function testValidatePrivateKey() - { - $keyPair = Cryptography::generateKeyPair(); - - $result = Cryptography::validatePrivateKey($keyPair->getPrivateKey()); - $this->assertTrue($result); - - $resultWithInValidKey = Cryptography::validatePublicKey('invalidKey'); - - $this->assertFalse($resultWithInValidKey); - } - - public function testValidateInvalidPrivateKey() - { - $result = Cryptography::validatePublicKey('Bogus Key'); - $this->assertFalse($result); - - $result = Cryptography::validatePrivateKey(Utilities::base64encode('Bogus Key')); - $this->assertFalse($result); - } - - public function testRequestSigning() - { - $client_private_der = __DIR__ . DIRECTORY_SEPARATOR . 'client_private.der'; - $client_public_der = __DIR__ . DIRECTORY_SEPARATOR . 'client_public.der'; - $content_file = __DIR__ . DIRECTORY_SEPARATOR . 'content.txt'; - - $hash = hash('sha1', file_get_contents($content_file)); - $this->assertEquals('fa2415f0735a8aa151195688852178e8fd6e77c5', $hash); - - $signature = Cryptography::signContent($hash, file_get_contents($client_private_der)); - $this->assertEquals("Gcnijq7V8AYXgdk/eP9IswXN7831FevlBNDTKN60Ku7xesPDuPX8e55+38WFGCQ87DbeiIr+61XIDoN4+bTM4Wl0YSUe7oHV9BBnBqGhyZTntDPedUYUomrF3IRcpVRK0SbQSRaYucIp/ZsSHdbQgQBtDCvH5pK1+5g+VK9ZFT16Isvk0PhMjZiLkUYxUklFuzak7agWiS3wllFPqYSM6ri0RF+5I5JbnR9fUAOfhOceax//5H7d2WsdLj6DwtuY+eL5WyHxSmGA04YeQF3JgOGJ3WX2DSH8L0zA7pkGOjz5y1Nu6+0U6KRUXcezU/iM4zy5OJOnD5eJH4pYZizkiA==", $signature); - - $result = Cryptography::verifyContent($hash, $signature, file_get_contents($client_public_der)); - $this->assertTrue($result); - } - -} diff --git a/tests/Socialbox/Classes/SecuredPasswordTest.php b/tests/Socialbox/Classes/SecuredPasswordTest.php deleted file mode 100644 index 4778602..0000000 --- a/tests/Socialbox/Classes/SecuredPasswordTest.php +++ /dev/null @@ -1,21 +0,0 @@ -assertTrue(SecuredPassword::verifyPassword('password!', $securedPassword, EncryptionRecordsManager::getAllRecords())); - } - } diff --git a/tests/Socialbox/Classes/ServerResolverTest.php b/tests/Socialbox/Classes/ServerResolverTest.php deleted file mode 100644 index ef7e8a1..0000000 --- a/tests/Socialbox/Classes/ServerResolverTest.php +++ /dev/null @@ -1,21 +0,0 @@ -getEndpoint()); - self::assertNotEmpty($resolvedServer->getPublicKey()); - } -} \ No newline at end of file diff --git a/tests/Socialbox/Classes/client_private.der b/tests/Socialbox/Classes/client_private.der deleted file mode 100644 index 249edf1..0000000 --- a/tests/Socialbox/Classes/client_private.der +++ /dev/null @@ -1 +0,0 @@ -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDZb4K+28kKYe1CvHPWHJALJFS396HOFmBv+anpAVWMDGBUyAWbWEqxmTAV17cBHiICjDDCNFBpOZLWzIiUpdWKA0Jo+Vu9zgWSPUyGe/Lik4GFNZ38gfolfdKGnLNFnn4nFR/fsZQ7hg4wWDarJmhJ+ZSLShOz2uIb4LaKk2qy12c6Zepufgrbk9TwWZQiXkzqBWbrZDpw0pp50CzoIwEnYJ+a7vhb98jpeS+Jjnp5zWlFjv9RgzOQUOwwOK4We2gNAVeFC5BP9trklpTh1bJlit4CECH68fCGjgoTOU92UbgucgyA4O5FVPGQYPAMuiZMGFaqXE2E7z1XwYIMAL4VAgMBAAECggEAAKiJz3CYuO+gGnL+F7qjaSXCUE8VvPfoCwuNYHNEFXo9DJBmnL7EU2WrYG+wARCP7O7qd0dEidx9u36ytjyCcKT4nYni8lM1zU7rVvbnLbsuRZS/4RO/RaYfPxig94fDfSeJ2ma0i7G56onj+MBbyTZarZ7Bf8hpcmKg9pkNEcEVcklNIwwbXKBOGq75Vka/+W56JZKJD3G9YmfrAO5RGF1prh93MRXlxlN/91k/m2pqkN9xYofepn0ePmI8Ci18jrMpJbmeu8BkypzgvC/5EfHipn7y/yJ215o/EtB575muz2zngRXe+GVO5lB5d5PuEwmXoaV5o3BqkIcb3aiz4QKBgQD7P1AE2/3oATNUF1FwlXzvdCS7M2BB28jQWjzJvHus1d1+qA2StWPgCPG2D/YTtHPI3xefBnAmeSIFCFEub0YLONbRvtQAZdTt5SAaZuUyMprqD1sCUHCizyVO0wHxo3DS0sIFmo/Lpc+jnYHn3KcuRPRJk3ncZNCQhy9a/rrnxQKBgQDdjHY82YdkWQWj/xM1EuVtkVVeCJWJ6tSDn+Uq8d+hXILFAQ47GOUbzj4Ty4qGgsAgsaAGqja5t6CE+fYs8Q34FsxTsYgIRm0VXqtPm4aYTQ4PwKbmMPEOgEsXBywe5Y+QB0u/WuNyhgwgYP5cy1IS3HA1HmbTisi0zLEfkVWSEQKBgCuP36zoA88NHjwvStSNZrsR1SiMEN16YQgXDUEhKARglGXYd3n/b1Cx3E7n14+1Evo6DBtrf1h8WjSrK4A0lN1vPnfhcVqcTV3uAzHwsz6P3aJFhU8SaWUhK2POXCDsaKx1FGTqVpJFrom8zoBIFsiD9iMnqdJXvH3CoqhRUFDNAoGAEJdwU2ZHCXDRR1LW8WaU3/u+VOh3qnh3qdPTqb+ra74t3OsTUcGvhsGPTJQ1r5UjJk+nGFiu+IGT9+FwWjVDQo0SiEIHWfdMPAl28uNG1SkQIIXg+eQ4aUmaVgMnfrjaY4LoXVBFMFJxngslgXWIk/kGPjQkpzsBhOi/awnLSsECgYEAkSEb3CXfq1r/8qXMTzI+A9CGPr++aC2H6ytFNGJq4J+P40u1tcsfkwNGcQ0Hp+Qz3FHBYFuMxtjXDq0QSvVKEhdV9bjlZhTqN3lqWcCukU3ESqRbxsIj9izuncpxSP7G19WEU0anGD9ev+QWYdHPTBY9nn1+H0tkJjqh4XkRBuY= \ No newline at end of file diff --git a/tests/Socialbox/Classes/client_public.der b/tests/Socialbox/Classes/client_public.der deleted file mode 100644 index 58b132e..0000000 --- a/tests/Socialbox/Classes/client_public.der +++ /dev/null @@ -1 +0,0 @@ -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2W+CvtvJCmHtQrxz1hyQCyRUt/ehzhZgb/mp6QFVjAxgVMgFm1hKsZkwFde3AR4iAowwwjRQaTmS1syIlKXVigNCaPlbvc4Fkj1Mhnvy4pOBhTWd/IH6JX3ShpyzRZ5+JxUf37GUO4YOMFg2qyZoSfmUi0oTs9riG+C2ipNqstdnOmXqbn4K25PU8FmUIl5M6gVm62Q6cNKaedAs6CMBJ2Cfmu74W/fI6XkviY56ec1pRY7/UYMzkFDsMDiuFntoDQFXhQuQT/ba5JaU4dWyZYreAhAh+vHwho4KEzlPdlG4LnIMgODuRVTxkGDwDLomTBhWqlxNhO89V8GCDAC+FQIDAQAB \ No newline at end of file diff --git a/tests/Socialbox/Classes/content.txt b/tests/Socialbox/Classes/content.txt deleted file mode 100644 index f2afb1a..0000000 --- a/tests/Socialbox/Classes/content.txt +++ /dev/null @@ -1,4 +0,0 @@ -{ - "method" : "ping", - "id" : "daa31852" -} \ No newline at end of file diff --git a/tests/Socialbox/Classes/server_private.der b/tests/Socialbox/Classes/server_private.der deleted file mode 100644 index 62d8d46..0000000 --- a/tests/Socialbox/Classes/server_private.der +++ /dev/null @@ -1 +0,0 @@ -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDxcYLVvKYHpxJQe49Z7boilJfmp/uYAr4rQNN2El9nPG3hVRYIsmhzIeByU7ZJLn0EG5D2G6T4SlydfbHbHK3NMciTgmyAZRJl10z/KIlPG1n6DaK5Oo6VT9ty+uI7JKFdQQwzSrPP2u4KNERqK4vtfsxdiAcMXS/hUncID4ARvmigmcTOTcosdH57axSqf1xfJJ12zl3QPb6wppsBAJrZ811Ll8eZYhFoQwe0oE3T5Q0aqK0Ecgh4cYhF63nEAKkcPOMxkhcZWpr3YzqD7Rj9VAzk7xRM/QJ1SjsVwdB+YCQK1tbTloTY3BqtEib9ErZvjlaB26GZbV32kNsPnwHXAgMBAAECggEAJBVtUskzXRBsjcexmGSNfW6MtyWi1ciPKEKzd8FuLa0b1OHU/a7AKnjFJQD6zLwcZflCtG1UPeFLLyRiaNdD6FdI3TbQRW4Vjk/bi4TA5Kg3TcYs7BbiyVDagLgbCHDEhv3aN24yKl3TVoYSNXXVn0RkgZP7Ta89oSSkcnlyj/QFOA8RfIm5q+qiAPvOqFf8NKlm0hZDrxWHG/OduYHq25S9ohNzymyM+1CYTrVFZCTfscDvLBDd2MVpNRyxoQquiMlfIEUBGlu9uFWy9Hovv8Sd3irgvcBtjL8iPaMzJe3p6T83KL2AgXHcYT7r9Vlvqib5x1iTYvlid25zzQ19IQKBgQD1BISfPugEp+fAdoGHOygG+gzNE8/1ldhnA9bTCZZ3FQBTI2lPRZBFDKuirc6glbCHiWrd8HoJ3BO3kbGzq4EDBf0VDFby/7nkrroTW+RIn+THlfciWgjSATGgCPOHmvM6JmIpuYsbKkdmV4ITVWwvLPxDAwlMnHyJOuYTj8xJ4QKBgQD8Q/rdWoRBSVCrDb0QO/Or5FAJELmYFtFWBBCEpadr9ci0e/mSbbZlXjP98m4XesIIRpcpG3gU8P3hKB7H60ynPN6Jyw33YhIlJHaEjYISN/h5Vw0ybQkyFR9CBRjOp59CBcb8AsdA/OQjxFz8h46PbPLCWCR5kM3tKbuNobBytwKBgQC4Rr+gLWW/KrEolXhxxtIh/SpniwEbSanKQJ7vdgSOZ2MpJDbuAfmxlQf5gBMpv6tXJMkVRuniRH0n0RH/eXu8VGK10+QJOsAK+EbGjJQy8t7UJTwLv/9mQrOaE2FlmepYz8mAbCXtNm0g0avo8pQ9Hu5TUBNMZV1csMmd6MbSwQKBgQDit+X6kqNSWaXaVdqZgIga8HLN8u4aNkelWrnNvWOer6LWMqW2aEwJBoULsponF/jSnz6zfzCJAZ3qgbhITLzzgM0wYgIHV2ifYQnzT4qa/RqfUxFVRJGDJWCWYSZOdG+5Up/nVkflrGMNkilP/DSvymbTK4x8hRvODje1rp96OQKBgFmXLpHPN8WAXP7VVyb3RqYYRgtxXjY2yj/CYwnXl0k4Uji08S9Ke2AqljiSzmZs1Wh1UBLap90F0smRVHmYgwl2rPjNiXbyKd4W9R4vEVYgEmcnvzba107o76qFmEbyW/K7a7K/jKaH8KAytgR/cHd+SIBctcDv8uKmZ1MJT9p8 \ No newline at end of file diff --git a/tests/Socialbox/Classes/server_public.der b/tests/Socialbox/Classes/server_public.der deleted file mode 100644 index 4bc983c..0000000 --- a/tests/Socialbox/Classes/server_public.der +++ /dev/null @@ -1 +0,0 @@ -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8XGC1bymB6cSUHuPWe26IpSX5qf7mAK+K0DTdhJfZzxt4VUWCLJocyHgclO2SS59BBuQ9huk+EpcnX2x2xytzTHIk4JsgGUSZddM/yiJTxtZ+g2iuTqOlU/bcvriOyShXUEMM0qzz9ruCjREaiuL7X7MXYgHDF0v4VJ3CA+AEb5ooJnEzk3KLHR+e2sUqn9cXySdds5d0D2+sKabAQCa2fNdS5fHmWIRaEMHtKBN0+UNGqitBHIIeHGIRet5xACpHDzjMZIXGVqa92M6g+0Y/VQM5O8UTP0CdUo7FcHQfmAkCtbW05aE2NwarRIm/RK2b45WgduhmW1d9pDbD58B1wIDAQAB \ No newline at end of file diff --git a/tests/Socialbox/Classes/server_secret.txt b/tests/Socialbox/Classes/server_secret.txt deleted file mode 100644 index 01baad6..0000000 --- a/tests/Socialbox/Classes/server_secret.txt +++ /dev/null @@ -1 +0,0 @@ -HHxV3e+igdTJIaBOT4340WBcJjBgjUPoP2SifAq0xuS1EjnTCWXeg6jr1pUX9HkUlMwTrZNFwye6gUIowGqsc1QrC4PReoZfBmTGR3dWXWvW01KHXbTodlnRjUFcHBUzNPpMDsE6d2J5+Mb+HGAsxXR9i4eN2jRGfO5YZ1It40vS4PCdbCtdQJ21bwnNnvHWq9+tMJRTnJ1M+niFjil9MKgS0fCLpwpG+fumnh0iRlS7wyN+y12IDSalvGEAJaX7iSN/tcWfLaQMXjIbYEsi9RMEXUibZWYQV7pkllqM5wqZpmNv52GeUPEbowtdnczA1IpsSyaj5MV4Q8DNqCoaXg== \ No newline at end of file diff --git a/tests/Socialbox/Managers/RegisteredPeerManagerTest.php b/tests/Socialbox/Managers/RegisteredPeerManagerTest.php deleted file mode 100644 index 74d4ad0..0000000 --- a/tests/Socialbox/Managers/RegisteredPeerManagerTest.php +++ /dev/null @@ -1,78 +0,0 @@ -getUuid()); - } - - self::$peerUuid = RegisteredPeerManager::createPeer('test_peer', true); - } - - public static function tearDownAfterClass(): void - { - if(RegisteredPeerManager::usernameExists('test_peer')) - { - RegisteredPeerManager::deletePeer(RegisteredPeerManager::getPeerByUsername('test_peer')->getUuid()); - } - } - - public function testEnablePeer() - { - RegisteredPeerManager::enablePeer(self::$peerUuid); - $peer = RegisteredPeerManager::getPeer(self::$peerUuid); - - $this->assertTrue($peer->isEnabled()); - } - - - public function testGetPeer() - { - $peer = RegisteredPeerManager::getPeer(self::$peerUuid); - - $this->assertEquals('test_peer', $peer->getUsername()); - } - - public function testGetPeerByUsername() - { - $peer = RegisteredPeerManager::getPeerByUsername('test_peer'); - - $this->assertEquals(self::$peerUuid, $peer->getUuid()); - } - - public function testUsernameExists() - { - $this->assertTrue(RegisteredPeerManager::usernameExists('test_peer')); - } - - public function testDisablePeer() - { - RegisteredPeerManager::disablePeer(self::$peerUuid); - $peer = RegisteredPeerManager::getPeer(self::$peerUuid); - - $this->assertFalse($peer->isEnabled()); - } - - public function testRemoveFlag() - { - RegisteredPeerManager::addFlag(self::$peerUuid, PeerFlags::ADMIN); - $peer = RegisteredPeerManager::getPeer(self::$peerUuid); - - $this->assertTrue($peer->flagExists(PeerFlags::ADMIN)); - - RegisteredPeerManager::removeFlag(self::$peerUuid, PeerFlags::ADMIN); - $peer = RegisteredPeerManager::getPeer(self::$peerUuid); - - $this->assertFalse($peer->flagExists(PeerFlags::ADMIN)); - } -} diff --git a/tests/Socialbox/Managers/ResolvedServersManagerTest.php b/tests/Socialbox/Managers/ResolvedServersManagerTest.php deleted file mode 100644 index 0813345..0000000 --- a/tests/Socialbox/Managers/ResolvedServersManagerTest.php +++ /dev/null @@ -1,43 +0,0 @@ -assertInstanceOf(DateTime::class, ResolvedServersManager::getResolvedServerUpdated('n64.cc')); - } - - public function testResolvedServerExists() - { - ResolvedServersManager::addResolvedServer('n64.cc', ServerResolver::resolveDomain('n64.cc')); - $this->assertTrue(ResolvedServersManager::resolvedServerExists('n64.cc')); - } - - public function testGetResolvedServer() - { - ResolvedServersManager::addResolvedServer('n64.cc', ServerResolver::resolveDomain('n64.cc')); - $resolvedServer = ResolvedServersManager::getResolvedServer('n64.cc'); - - $this->assertEquals('n64.cc', $resolvedServer->getDomain()); - $this->assertIsString($resolvedServer->getEndpoint()); - $this->assertIsString($resolvedServer->getPublicKey()); - $this->assertInstanceOf(DateTime::class, $resolvedServer->getUpdated()); - } -} diff --git a/tests/Socialbox/Managers/SessionManagerTest.php b/tests/Socialbox/Managers/SessionManagerTest.php deleted file mode 100644 index 8fea5cb..0000000 --- a/tests/Socialbox/Managers/SessionManagerTest.php +++ /dev/null @@ -1,40 +0,0 @@ -expectException(InvalidArgumentException::class); - SessionManager::createSession($publicKey); - } - - public function testCreateSession(): void - { - $keyPair = Cryptography::generateKeyPair(); - $uuid = SessionManager::createSession($keyPair->getPublicKey()); - - $this->assertTrue(SessionManager::sessionExists($uuid)); - } - - public function testGetSessionWithValidUuid(): void - { - $keyPair = Cryptography::generateKeyPair(); - $uuid = SessionManager::createSession($keyPair->getPublicKey()); - - $session = SessionManager::getSession($uuid); - - $this->assertInstanceOf(SessionRecord::class, $session); - $this->assertEquals($uuid, $session->getUuid()); - $this->assertEquals($keyPair->getPublicKey(), $session->getPublicKey()); - } -} diff --git a/tests/Socialbox/Managers/VariableManagerTest.php b/tests/Socialbox/Managers/VariableManagerTest.php deleted file mode 100644 index d1060f3..0000000 --- a/tests/Socialbox/Managers/VariableManagerTest.php +++ /dev/null @@ -1,34 +0,0 @@ -clear(); - - VariableManager::deleteVariable('test_name'); - VariableManager::setVariable('test_name', 'test_value'); - $this->assertTrue(VariableManager::variableExists('test_name')); - $this->assertEquals('test_value', VariableManager::getVariable('test_name')); - VariableManager::deleteVariable('test_name'); - - VariableManager::deleteVariable('test_name2'); - VariableManager::setVariable('test_name2', 'test_value2'); - $this->assertTrue(VariableManager::variableExists('test_name2')); - $this->assertEquals('test_value2', VariableManager::getVariable('test_name2')); - VariableManager::deleteVariable('test_name2'); - } - -} \ No newline at end of file diff --git a/tests/Socialbox/Other/CaptchaTest.php b/tests/Socialbox/Other/CaptchaTest.php deleted file mode 100644 index b192f1a..0000000 --- a/tests/Socialbox/Other/CaptchaTest.php +++ /dev/null @@ -1,17 +0,0 @@ -build(); - - $builder->save(__DIR__ . DIRECTORY_SEPARATOR . 'test.png'); - } -} \ No newline at end of file From 830133d1024878b84dd5b897306f9f963968e800 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 23 Dec 2024 21:21:30 -0500 Subject: [PATCH 070/420] Minor change --- src/Socialbox/Abstracts/CacheLayer.php | 162 ++++++++++++------------- src/Socialbox/Socialbox.php | 6 + 2 files changed, 87 insertions(+), 81 deletions(-) diff --git a/src/Socialbox/Abstracts/CacheLayer.php b/src/Socialbox/Abstracts/CacheLayer.php index 4e07a5c..3e1f7af 100644 --- a/src/Socialbox/Abstracts/CacheLayer.php +++ b/src/Socialbox/Abstracts/CacheLayer.php @@ -1,90 +1,90 @@ getEngine(); + if (self::$instance === null) + { + $engine = Configuration::getCacheConfiguration()->getEngine(); - if ($engine === 'redis') - { - self::$instance = new RedisCacheLayer(); - } - else if ($engine === 'memcached') - { - self::$instance = new MemcachedCacheLayer(); - } - else - { - throw new RuntimeException('Invalid cache engine specified in the configuration, must be either "redis" or "memcached".'); + if ($engine === 'redis') + { + self::$instance = new RedisCacheLayer(); + } + else if ($engine === 'memcached') + { + self::$instance = new MemcachedCacheLayer(); + } + else + { + throw new RuntimeException('Invalid cache engine specified in the configuration, must be either "redis" or "memcached".'); + } } + + return self::$instance; } - - return self::$instance; - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index 4d4b0c6..7feb1d3 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -127,6 +127,12 @@ return; } } + else + { + http_response_code(400); + print('External domains are not supported yet'); + return; + } try { From c85ca908f05e2431e8409486a69a2ce15d8010bb Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 24 Dec 2024 00:46:20 -0500 Subject: [PATCH 071/420] Add "server" property to RegisteredPeerRecord class --- .idea/socialbox-php.iml | 1 + .../Objects/Database/RegisteredPeerRecord.php | 315 ++++++++++-------- 2 files changed, 169 insertions(+), 147 deletions(-) diff --git a/.idea/socialbox-php.iml b/.idea/socialbox-php.iml index 9ac0b9d..c5da166 100644 --- a/.idea/socialbox-php.iml +++ b/.idea/socialbox-php.iml @@ -2,6 +2,7 @@ + diff --git a/src/Socialbox/Objects/Database/RegisteredPeerRecord.php b/src/Socialbox/Objects/Database/RegisteredPeerRecord.php index acb864a..2226b7f 100644 --- a/src/Socialbox/Objects/Database/RegisteredPeerRecord.php +++ b/src/Socialbox/Objects/Database/RegisteredPeerRecord.php @@ -1,168 +1,189 @@ uuid = $data['uuid']; - $this->username = $data['username']; - $this->displayName = $data['display_name'] ?? null; + private string $uuid; + private string $username; + private string $server; + private ?string $displayName; + /** + * @var PeerFlags[] + */ + private ?array $flags; + private bool $enabled; + private DateTime $created; - if($data['flags']) + /** + * Constructor for initializing class properties from provided data. + * + * @param array $data Array containing initialization data. + * @throws \DateMalformedStringException + */ + public function __construct(array $data) { - $this->flags = PeerFlags::fromString($data['flags']); - } - else - { - $this->flags = []; + $this->uuid = $data['uuid']; + $this->username = $data['username']; + $this->server = $data['server']; + $this->displayName = $data['display_name'] ?? null; + + if($data['flags']) + { + $this->flags = PeerFlags::fromString($data['flags']); + } + else + { + $this->flags = []; + } + + $this->enabled = $data['enabled']; + + if (is_string($data['created'])) + { + $this->created = new DateTime($data['created']); + } + else + { + $this->created = $data['created']; + } } - $this->enabled = $data['enabled']; - - if (is_string($data['created'])) + /** + * Retrieves the UUID of the current instance. + * + * @return string The UUID. + */ + public function getUuid(): string { - $this->created = new DateTime($data['created']); + return $this->uuid; } - else + + /** + * Retrieves the username. + * + * @return string The username. + */ + public function getUsername(): string { - $this->created = $data['created']; + return $this->username; } - } - /** - * Retrieves the UUID of the current instance. - * - * @return string The UUID. - */ - public function getUuid(): string - { - return $this->uuid; - } - - /** - * Retrieves the username. - * - * @return string The username. - */ - public function getUsername(): string - { - return $this->username; - } - - /** - * Constructs and retrieves the peer address using the current instance's username and the domain from the configuration. - * - * @return string The constructed peer address. - */ - public function getAddress(): string - { - return sprintf("%s@%s", $this->username, Configuration::getInstanceConfiguration()->getDomain()); - } - - /** - * Retrieves the display name. - * - * @return string|null The display name if set, or null otherwise. - */ - public function getDisplayName(): ?string - { - return $this->displayName; - } - - public function getFlags(): array - { - return $this->flags; - } - - public function flagExists(PeerFlags $flag): bool - { - return in_array($flag, $this->flags, true); - } - - public function removeFlag(PeerFlags $flag): void - { - $key = array_search($flag, $this->flags, true); - if($key !== false) + /** + * Retrieves the server. + * + * @return string The server. + */ + public function getServer(): string { - unset($this->flags[$key]); + return $this->server; } - } - /** - * Checks if the current instance is enabled. - * - * @return bool True if enabled, false otherwise. - */ - public function isEnabled(): bool - { - return $this->enabled; - } + /** + * Constructs and retrieves the peer address using the current instance's username and the domain from the configuration. + * + * @return string The constructed peer address. + */ + public function getAddress(): string + { + return sprintf("%s@%s", $this->username, Configuration::getInstanceConfiguration()->getDomain()); + } - /** - * Retrieves the creation date and time. - * - * @return DateTime The creation date and time. - */ - public function getCreated(): DateTime - { - return $this->created; - } + /** + * Retrieves the display name. + * + * @return string|null The display name if set, or null otherwise. + */ + public function getDisplayName(): ?string + { + return $this->displayName; + } - /** - * Converts the current instance to a SelfUser object. - * - * @return SelfUser The SelfUser object. - */ - public function toSelfUser(): SelfUser - { - return new SelfUser($this); - } + public function getFlags(): array + { + return $this->flags; + } - /** - * @inheritDoc - */ - public static function fromArray(array $data): object - { - return new self($data); - } + public function flagExists(PeerFlags $flag): bool + { + return in_array($flag, $this->flags, true); + } - /** - * @inheritDoc - */ - public function toArray(): array - { - return [ - 'uuid' => $this->uuid, - 'username' => $this->username, - 'display_name' => $this->displayName, - 'flags' => PeerFlags::toString($this->flags), - 'enabled' => $this->enabled, - 'created' => $this->created - ]; - } -} \ No newline at end of file + public function removeFlag(PeerFlags $flag): void + { + $key = array_search($flag, $this->flags, true); + if($key !== false) + { + unset($this->flags[$key]); + } + } + + /** + * Checks if the current instance is enabled. + * + * @return bool True if enabled, false otherwise. + */ + public function isEnabled(): bool + { + return $this->enabled; + } + + /** + * Retrieves the creation date and time. + * + * @return DateTime The creation date and time. + */ + public function getCreated(): DateTime + { + return $this->created; + } + + /** + * Determines if the server is external. + * + * @return bool True if the server is external, false otherwise. + */ + public function isExternal(): bool + { + return $this->server === 'host'; + } + + /** + * Converts the current instance to a SelfUser object. + * + * @return SelfUser The SelfUser object. + */ + public function toSelfUser(): SelfUser + { + return new SelfUser($this); + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): object + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'uuid' => $this->uuid, + 'username' => $this->username, + 'display_name' => $this->displayName, + 'flags' => PeerFlags::toString($this->flags), + 'enabled' => $this->enabled, + 'created' => $this->created + ]; + } + } \ No newline at end of file From 9da2ac2db25a43f1cc225bdb9ccdc46f4b101422 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 24 Dec 2024 00:46:30 -0500 Subject: [PATCH 072/420] Minor correction --- src/Socialbox/Objects/Database/RegisteredPeerRecord.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Socialbox/Objects/Database/RegisteredPeerRecord.php b/src/Socialbox/Objects/Database/RegisteredPeerRecord.php index 2226b7f..f0c14b8 100644 --- a/src/Socialbox/Objects/Database/RegisteredPeerRecord.php +++ b/src/Socialbox/Objects/Database/RegisteredPeerRecord.php @@ -180,6 +180,7 @@ return [ 'uuid' => $this->uuid, 'username' => $this->username, + 'server' => $this->server, 'display_name' => $this->displayName, 'flags' => PeerFlags::toString($this->flags), 'enabled' => $this->enabled, From 85bdff7d3cad9b30a54721a481a6f8c629e51c6b Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 24 Dec 2024 00:51:13 -0500 Subject: [PATCH 073/420] Add SettingsSetDisplayName method and refactor unused imports --- src/Socialbox/Classes/Database.php | 2 - src/Socialbox/Classes/Resources.php | 1 - .../GetCommunityGuidelines.php | 1 - .../StandardMethods/GetPrivacyPolicy.php | 1 - .../StandardMethods/GetTermsOfService.php | 1 - .../SettingsSetDisplayName.php | 46 +++++++++++++++++++ src/Socialbox/Classes/Utilities.php | 2 - src/Socialbox/Enums/Flags/PeerFlags.php | 2 - .../Managers/RegisteredPeerManager.php | 45 ++++++++++++++++++ src/Socialbox/Managers/SessionManager.php | 7 +++ src/Socialbox/Objects/ClientRequest.php | 1 - .../Objects/Database/CaptchaRecord.php | 1 - .../Objects/Database/EncryptionRecord.php | 1 - src/Socialbox/Objects/RpcRequest.php | 2 - .../Objects/Standard/ImageCaptcha.php | 1 - 15 files changed, 98 insertions(+), 16 deletions(-) create mode 100644 src/Socialbox/Classes/StandardMethods/SettingsSetDisplayName.php diff --git a/src/Socialbox/Classes/Database.php b/src/Socialbox/Classes/Database.php index 47fdcbc..68b3955 100644 --- a/src/Socialbox/Classes/Database.php +++ b/src/Socialbox/Classes/Database.php @@ -2,8 +2,6 @@ namespace Socialbox\Classes; -use mysqli; -use mysqli_sql_exception; use PDO; use PDOException; use Socialbox\Exceptions\DatabaseOperationException; diff --git a/src/Socialbox/Classes/Resources.php b/src/Socialbox/Classes/Resources.php index ca23c34..c44a114 100644 --- a/src/Socialbox/Classes/Resources.php +++ b/src/Socialbox/Classes/Resources.php @@ -2,7 +2,6 @@ namespace Socialbox\Classes; - use InvalidArgumentException; use Socialbox\Enums\DatabaseObjects; class Resources diff --git a/src/Socialbox/Classes/StandardMethods/GetCommunityGuidelines.php b/src/Socialbox/Classes/StandardMethods/GetCommunityGuidelines.php index 4021f57..9b4abb0 100644 --- a/src/Socialbox/Classes/StandardMethods/GetCommunityGuidelines.php +++ b/src/Socialbox/Classes/StandardMethods/GetCommunityGuidelines.php @@ -5,7 +5,6 @@ use Socialbox\Abstracts\Method; use Socialbox\Classes\Configuration; use Socialbox\Classes\Resources; - use Socialbox\Enums\StandardError; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\ClientRequest; use Socialbox\Objects\RpcRequest; diff --git a/src/Socialbox/Classes/StandardMethods/GetPrivacyPolicy.php b/src/Socialbox/Classes/StandardMethods/GetPrivacyPolicy.php index db83fd0..53c6136 100644 --- a/src/Socialbox/Classes/StandardMethods/GetPrivacyPolicy.php +++ b/src/Socialbox/Classes/StandardMethods/GetPrivacyPolicy.php @@ -5,7 +5,6 @@ use Socialbox\Abstracts\Method; use Socialbox\Classes\Configuration; use Socialbox\Classes\Resources; - use Socialbox\Enums\StandardError; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\ClientRequest; use Socialbox\Objects\RpcRequest; diff --git a/src/Socialbox/Classes/StandardMethods/GetTermsOfService.php b/src/Socialbox/Classes/StandardMethods/GetTermsOfService.php index 96c1d51..c0c9224 100644 --- a/src/Socialbox/Classes/StandardMethods/GetTermsOfService.php +++ b/src/Socialbox/Classes/StandardMethods/GetTermsOfService.php @@ -5,7 +5,6 @@ use Socialbox\Abstracts\Method; use Socialbox\Classes\Configuration; use Socialbox\Classes\Resources; - use Socialbox\Enums\StandardError; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\ClientRequest; use Socialbox\Objects\RpcRequest; diff --git a/src/Socialbox/Classes/StandardMethods/SettingsSetDisplayName.php b/src/Socialbox/Classes/StandardMethods/SettingsSetDisplayName.php new file mode 100644 index 0000000..93b8d90 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/SettingsSetDisplayName.php @@ -0,0 +1,46 @@ +containsParameter('name')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'name' parameter"); + } + + try + { + // Set the password + RegisteredPeerManager::updateDisplayName($request->getPeer(), $rpcRequest->getParameter('name')); + + // Remove the SET_PASSWORD flag + SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::SET_DISPLAY_NAME]); + + // Check & update the session flow + SessionManager::updateFlow($request->getSession()); + } + catch(Exception $e) + { + throw new StandardException('Failed to set password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/Utilities.php b/src/Socialbox/Classes/Utilities.php index ff38b98..7d1b73b 100644 --- a/src/Socialbox/Classes/Utilities.php +++ b/src/Socialbox/Classes/Utilities.php @@ -2,12 +2,10 @@ namespace Socialbox\Classes; -use DateTime; use InvalidArgumentException; use JsonException; use RuntimeException; use Socialbox\Enums\StandardHeaders; -use Socialbox\Objects\PeerAddress; use Throwable; class Utilities diff --git a/src/Socialbox/Enums/Flags/PeerFlags.php b/src/Socialbox/Enums/Flags/PeerFlags.php index 1ca08b2..a92537a 100644 --- a/src/Socialbox/Enums/Flags/PeerFlags.php +++ b/src/Socialbox/Enums/Flags/PeerFlags.php @@ -2,8 +2,6 @@ namespace Socialbox\Enums\Flags; -use Socialbox\Classes\Logger; - enum PeerFlags : string { // Administrative Flags diff --git a/src/Socialbox/Managers/RegisteredPeerManager.php b/src/Socialbox/Managers/RegisteredPeerManager.php index e70c4cc..669ec6b 100644 --- a/src/Socialbox/Managers/RegisteredPeerManager.php +++ b/src/Socialbox/Managers/RegisteredPeerManager.php @@ -2,6 +2,7 @@ namespace Socialbox\Managers; + use InvalidArgumentException; use PDO; use PDOException; use Socialbox\Classes\Configuration; @@ -317,6 +318,50 @@ } } + /** + * Updates the display name of a registered peer based on the given unique identifier or RegisteredPeerRecord object. + * + * @param string|RegisteredPeerRecord $peer The unique identifier of the registered peer, or an instance of RegisteredPeerRecord. + * @param string $name The new + */ + public static function updateDisplayName(string|RegisteredPeerRecord $peer, string $name): void + { + if(empty($name)) + { + throw new InvalidArgumentException('The display name cannot be empty'); + } + + if(strlen($name) > 256) + { + throw new InvalidArgumentException('The display name cannot exceed 256 characters'); + } + + if(is_string($peer)) + { + $peer = self::getPeer($peer); + } + + if($peer->isExternal()) + { + throw new InvalidArgumentException('Cannot update the display name of an external peer'); + } + + Logger::getLogger()->verbose(sprintf("Updating display name of peer %s to %s", $peer->getUuid(), $name)); + + try + { + $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET display_name=? WHERE uuid=?'); + $statement->bindParam(1, $name); + $uuid = $peer->getUuid(); + $statement->bindParam(2, $uuid); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to update the display name of the peer in the database', $e); + } + } + /** * Retrieves the password authentication record associated with the given unique peer identifier or a RegisteredPeerRecord object. * diff --git a/src/Socialbox/Managers/SessionManager.php b/src/Socialbox/Managers/SessionManager.php index c96bc2a..4111e8d 100644 --- a/src/Socialbox/Managers/SessionManager.php +++ b/src/Socialbox/Managers/SessionManager.php @@ -460,6 +460,13 @@ */ public static function updateFlow(SessionRecord $session): void { + // Don't do anything if the session is already authenticated + if(!in_array(SessionFlags::REGISTRATION_REQUIRED, $session->getFlags()) || !in_array(SessionFlags::AUTHENTICATION_REQUIRED, $session->getFlags())) + { + return; + } + + // Check if all registration/authentication requirements are met if(SessionFlags::isComplete($session->getFlags())) { SessionManager::setAuthenticated($session->getUuid(), true); diff --git a/src/Socialbox/Objects/ClientRequest.php b/src/Socialbox/Objects/ClientRequest.php index 5a368fb..00bd0fc 100644 --- a/src/Socialbox/Objects/ClientRequest.php +++ b/src/Socialbox/Objects/ClientRequest.php @@ -4,7 +4,6 @@ use InvalidArgumentException; use Socialbox\Classes\Cryptography; - use Socialbox\Classes\Logger; use Socialbox\Classes\Utilities; use Socialbox\Enums\SessionState; use Socialbox\Enums\StandardHeaders; diff --git a/src/Socialbox/Objects/Database/CaptchaRecord.php b/src/Socialbox/Objects/Database/CaptchaRecord.php index 0e94d68..72b77e2 100644 --- a/src/Socialbox/Objects/Database/CaptchaRecord.php +++ b/src/Socialbox/Objects/Database/CaptchaRecord.php @@ -4,7 +4,6 @@ namespace Socialbox\Objects\Database; use DateTime; use Socialbox\Classes\Configuration; -use Socialbox\Classes\Logger; use Socialbox\Enums\Status\CaptchaStatus; use Socialbox\Interfaces\SerializableInterface; diff --git a/src/Socialbox/Objects/Database/EncryptionRecord.php b/src/Socialbox/Objects/Database/EncryptionRecord.php index e2aad9c..707c39d 100644 --- a/src/Socialbox/Objects/Database/EncryptionRecord.php +++ b/src/Socialbox/Objects/Database/EncryptionRecord.php @@ -5,7 +5,6 @@ use Socialbox\Classes\Configuration; use Socialbox\Classes\SecuredPassword; use Socialbox\Exceptions\CryptographyException; - use Socialbox\Managers\EncryptionRecordsManager; class EncryptionRecord { diff --git a/src/Socialbox/Objects/RpcRequest.php b/src/Socialbox/Objects/RpcRequest.php index b0ffa88..6bc1d94 100644 --- a/src/Socialbox/Objects/RpcRequest.php +++ b/src/Socialbox/Objects/RpcRequest.php @@ -3,10 +3,8 @@ namespace Socialbox\Objects; use InvalidArgumentException; - use ncc\ThirdParty\nikic\PhpParser\Node\Expr\BinaryOp\BooleanOr; use Socialbox\Classes\Logger; use Socialbox\Enums\StandardError; - use Socialbox\Exceptions\RpcException; use Socialbox\Exceptions\StandardException; use Socialbox\Interfaces\SerializableInterface; diff --git a/src/Socialbox/Objects/Standard/ImageCaptcha.php b/src/Socialbox/Objects/Standard/ImageCaptcha.php index ed66b24..cbcc158 100644 --- a/src/Socialbox/Objects/Standard/ImageCaptcha.php +++ b/src/Socialbox/Objects/Standard/ImageCaptcha.php @@ -3,7 +3,6 @@ namespace Socialbox\Objects\Standard; use Socialbox\Interfaces\SerializableInterface; -use Socialbox\Objects\Database\CaptchaRecord; class ImageCaptcha implements SerializableInterface { From 738f8a455c80c0601c2782848074635726278163 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 24 Dec 2024 15:05:35 -0500 Subject: [PATCH 074/420] Add display picture support and error code refactor --- .../ClientCommands/StorageConfiguration.php | 2 +- .../SettingsSetDisplayPicture.php | 69 ++ src/Socialbox/Classes/Utilities.php | 614 ++++++++++-------- src/Socialbox/Enums/StandardError.php | 37 +- src/Socialbox/Enums/StandardMethods.php | 2 + .../Managers/RegisteredPeerManager.php | 57 +- .../Objects/Database/RegisteredPeerRecord.php | 28 + 7 files changed, 523 insertions(+), 286 deletions(-) create mode 100644 src/Socialbox/Classes/StandardMethods/SettingsSetDisplayPicture.php diff --git a/src/Socialbox/Classes/ClientCommands/StorageConfiguration.php b/src/Socialbox/Classes/ClientCommands/StorageConfiguration.php index 913c3d4..0050418 100644 --- a/src/Socialbox/Classes/ClientCommands/StorageConfiguration.php +++ b/src/Socialbox/Classes/ClientCommands/StorageConfiguration.php @@ -37,7 +37,7 @@ */ public function getUserDisplayImagesPath(): string { - return $this->userDisplayImagesPath; + return $this->path . DIRECTORY_SEPARATOR . $this->userDisplayImagesPath; } /** diff --git a/src/Socialbox/Classes/StandardMethods/SettingsSetDisplayPicture.php b/src/Socialbox/Classes/StandardMethods/SettingsSetDisplayPicture.php new file mode 100644 index 0000000..ddd77d2 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/SettingsSetDisplayPicture.php @@ -0,0 +1,69 @@ +containsParameter('image')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'image' parameter"); + } + + if(strlen($rpcRequest->getParameter('image')) > Configuration::getStorageConfiguration()->getUserDisplayImagesMaxSize()) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Image size exceeds the maximum allowed size of " . Configuration::getStorageConfiguration()->getUserDisplayImagesMaxSize() . " bytes"); + } + + try + { + $decodedImage = base64_decode($rpcRequest->getParameter('image')); + + if($decodedImage === false) + { + return $rpcRequest->produceError(StandardError::BAD_REQUEST, "Failed to decode JPEG image base64 data"); + } + + $sanitizedImage = Utilities::resizeImage(Utilities::sanitizeJpeg($decodedImage), 126, 126); + } + catch(Exception $e) + { + throw new StandardException('Failed to process JPEG image: ' . $e->getMessage(), StandardError::BAD_REQUEST, $e); + } + + try + { + // Set the password + RegisteredPeerManager::updateDisplayPicture($request->getPeer(), $sanitizedImage); + + // Remove the SET_DISPLAY_PICTURE flag + SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::SET_DISPLAY_PICTURE]); + + // Check & update the session flow + SessionManager::updateFlow($request->getSession()); + } + catch(Exception $e) + { + throw new StandardException('Failed to update display picture: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/Utilities.php b/src/Socialbox/Classes/Utilities.php index 7d1b73b..351acc5 100644 --- a/src/Socialbox/Classes/Utilities.php +++ b/src/Socialbox/Classes/Utilities.php @@ -1,287 +1,367 @@ new InvalidArgumentException("JSON decoding failed: Maximum stack depth exceeded"), - JSON_ERROR_STATE_MISMATCH => new InvalidArgumentException("JSON decoding failed: Underflow or the modes mismatch"), - JSON_ERROR_CTRL_CHAR => new InvalidArgumentException("JSON decoding failed: Unexpected control character found"), - JSON_ERROR_SYNTAX => new InvalidArgumentException("JSON decoding failed: Syntax error, malformed JSON"), - JSON_ERROR_UTF8 => new InvalidArgumentException("JSON decoding failed: Malformed UTF-8 characters, possibly incorrectly encoded"), - default => new InvalidArgumentException("JSON decoding failed: Unknown error"), - }; - } - - 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 InvalidArgumentException("Failed to encode json input", $e); - } - } - - /** - * Encodes the given data in Base64. - * - * @param string $data The data to be encoded. - * @return string The Base64 encoded string. - * @throws InvalidArgumentException if the encoding fails. - */ - public static function base64encode(string $data): string - { - $encoded = base64_encode($data); - - if (!$encoded) - { - throw new InvalidArgumentException('Failed to encode data in Base64'); - } - - return $encoded; - } - - /** - * Decodes a Base64 encoded string. - * - * @param string $data The Base64 encoded data to be decoded. - * @return string The decoded data. - * @throws InvalidArgumentException If decoding fails. - */ - public static function base64decode(string $data): string - { - $decoded = base64_decode($data, true); - - if ($decoded === false) - { - throw new InvalidArgumentException('Failed to decode data from Base64'); - } - - return $decoded; - } - - /** - * Returns the request headers as an associative array - * - * @return array - */ - public static function getRequestHeaders(): array - { - // Check if function getallheaders() exists - if (function_exists('getallheaders')) - { - $headers = getallheaders(); - } - else - { - // Fallback for servers where getallheaders() is not available - $headers = []; - foreach ($_SERVER as $key => $value) - { - if (str_starts_with($key, 'HTTP_')) + throw match (json_last_error()) { - // Convert header names to the normal HTTP format - $headers[str_replace('_', '-', strtolower(substr($key, 5)))] = $value; + JSON_ERROR_DEPTH => new InvalidArgumentException("JSON decoding failed: Maximum stack depth exceeded"), + JSON_ERROR_STATE_MISMATCH => new InvalidArgumentException("JSON decoding failed: Underflow or the modes mismatch"), + JSON_ERROR_CTRL_CHAR => new InvalidArgumentException("JSON decoding failed: Unexpected control character found"), + JSON_ERROR_SYNTAX => new InvalidArgumentException("JSON decoding failed: Syntax error, malformed JSON"), + JSON_ERROR_UTF8 => new InvalidArgumentException("JSON decoding failed: Malformed UTF-8 characters, possibly incorrectly encoded"), + default => new InvalidArgumentException("JSON decoding failed: Unknown error"), + }; + } + + 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 InvalidArgumentException("Failed to encode json input", $e); + } + } + + /** + * Encodes the given data in Base64. + * + * @param string $data The data to be encoded. + * @return string The Base64 encoded string. + * @throws InvalidArgumentException if the encoding fails. + */ + public static function base64encode(string $data): string + { + $encoded = base64_encode($data); + + if (!$encoded) + { + throw new InvalidArgumentException('Failed to encode data in Base64'); + } + + return $encoded; + } + + /** + * Decodes a Base64 encoded string. + * + * @param string $data The Base64 encoded data to be decoded. + * @return string The decoded data. + * @throws InvalidArgumentException If decoding fails. + */ + public static function base64decode(string $data): string + { + $decoded = base64_decode($data, true); + + if ($decoded === false) + { + throw new InvalidArgumentException('Failed to decode data from Base64'); + } + + return $decoded; + } + + /** + * Returns the request headers as an associative array + * + * @return array + */ + public static function getRequestHeaders(): array + { + // Check if function getallheaders() exists + if (function_exists('getallheaders')) + { + $headers = getallheaders(); + } + else + { + // Fallback for servers where getallheaders() is not available + $headers = []; + foreach ($_SERVER as $key => $value) + { + if (str_starts_with($key, 'HTTP_')) + { + // Convert header names to the normal HTTP format + $headers[str_replace('_', '-', strtolower(substr($key, 5)))] = $value; + } + } + } + + if($headers === false) + { + throw new RuntimeException('Failed to get request headers'); + } + + return $headers; + } + + /** + * Converts a Throwable object into a formatted string. + * + * @param Throwable $e The throwable to be converted into a string. + * @return string The formatted string representation of the throwable, including the exception class, message, file, line, and stack trace. + */ + public static function throwableToString(Throwable $e): string + { + return sprintf( + "%s: %s in %s:%d\nStack trace:\n%s", + get_class($e), + $e->getMessage(), + $e->getFile(), + $e->getLine(), + $e->getTraceAsString() + ); + } + + /** + * Generates a formatted header string. + * + * @param StandardHeaders $header The standard header object. + * @param string $value The header value to be associated with the standard header. + * @return string The formatted header string. + */ + public static function generateHeader(StandardHeaders $header, string $value): string + { + return $header->value . ': ' . $value; + } + + /** + * Generates a random string of specified length using the provided character set. + * + * @param int $int The length of the random string to be generated. + * @param string $string The character set to use for generating the random string. + * @return string The generated random string. + */ + public static function randomString(int $int, string $string): string + { + $characters = str_split($string); + $randomString = ''; + + for ($i = 0; $i < $int; $i++) + { + $randomString .= $characters[array_rand($characters)]; + } + + return $randomString; + } + + /** + * Generates a random CRC32 hash. + * + * @return string The generated CRC32 hash as a string. + */ + public static function randomCrc32(): string + { + return hash('crc32b', uniqid()); + } + + /** + * Sanitizes a file name by removing any characters that are not alphanumeric, hyphen, or underscore. + * + * @param string $name The file name to be sanitized. + * @return string The sanitized file name. + */ + public static function sanitizeFileName(string $name): string + { + return preg_replace('/[^a-zA-Z0-9-_]/', '', $name); + } + + + /** + * Sanitizes a Base64-encoded JPEG image by validating its data, decoding it, + * and re-encoding it to ensure it conforms to the JPEG format. + * + * @param string $data The Base64-encoded string potentially containing a JPEG image, + * optionally prefixed with "data:image/...;base64,". + * @return string A sanitized and re-encoded JPEG image as a binary string. + * @throws InvalidArgumentException If the input data is not valid Base64, + * does not represent an image, or is not in the JPEG format. + */ + public static function sanitizeJpeg(string $data): string + { + // Temporarily load the decoded data as an image + $tempResource = imagecreatefromstring($data); + + // Validate that the decoded data is indeed an image + if ($tempResource === false) + { + throw new InvalidArgumentException("The data does not represent a valid image."); + } + + // Validate MIME type using getimagesizefromstring + $imageInfo = getimagesizefromstring($data); + if ($imageInfo === false || $imageInfo['mime'] !== 'image/jpeg') + { + imagedestroy($tempResource); // Cleanup resources + throw new InvalidArgumentException("The image is not a valid JPEG format."); + } + + // Capture the re-encoded image in memory and return it as a string + ob_start(); // Start output buffering + $saveResult = imagejpeg($tempResource, null, 100); // Max quality, save to output buffer + imagedestroy($tempResource); // Free up memory resources + + if (!$saveResult) + { + ob_end_clean(); // Clean the output buffer if encoding failed + throw new InvalidArgumentException("Failed to encode the sanitized image."); + } + + // Return the sanitized jpeg image as the result + return ob_get_clean(); + } + + /** + * Resizes an image to a specified width and height while maintaining its aspect ratio. + * The resized image is centered on a black background matching the target dimensions. + * + * @param string $data The binary data of the source image. + * @param int $width The desired width of the resized image. + * @param int $height The desired height of the resized image. + * @return string The binary data of the resized image in PNG format. + * @throws InvalidArgumentException If the source image cannot be created from the provided data. + * @throws Exception If image processing fails during resizing. + */ + public static function resizeImage(string $data, int $width, int $height): string + { + try + { + // Create image resource from binary data + $sourceImage = imagecreatefromstring($data); + if (!$sourceImage) + { + throw new InvalidArgumentException("Failed to create image from provided data"); + } + + // Get original dimensions + $sourceWidth = imagesx($sourceImage); + $sourceHeight = imagesy($sourceImage); + + // Calculate aspect ratios + $sourceRatio = $sourceWidth / $sourceHeight; + $targetRatio = $width / $height; + + // Initialize dimensions for scaling + $scaleWidth = $width; + $scaleHeight = $height; + + // Calculate scaling dimensions to maintain aspect ratio + if ($sourceRatio > $targetRatio) + { + // Source image is wider - scale by width + $scaleHeight = $width / $sourceRatio; + } + else + { + // Source image is taller - scale by height + $scaleWidth = $height * $sourceRatio; + } + + // Create target image with desired dimensions + $targetImage = imagecreatetruecolor($width, $height); + if (!$targetImage) + { + throw new Exception("Failed to create target image"); + } + + // Fill background with black + $black = imagecolorallocate($targetImage, 0, 0, 0); + imagefill($targetImage, 0, 0, $black); + + // Calculate padding to center the scaled image + $paddingX = ($width - $scaleWidth) / 2; + $paddingY = ($height - $scaleHeight) / 2; + + // Enable alpha blending + imagealphablending($targetImage, true); + imagesavealpha($targetImage, true); + + // Resize and copy the image with high-quality resampling + if (!imagecopyresampled($targetImage, $sourceImage, (int)$paddingX, (int)$paddingY, 0, 0, (int)$scaleWidth, (int)$scaleHeight, $sourceWidth, $sourceHeight)) + { + throw new Exception("Failed to resize image"); + } + + // Start output buffering + ob_start(); + + // Output image as PNG (you can modify this to support other formats) + imagepng($targetImage); + + // Return the image data + return ob_get_clean(); + } + finally + { + if (isset($sourceImage)) + { + imagedestroy($sourceImage); + } + + if (isset($targetImage)) + { + imagedestroy($targetImage); } } } - if($headers === false) + /** + * Converts an array into a serialized string by joining the elements with a comma. + * + * @param array $list An array of elements that need to be converted to a comma-separated string. + * @return string A string representation of the array elements, joined by commas. + */ + public static function serializeList(array $list): string { - throw new RuntimeException('Failed to get request headers'); + return implode(',', $list); } - return $headers; - } - - /** - * Converts a Throwable object into a formatted string. - * - * @param Throwable $e The throwable to be converted into a string. - * @return string The formatted string representation of the throwable, including the exception class, message, file, line, and stack trace. - */ - public static function throwableToString(Throwable $e): string - { - return sprintf( - "%s: %s in %s:%d\nStack trace:\n%s", - get_class($e), - $e->getMessage(), - $e->getFile(), - $e->getLine(), - $e->getTraceAsString() - ); - } - - /** - * Generates a formatted header string. - * - * @param StandardHeaders $header The standard header object. - * @param string $value The header value to be associated with the standard header. - * @return string The formatted header string. - */ - public static function generateHeader(StandardHeaders $header, string $value): string - { - return $header->value . ': ' . $value; - } - - /** - * Generates a random string of specified length using the provided character set. - * - * @param int $int The length of the random string to be generated. - * @param string $string The character set to use for generating the random string. - * @return string The generated random string. - */ - public static function randomString(int $int, string $string): string - { - $characters = str_split($string); - $randomString = ''; - - for ($i = 0; $i < $int; $i++) + /** + * Converts a serialized string into an array by splitting the string at each comma. + * + * @param string $list A comma-separated string that needs to be converted to an array. + * @return array An array of string values obtained by splitting the input string. + */ + public static function unserializeList(string $list): array { - $randomString .= $characters[array_rand($characters)]; + return explode(',', $list); } - return $randomString; - } - - /** - * Generates a random CRC32 hash. - * - * @return string The generated CRC32 hash as a string. - */ - public static function randomCrc32(): string - { - return hash('crc32b', uniqid()); - } - - /** - * Sanitizes a file name by removing any characters that are not alphanumeric, hyphen, or underscore. - * - * @param string $name The file name to be sanitized. - * @return string The sanitized file name. - */ - public static function sanitizeFileName(string $name): string - { - return preg_replace('/[^a-zA-Z0-9-_]/', '', $name); - } - - - /** - * Sanitizes a Base64-encoded JPEG image by validating its data, decoding it, - * and re-encoding it to ensure it conforms to the JPEG format. - * - * @param string $data The Base64-encoded string potentially containing a JPEG image, - * optionally prefixed with "data:image/...;base64,". - * @return string A sanitized and re-encoded JPEG image as a binary string. - * @throws InvalidArgumentException If the input data is not valid Base64, - * does not represent an image, or is not in the JPEG format. - */ - public static function sanitizeBase64Jpeg(string $data): string - { - // Detect and strip the potential "data:image/...;base64," prefix, if present - if (str_contains($data, ',')) + /** + * Checks if the given HTTP response code indicates success or failure. + * + * @param int $responseCode The HTTP response code to check. + * @return bool True if the response code indicates success, false otherwise. + */ + public static function isSuccessCodes(int $responseCode): bool { - [, $data] = explode(',', $data, 2); + return $responseCode >= 200 && $responseCode < 300; } - - // Decode the Base64 string - $decodedData = base64_decode($data, true); - - // Check if decoding succeeded - if ($decodedData === false) - { - throw new InvalidArgumentException("Invalid Base64 data."); - } - - // Temporarily load the decoded data as an image - $tempResource = imagecreatefromstring($decodedData); - - // Validate that the decoded data is indeed an image - if ($tempResource === false) - { - throw new InvalidArgumentException("The Base64 data does not represent a valid image."); - } - - // Validate MIME type using getimagesizefromstring - $imageInfo = getimagesizefromstring($decodedData); - if ($imageInfo === false || $imageInfo['mime'] !== 'image/jpeg') - { - imagedestroy($tempResource); // Cleanup resources - throw new InvalidArgumentException("The image is not a valid JPEG format."); - } - - // Capture the re-encoded image in memory and return it as a string - ob_start(); // Start output buffering - $saveResult = imagejpeg($tempResource, null, 100); // Max quality, save to output buffer - imagedestroy($tempResource); // Free up memory resources - - if (!$saveResult) - { - ob_end_clean(); // Clean the output buffer if encoding failed - throw new InvalidArgumentException("Failed to encode the sanitized image."); - } - - // Return the sanitized jpeg image as the result - return ob_get_clean(); - } - - /** - * Converts an array into a serialized string by joining the elements with a comma. - * - * @param array $list An array of elements that need to be converted to a comma-separated string. - * @return string A string representation of the array elements, joined by commas. - */ - public static function serializeList(array $list): string - { - return implode(',', $list); - } - - /** - * Converts a serialized string into an array by splitting the string at each comma. - * - * @param string $list A comma-separated string that needs to be converted to an array. - * @return array An array of string values obtained by splitting the input string. - */ - public static function unserializeList(string $list): array - { - return explode(',', $list); - } - - /** - * Checks if the given HTTP response code indicates success or failure. - * - * @param int $responseCode The HTTP response code to check. - * @return bool True if the response code indicates success, false otherwise. - */ - public static function isSuccessCodes(int $responseCode): bool - { - return $responseCode >= 200 && $responseCode < 300; - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/Socialbox/Enums/StandardError.php b/src/Socialbox/Enums/StandardError.php index d634a79..6b7544a 100644 --- a/src/Socialbox/Enums/StandardError.php +++ b/src/Socialbox/Enums/StandardError.php @@ -15,28 +15,31 @@ enum StandardError : int case INTERNAL_SERVER_ERROR = -2000; case SERVER_UNAVAILABLE = -2001; + // Client Errors + case BAD_REQUEST = -3000; + case METHOD_NOT_ALLOWED = -3001; + // Authentication/Cryptography Errors - case INVALID_PUBLIC_KEY = -3000; + case INVALID_PUBLIC_KEY = -4000; // * - case SESSION_REQUIRED = -3001; - case SESSION_NOT_FOUND = -3002; - case SESSION_EXPIRED = -3003; - case SESSION_DHE_REQUIRED = -3004; + case SESSION_REQUIRED = -5001; // * + case SESSION_NOT_FOUND = -5002; // * + case SESSION_EXPIRED = -5003; // * + case SESSION_DHE_REQUIRED = -5004; // * - case ALREADY_AUTHENTICATED = -3005; - case UNSUPPORTED_AUTHENTICATION_TYPE = -3006; - case AUTHENTICATION_REQUIRED = -3007; - case REGISTRATION_DISABLED = -3008; - case CAPTCHA_NOT_AVAILABLE = -3009; - case INCORRECT_CAPTCHA_ANSWER = -3010; - case CAPTCHA_EXPIRED = -3011; + case ALREADY_AUTHENTICATED = -6005; + case UNSUPPORTED_AUTHENTICATION_TYPE = -6006; + case AUTHENTICATION_REQUIRED = -6007; + case REGISTRATION_DISABLED = -6008; + case CAPTCHA_NOT_AVAILABLE = -6009; + case INCORRECT_CAPTCHA_ANSWER = -6010; + case CAPTCHA_EXPIRED = -6011; // General Error Messages - case PEER_NOT_FOUND = -4000; - case INVALID_USERNAME = -4001; - case USERNAME_ALREADY_EXISTS = -4002; - case NOT_REGISTERED = -4003; - case METHOD_NOT_ALLOWED = -4004; + case PEER_NOT_FOUND = -7000; + case INVALID_USERNAME = -7001; + case USERNAME_ALREADY_EXISTS = -7002; + case NOT_REGISTERED = -7003; /** * Returns the default generic message for the error diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index 0afad9c..ce0b495 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -10,6 +10,7 @@ use Socialbox\Classes\StandardMethods\GetSessionState; use Socialbox\Classes\StandardMethods\GetTermsOfService; use Socialbox\Classes\StandardMethods\Ping; + use Socialbox\Classes\StandardMethods\SettingsSetDisplayName; use Socialbox\Classes\StandardMethods\SettingsSetPassword; use Socialbox\Classes\StandardMethods\VerificationAnswerImageCaptcha; use Socialbox\Classes\StandardMethods\VerificationGetImageCaptcha; @@ -83,6 +84,7 @@ self::VERIFICATION_ANSWER_IMAGE_CAPTCHA => VerificationAnswerImageCaptcha::execute($request, $rpcRequest), self::SETTINGS_SET_PASSWORD => SettingsSetPassword::execute($request, $rpcRequest), + self::SETTINGS_SET_DISPLAY_NAME => SettingsSetDisplayName::execute($request, $rpcRequest), default => $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, sprintf("The method %s is not supported by the server", $rpcRequest->getMethod())) }; diff --git a/src/Socialbox/Managers/RegisteredPeerManager.php b/src/Socialbox/Managers/RegisteredPeerManager.php index 669ec6b..0595ae5 100644 --- a/src/Socialbox/Managers/RegisteredPeerManager.php +++ b/src/Socialbox/Managers/RegisteredPeerManager.php @@ -322,7 +322,8 @@ * Updates the display name of a registered peer based on the given unique identifier or RegisteredPeerRecord object. * * @param string|RegisteredPeerRecord $peer The unique identifier of the registered peer, or an instance of RegisteredPeerRecord. - * @param string $name The new + * @param string $name The new display name to set to the user + * @throws DatabaseOperationException Thrown if there was an error while trying to update the display name */ public static function updateDisplayName(string|RegisteredPeerRecord $peer, string $name): void { @@ -362,6 +363,60 @@ } } + /** + * Updates the display picture of a registered peer in the database. + * + * @param string|RegisteredPeerRecord $peer The unique identifier of the peer or an instance of RegisteredPeerRecord. + * @param string $displayPictureData The raw jpeg data of the display picture. + * @return void + * @throws DatabaseOperationException If there is an error during the database operation. + */ + public static function updateDisplayPicture(string|RegisteredPeerRecord $peer, string $displayPictureData): void + { + if(empty($uuid)) + { + throw new InvalidArgumentException('The display picture UUID cannot be empty'); + } + + $uuid = Uuid::v4()->toRfc4122(); + $displayPicturePath = Configuration::getStorageConfiguration()->getUserDisplayImagesPath() . DIRECTORY_SEPARATOR . $uuid . '.jpeg'; + + // Delete the file if it already exists + if(file_exists($displayPicturePath)) + { + unlink($displayPicturePath); + } + + // Write the file contents & set the permissions + file_put_contents($displayPicturePath, $displayPictureData); + chmod($displayPicturePath, 0644); + + if(is_string($peer)) + { + $peer = self::getPeer($peer); + } + + if($peer->isExternal()) + { + throw new InvalidArgumentException('Cannot update the display picture of an external peer'); + } + + Logger::getLogger()->verbose(sprintf("Updating display picture of peer %s to %s", $peer->getUuid(), $uuid)); + + try + { + $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET display_picture=? WHERE uuid=?'); + $statement->bindParam(1, $uuid); + $peerUuid = $peer->getUuid(); + $statement->bindParam(2, $peerUuid); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to update the display picture of the peer in the database', $e); + } + } + /** * Retrieves the password authentication record associated with the given unique peer identifier or a RegisteredPeerRecord object. * diff --git a/src/Socialbox/Objects/Database/RegisteredPeerRecord.php b/src/Socialbox/Objects/Database/RegisteredPeerRecord.php index f0c14b8..7d805bb 100644 --- a/src/Socialbox/Objects/Database/RegisteredPeerRecord.php +++ b/src/Socialbox/Objects/Database/RegisteredPeerRecord.php @@ -14,6 +14,7 @@ private string $username; private string $server; private ?string $displayName; + private ?string $displayPicture; /** * @var PeerFlags[] */ @@ -33,6 +34,7 @@ $this->username = $data['username']; $this->server = $data['server']; $this->displayName = $data['display_name'] ?? null; + $this->displayPicture = $data['display_picture'] ?? null; if($data['flags']) { @@ -105,16 +107,41 @@ return $this->displayName; } + /** + * Retrieves the display picture. + * + * @return string|null The display picture if set, or null otherwise. + */ + public function getDisplayPicture(): ?string + { + return $this->displayPicture; + } + + /** + * Retrieves the flags. + * + * @return PeerFlags[] The flags. + */ public function getFlags(): array { return $this->flags; } + /** + * Adds a flag to the current instance. + * + * @param PeerFlags $flag The flag to add. + */ public function flagExists(PeerFlags $flag): bool { return in_array($flag, $this->flags, true); } + /** + * Adds a flag to the current instance. + * + * @param PeerFlags $flag The flag to add. + */ public function removeFlag(PeerFlags $flag): void { $key = array_search($flag, $this->flags, true); @@ -182,6 +209,7 @@ 'username' => $this->username, 'server' => $this->server, 'display_name' => $this->displayName, + 'display_picture' => $this->displayPicture, 'flags' => PeerFlags::toString($this->flags), 'enabled' => $this->enabled, 'created' => $this->created From c380556255dca49eb27ab9fceda624b4ef1275fd Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 24 Dec 2024 19:18:06 -0500 Subject: [PATCH 075/420] Refactor session initialization and host validation logic --- src/Socialbox/Classes/RpcClient.php | 36 ++++- src/Socialbox/Enums/Options/ClientOptions.php | 8 ++ src/Socialbox/Objects/PeerAddress.php | 3 +- src/Socialbox/SocialClient.php | 1 + src/Socialbox/Socialbox.php | 134 +++++++++++++----- 5 files changed, 138 insertions(+), 44 deletions(-) create mode 100644 src/Socialbox/Enums/Options/ClientOptions.php diff --git a/src/Socialbox/Classes/RpcClient.php b/src/Socialbox/Classes/RpcClient.php index 1be63d1..9c3a42e 100644 --- a/src/Socialbox/Classes/RpcClient.php +++ b/src/Socialbox/Classes/RpcClient.php @@ -2,6 +2,7 @@ namespace Socialbox\Classes; + use Socialbox\Enums\Options\ClientOptions; use Socialbox\Enums\StandardHeaders; use Socialbox\Enums\Types\RequestType; use Socialbox\Exceptions\CryptographyException; @@ -60,7 +61,19 @@ // Set the initial properties $this->peerAddress = $peerAddress; - $this->keyPair = Cryptography::generateKeyPair(); + + // If the username is `host` and the domain is the same as this server's domain, we use our keypair + // Essentially this is a special case for the server to contact another server + if($this->peerAddress->isHost()) + { + $this->keyPair = new KeyPair(Configuration::getInstanceConfiguration()->getPublicKey(), Configuration::getInstanceConfiguration()->getPrivateKey()); + } + // Otherwise we generate a random keypair + else + { + $this->keyPair = Cryptography::generateKeyPair(); + } + $this->encryptionKey = Cryptography::generateEncryptionKey(); // Resolve the domain and get the server's Public Key & RPC Endpoint @@ -97,16 +110,25 @@ { $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $this->rpcEndpoint); - curl_setopt($ch, CURLOPT_HTTPGET, true); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_HTTPHEADER, [ + // Basic session details + $headers = [ StandardHeaders::REQUEST_TYPE->value . ': ' . RequestType::INITIATE_SESSION->value, StandardHeaders::CLIENT_NAME->value . ': ' . self::CLIENT_NAME, StandardHeaders::CLIENT_VERSION->value . ': ' . self::CLIENT_VERSION, - StandardHeaders::PUBLIC_KEY->value . ': ' . $this->keyPair->getPublicKey(), StandardHeaders::IDENTIFY_AS->value . ': ' . $this->peerAddress->getAddress(), - ]); + ]; + + // If we're not connecting as the host, we need to provide our public key + // Otherwise, the server will obtain the public key itself from DNS records rather than trusting the client + if(!$this->peerAddress->isHost()) + { + $headers[] = StandardHeaders::PUBLIC_KEY->value . ': ' . $this->keyPair->getPublicKey(); + } + + curl_setopt($ch, CURLOPT_URL, $this->rpcEndpoint); + curl_setopt($ch, CURLOPT_HTTPGET, true); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); $response = curl_exec($ch); diff --git a/src/Socialbox/Enums/Options/ClientOptions.php b/src/Socialbox/Enums/Options/ClientOptions.php new file mode 100644 index 0000000..1842f58 --- /dev/null +++ b/src/Socialbox/Enums/Options/ClientOptions.php @@ -0,0 +1,8 @@ +username === ReservedUsernames::HOST->value; + return $this->username === ReservedUsernames::HOST->value && $this->domain === Configuration::getInstanceConfiguration()->getDomain(); } /** diff --git a/src/Socialbox/SocialClient.php b/src/Socialbox/SocialClient.php index f23e79f..ad44546 100644 --- a/src/Socialbox/SocialClient.php +++ b/src/Socialbox/SocialClient.php @@ -23,6 +23,7 @@ * * @param string|PeerAddress $peerAddress The address of the peer to connect to. * @param ExportedSession|null $exportedSession Optional. The exported session to use for communication. + * @param array $options Optional. Additional options to pass to the client. * @throws CryptographyException If the public key is invalid. * @throws ResolutionException If the domain cannot be resolved. * @throws RpcException If the RPC request fails. diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index 7feb1d3..1cde47b 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -7,8 +7,10 @@ use Socialbox\Classes\Configuration; use Socialbox\Classes\Cryptography; use Socialbox\Classes\Logger; + use Socialbox\Classes\ServerResolver; use Socialbox\Classes\Utilities; use Socialbox\Classes\Validator; + use Socialbox\Enums\ReservedUsernames; use Socialbox\Enums\SessionState; use Socialbox\Enums\StandardError; use Socialbox\Enums\StandardHeaders; @@ -68,6 +70,58 @@ } } + /** + * Validates the headers in an initialization request to ensure that all + * required information is present and properly formatted. This includes + * checking for headers such as Client Name, Client Version, Public Key, + * and Identify-As, as well as validating the Identify-As header value. + * If any validation fails, a corresponding HTTP response code and message + * are returned. + * + * @param ClientRequest $clientRequest The client request containing headers to validate. + * + * @return bool Returns true if all required headers are valid, otherwise false. + */ + private static function validateInitHeaders(ClientRequest $clientRequest): bool + { + if(!$clientRequest->getClientName()) + { + http_response_code(400); + print('Missing required header: ' . StandardHeaders::CLIENT_NAME->value); + return false; + } + + if(!$clientRequest->getClientVersion()) + { + http_response_code(400); + print('Missing required header: ' . StandardHeaders::CLIENT_VERSION->value); + return false; + } + + if(!$clientRequest->headerExists(StandardHeaders::PUBLIC_KEY)) + { + http_response_code(400); + print('Missing required header: ' . StandardHeaders::PUBLIC_KEY->value); + return false; + } + + if(!$clientRequest->headerExists(StandardHeaders::IDENTIFY_AS)) + { + http_response_code(400); + print('Missing required header: ' . StandardHeaders::IDENTIFY_AS->value); + return false; + } + + if(!Validator::validatePeerAddress($clientRequest->getHeader(StandardHeaders::IDENTIFY_AS))) + { + http_response_code(400); + print('Invalid Identify-As header: ' . $clientRequest->getHeader(StandardHeaders::IDENTIFY_AS)); + return false; + } + + return true; + } + /** * Processes a client request to initiate a session. Validates required headers, * ensures the peer is authorized and enabled, and creates a new session UUID @@ -80,58 +134,66 @@ */ private static function handleInitiateSession(ClientRequest $clientRequest): void { - - if(!$clientRequest->getClientName()) + if(!self::validateInitHeaders($clientRequest)) { - http_response_code(400); - print('Missing required header: ' . StandardHeaders::CLIENT_NAME->value); return; } - if(!$clientRequest->getClientVersion()) - { - http_response_code(400); - print('Missing required header: ' . StandardHeaders::CLIENT_VERSION->value); - return; - } - - if(!$clientRequest->headerExists(StandardHeaders::PUBLIC_KEY)) - { - http_response_code(400); - print('Missing required header: ' . StandardHeaders::PUBLIC_KEY->value); - return; - } - - if(!$clientRequest->headerExists(StandardHeaders::IDENTIFY_AS)) - { - http_response_code(400); - print('Missing required header: ' . StandardHeaders::IDENTIFY_AS->value); - return; - } - - if(!Validator::validatePeerAddress($clientRequest->getHeader(StandardHeaders::IDENTIFY_AS))) - { - http_response_code(400); - print('Invalid Identify-As header: ' . $clientRequest->getHeader(StandardHeaders::IDENTIFY_AS)); - return; - } + // We always accept the client's public key at first + $publicKey = $clientRequest->getHeader(StandardHeaders::PUBLIC_KEY); // If the peer is identifying as the same domain if($clientRequest->getIdentifyAs()->getDomain() === Configuration::getInstanceConfiguration()->getDomain()) { // Prevent the peer from identifying as the host unless it's coming from an external domain - if($clientRequest->getIdentifyAs()->getUsername() === 'host') + if($clientRequest->getIdentifyAs()->getUsername() === ReservedUsernames::HOST->value) { http_response_code(403); print('Unauthorized: The requested peer is not allowed to identify as the host'); return; } } + // If the peer is identifying as an external domain else { - http_response_code(400); - print('External domains are not supported yet'); - return; + // Only allow the host to identify as an external peer + if($clientRequest->getIdentifyAs()->getUsername() !== ReservedUsernames::HOST->value) + { + http_response_code(403); + print('Unauthorized: The requested peer is not allowed to identify as an external peer'); + return; + } + + try + { + // We need to obtain the public key of the host, since we can't trust the client + $resolvedServer = ServerResolver::resolveDomain($clientRequest->getIdentifyAs()->getDomain()); + + // Override the public key with the resolved server's public key + $publicKey = $resolvedServer->getPublicKey(); + } + catch (Exceptions\ResolutionException $e) + { + Logger::getLogger()->error('Failed to resolve the host domain', $e); + http_response_code(409); + print('Conflict: Failed to resolve the host domain: ' . $e->getMessage()); + return; + } + catch (Exception $e) + { + Logger::getLogger()->error('An internal error occurred while resolving the host domain', $e); + http_response_code(500); + if(Configuration::getSecurityConfiguration()->isDisplayInternalExceptions()) + { + print(Utilities::throwableToString($e)); + } + else + { + print('An internal error occurred'); + } + + return; + } } try @@ -165,7 +227,7 @@ } // Create the session UUID - $sessionUuid = SessionManager::createSession($clientRequest->getHeader(StandardHeaders::PUBLIC_KEY), $registeredPeer, $clientRequest->getClientName(), $clientRequest->getClientVersion()); + $sessionUuid = SessionManager::createSession($publicKey, $registeredPeer, $clientRequest->getClientName(), $clientRequest->getClientVersion()); http_response_code(201); // Created print($sessionUuid); // Return the session UUID } From 85a81784f9e3ffee2ca959aafec6c85a838a1faf Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 30 Dec 2024 13:58:47 -0500 Subject: [PATCH 076/420] Updated database & Implemented Docker support. (unfinished) --- .env | 20 ++ .gitignore | 3 +- .idea/sqldialects.xml | 2 +- .idea/webResources.xml | 1 + Dockerfile | 119 ++++++++++++ docker-compose.yml | 98 ++++++++++ entrypoint.sh | 23 +++ nginx.conf | 37 ++++ project.json | 2 +- {examples => public}/index.php | 0 redis.conf | 4 + .../Classes/CliCommands/InitializeCommand.php | 174 +++++++++++++++++- .../Configuration/DatabaseConfiguration.php | 11 ++ src/Socialbox/Classes/Database.php | 60 +++--- .../database/authentication_passwords.sql | 20 ++ .../Resources/database/captcha_images.sql | 24 +++ .../Resources/database/encryption_records.sql | 8 + .../Resources/database/external_sessions.sql | 35 ++++ .../database/password_authentication.sql | 18 -- .../Resources/database/registered_peers.sql | 35 +++- .../Resources/database/resolved_servers.sql | 12 ++ .../Classes/Resources/database/sessions.sql | 21 ++- .../Classes/Resources/database/variables.sql | 15 +- src/Socialbox/Enums/DatabaseObjects.php | 68 +++---- supervisord.conf | 47 +++++ 25 files changed, 752 insertions(+), 105 deletions(-) create mode 100644 .env create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 entrypoint.sh create mode 100644 nginx.conf rename {examples => public}/index.php (100%) create mode 100644 redis.conf create mode 100644 src/Socialbox/Classes/Resources/database/authentication_passwords.sql create mode 100644 src/Socialbox/Classes/Resources/database/captcha_images.sql create mode 100644 src/Socialbox/Classes/Resources/database/encryption_records.sql create mode 100644 src/Socialbox/Classes/Resources/database/external_sessions.sql delete mode 100644 src/Socialbox/Classes/Resources/database/password_authentication.sql create mode 100644 src/Socialbox/Classes/Resources/database/resolved_servers.sql create mode 100644 supervisord.conf diff --git a/.env b/.env new file mode 100644 index 0000000..e6e3e7b --- /dev/null +++ b/.env @@ -0,0 +1,20 @@ +# Socialbox Configuration +LOG_LEVEL=debug +SB_MODE=automated +SB_DOMAIN=localhost +SB_RPC_ENDPOINT=http://127.0.0.0:8085/ + +# MariaDB Configuration +MYSQL_ROOT_PASSWORD=sb_root +MYSQL_DATABASE=socialbox +MYSQL_USER=socialbox +MYSQL_PASSWORD=socialbox + +# Redis Configuration +REDIS_PASSWORD=root + +# Test Configuration, can be ignored. Used for docker-compose-test.yml +SB_ALICE_DOMAIN=localhost +SB_ALICE_RPC_ENDPOINT=http://127.0.0.0:8086/ +SB_BOB_DOMAIN=localhost +SB_BOB_RPC_ENDPOINT=http://127.0.0.0:8087/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index 33a3681..e53be25 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /build /.idea/gbrowser_project.xml -.phpunit.result.cache \ No newline at end of file +.phpunit.result.cache +/socialbox \ No newline at end of file diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml index 5975784..bb99828 100644 --- a/.idea/sqldialects.xml +++ b/.idea/sqldialects.xml @@ -1,7 +1,7 @@ - + diff --git a/.idea/webResources.xml b/.idea/webResources.xml index 38d7e3c..7990450 100644 --- a/.idea/webResources.xml +++ b/.idea/webResources.xml @@ -7,6 +7,7 @@ + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..22628b7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,119 @@ +# ----------------------------------------------------------------------------- +# Dockerfile for PHP 8.3 + FPM with Cron support and Supervisor +# ----------------------------------------------------------------------------- + +# Base image: Official PHP 8.3 with FPM +FROM php:8.3-fpm AS base + +# ----------------------------- Metadata labels ------------------------------ +LABEL maintainer="Netkas " \ + version="1.0" \ + description="Socialbox Docker image based off PHP 8.3 FPM and NCC" \ + application="SocialBox" \ + base_image="php:8.3-fpm" + +# Environment variable for non-interactive installations +ENV DEBIAN_FRONTEND=noninteractive + +# ----------------------------- System Dependencies -------------------------- +# Update system packages and install required dependencies in one step +RUN apt-get update -yqq && apt-get install -yqq --no-install-recommends \ + git \ + libpq-dev \ + libzip-dev \ + zip \ + make \ + wget \ + gnupg \ + cron \ + supervisor \ + mariadb-client \ + libcurl4-openssl-dev \ + libmemcached-dev \ + redis \ + libgd-dev \ + nginx \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# ----------------------------- PHP Extensions ------------------------------- +# Install PHP extensions and enable additional ones +RUN docker-php-ext-install -j$(nproc) \ + pdo \ + pdo_mysql \ + mysqli \ + gd \ + curl \ + opcache \ + zip \ + pcntl && \ + pecl install redis memcached && \ + docker-php-ext-enable redis memcached + +# ----------------------------- Additional Tools ----------------------------- +# Install Phive (Package Manager for PHAR libraries) and global tools in one step +RUN wget -O /usr/local/bin/phive https://phar.io/releases/phive.phar && \ + wget -O /usr/local/bin/phive.asc https://phar.io/releases/phive.phar.asc && \ + gpg --keyserver hkps://keys.openpgp.org --recv-keys 0x9D8A98B29B2D5D79 && \ + gpg --verify /usr/local/bin/phive.asc /usr/local/bin/phive && \ + chmod +x /usr/local/bin/phive && \ + phive install phpab --global --trust-gpg-keys 0x2A8299CE842DD38C + +# ----------------------------- Clone and Build NCC -------------------------- +# Clone the NCC repository, build the project, and install it +RUN git clone https://git.n64.cc/nosial/ncc.git && \ + cd ncc && \ + make redist && \ + NCC_DIR=$(find build/ -type d -name "ncc_*" | head -n 1) && \ + if [ -z "$NCC_DIR" ]; then \ + echo "NCC build directory not found"; \ + exit 1; \ + fi && \ + php "$NCC_DIR/INSTALL" --auto && \ + cd .. && rm -rf ncc + +# ----------------------------- Project Build --------------------------------- +# Set build directory and copy pre-needed project files +WORKDIR /tmp/build +COPY . . + +RUN ncc build --config release --build-source --log-level debug && \ + ncc package install --package=build/release/net.nosial.socialbox.ncc --build-source -y --log-level=debug + +# Clean up +RUN rm -rf /tmp/build && rm -rf /var/www/html/* + +# Copy over the required files +COPY nginx.conf /etc/nginx/nginx.conf +COPY public/index.php /var/www/html/index.php +RUN chown -R www-data:www-data /var/www/html && chmod -R 755 /var/www/html + +# ----------------------------- Cron Configuration --------------------------- +RUN echo "*/1 * * * * root for i in {1..12}; do /usr/bin/socialbox process-outgoing; sleep 5; done" > /etc/cron.d/socialbox-process-outgoing && \ + echo "*/1 * * * * root /usr/bin/socialbox session-cleanup" > /etc/cron.d/socialbox-session-cleanup && \ + echo "*/5 * * * * root /usr/bin/socialbox peer-cleanup" > /etc/cron.d/socialbox-peer-cleanup && \ + \ + chmod 0644 /etc/cron.d/socialbox-process-outgoing && \ + chmod 0644 /etc/cron.d/socialbox-session-cleanup && \ + chmod 0644 /etc/cron.d/socialbox-peer-cleanup && \ + \ + crontab /etc/cron.d/socialbox-process-outgoing && \ + crontab /etc/cron.d/socialbox-session-cleanup && \ + crontab /etc/cron.d/socialbox-peer-cleanup + +# ----------------------------- Supervisor Configuration --------------------- +# Copy Supervisor configuration +COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf + +# ----------------------------- Cleanup --------------------- +WORKDIR / + +# ----------------------------- Port Exposing --------------------------------- +EXPOSE 8085 + +# ----------------------------- Container Startup ---------------------------- +# Copy over entrypoint script and set it as executable +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +# Set the entrypoint +ENTRYPOINT ["/usr/bin/bash", "/usr/local/bin/entrypoint.sh"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b67ee76 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,98 @@ +services: + socialbox: + container_name: socialbox + build: + context: . + dockerfile: Dockerfile + ports: + - "8085:8085" + depends_on: + mariadb: + condition: service_healthy + redis: + condition: service_healthy + networks: + - internal_network + restart: unless-stopped + volumes: + - ./socialbox/config:/etc/config + - ./socialbox/logs:/var/log + - ./socialbox/data:/etc/socialbox + environment: + # No need to change these values + LOG_LEVEL: ${LOG_LEVEL:-debug} + CONFIGLIB_PATH: /etc/config + LOGGING_DIRECTORY: /var/log + SB_MODE: automated + SB_STORAGE_PATH: /etc/socialbox + # Change these values to match your environment or update the .env file + SB_INSTANCE_DOMAIN: ${SB_DOMAIN:-localhost} + SB_INSTANCE_RPC_ENDPOINT: ${SB_RPC_ENDPOINT:-http://127.0.0.0:8085/} + SB_DATABASE_HOST: mariadb + SB_DATABASE_USERNAME: ${MYSQL_USER:-socialbox} + SB_DATABASE_PASSWORD: ${MYSQL_PASSWORD:-socialbox} + SB_DATABASE_NAME: ${MYSQL_DATABASE:-socialbox} + SB_CACHE_ENGINE: redis + SB_CACHE_HOST: redis + SB_CACHE_PASSWORD: ${REDIS_PASSWORD:-root} + healthcheck: + test: ["CMD", "curl", "-f", "-H", "Request-Type: ping", "${SB_INSTANCE_DOMAIN-http://127.0.0.0:8085/}"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + mariadb: + container_name: socialbox_mariadb + image: mariadb:10.5 + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-sb_root} + MYSQL_DATABASE: ${MYSQL_DATABASE:-socialbox} + MYSQL_USER: ${MYSQL_USER:-socialbox} + MYSQL_PASSWORD: ${MYSQL_PASSWORD:-socialbox} + volumes: + - mariadb_data:/var/lib/mysql + networks: + - internal_network + expose: + - "3306" + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "mariadb", "-u", "${MYSQL_USER:-socialbox}", "-p${MYSQL_PASSWORD:-socialbox}"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 30s + + redis: + container_name: socialbox_redis + image: redis:alpine + restart: unless-stopped + command: redis-server /usr/local/etc/redis/redis.conf --appendonly yes + volumes: + - redis_data:/data + - ./redis.conf:/usr/local/etc/redis/redis.conf + networks: + - internal_network + environment: + REDIS_PASSWORD: ${REDIS_PASSWORD:-root} + REDIS_DB: 0 + expose: + - "6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 5s + +volumes: + mariadb_data: + driver: local + redis_data: + driver: local + +networks: + internal_network: + driver: bridge + name: socialbox_network \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..22a954b --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Banner with cool ASCII art +echo "███████╗ ██████╗ ██████╗██╗ █████╗ ██╗ ██████╗ ██████╗ ██╗ ██╗" +echo "██╔════╝██╔═══██╗██╔════╝██║██╔══██╗██║ ██╔══██╗██╔═══██╗╚██╗██╔╝" +echo "███████╗██║ ██║██║ ██║███████║██║ ██████╔╝██║ ██║ ╚███╔╝ " +echo "╚════██║██║ ██║██║ ██║██╔══██║██║ ██╔══██╗██║ ██║ ██╔██╗ " +echo "███████║╚██████╔╝╚██████╗██║██║ ██║███████╗██████╔╝╚██████╔╝██╔╝ ██╗" +echo "╚══════╝ ╚═════╝ ╚═════╝╚═╝╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚═╝ ╚═╝" + +# Check if the environment variable SB_MODE is set to "automated", if not exit. +if [ "$SB_MODE" != "automated" ]; then + echo "SB_MODE is not set to 'automated', exiting..." + exit 1 +fi + +# Initialize Socialbox +echo "Initializing Socialbox..." +/usr/bin/socialbox init --log-level=${LOG_LEVEL-INFO} + +# Run supervisord, final command +echo "Starting supervisord..." +/usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf \ No newline at end of file diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..9909244 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,37 @@ +http { + include mime.types; + default_type application/octet-stream; + + server { + listen 8085; + server_name localhost; + + access_log /var/log/access.log; + error_log /var/log/error.log; + + root /var/www/html; + index index.php; + + # Handle all requests + location / { + try_files $uri $uri/ /index.php?$query_string =503; + autoindex off; + } + + location ~ \.php$ { + include fastcgi_params; + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + } + + # Block any .ht* files + location ~ /\.ht { + deny all; + } + } +} + +events { + worker_connections 1024; +} \ No newline at end of file diff --git a/project.json b/project.json index 985bc07..19c0bbc 100644 --- a/project.json +++ b/project.json @@ -93,7 +93,7 @@ "execute": { "working_directory": "%CWD%", "silent": false, - "tty": true, + "tty": false, "timeout": null, "idle_timeout": null, "target": "main" diff --git a/examples/index.php b/public/index.php similarity index 100% rename from examples/index.php rename to public/index.php diff --git a/redis.conf b/redis.conf new file mode 100644 index 0000000..6192ab8 --- /dev/null +++ b/redis.conf @@ -0,0 +1,4 @@ +bind 0.0.0.0 +protected-mode yes +port 6379 +appendonly yes diff --git a/src/Socialbox/Classes/CliCommands/InitializeCommand.php b/src/Socialbox/Classes/CliCommands/InitializeCommand.php index aa3c145..906679d 100644 --- a/src/Socialbox/Classes/CliCommands/InitializeCommand.php +++ b/src/Socialbox/Classes/CliCommands/InitializeCommand.php @@ -3,6 +3,7 @@ namespace Socialbox\Classes\CliCommands; use Exception; + use LogLib\Log; use PDOException; use Socialbox\Abstracts\CacheLayer; use Socialbox\Classes\Configuration; @@ -23,7 +24,7 @@ */ public static function execute(array $args): int { - if(Configuration::getInstanceConfiguration()->isEnabled() === false && !isset($args['force'])) + if(Configuration::getInstanceConfiguration()->isEnabled() === false && !isset($args['force']) && getenv('SB_MODE') !== 'automated') { $required_configurations = [ 'database.host', 'database.port', 'database.username', 'database.password', 'database.name', @@ -47,9 +48,170 @@ Logger::getLogger()->info(' configlib --conf socialbox -e nano'); Logger::getLogger()->info('Or manually at:'); Logger::getLogger()->info(sprintf(' %s', Configuration::getConfigurationLib()->getPath())); + Logger::getLogger()->info('Automated Setup Procedure is done using environment variables:'); + Logger::getLogger()->info(' - SB_MODE=automated'); + Logger::getLogger()->info(' - SB_INSTANCE_DOMAIN=example.com => The Domain Name'); + Logger::getLogger()->info(' - SB_INSTANCE_RPC_ENDPOINT=http://localhost => The RPC Endpoint, must be publicly accessible'); + Logger::getLogger()->info(' - SB_DATABASE_HOST=localhost => The MySQL Host'); + Logger::getLogger()->info(' - SB_DATABASE_PORT=3306 => The MySQL Port'); + Logger::getLogger()->info(' - SB_DATABASE_USER=root => The MySQL Username'); + Logger::getLogger()->info(' - SB_DATABASE_PASSWORD=pass => The MySQL Password'); + Logger::getLogger()->info(' - SB_DATABASE_DATABASE=socialbox => The MySQL Database'); + Logger::getLogger()->info(' - SB_CACHE_ENGINE=redis => The Cache engine to use, supports redis, memcached or null'); + Logger::getLogger()->info(' - SB_CACHE_HOST=localhost => The Cache Host'); + Logger::getLogger()->info(' - SB_CACHE_PORT=6379 => The Cache Port'); + Logger::getLogger()->info(' - SB_CACHE_PASSWORD=pass => The Cache Password'); + Logger::getLogger()->info(' - SB_CACHE_DATABASE=0 => The Cache Database'); + Logger::getLogger()->info(' - SB_STORAGE_PATH=/etc/socialbox => The Storage Path'); + Logger::getLogger()->info('Anything omitted will be null or empty in the configuration'); + return 1; } + // Overwrite the configuration if the automated setup procedure is detected + // This is useful for CI/CD pipelines & Docker + if(getenv('SB_MODE') === 'automated') + { + Logger::getLogger()->info('Automated Setup Procedure is detected'); + + if(getenv('SB_INSTANCE_DOMAIN') !== false) + { + Configuration::getConfigurationLib()->set('instance.domain', getenv('SB_INSTANCE_DOMAIN')); + Logger::getLogger()->info('Set instance.domain to ' . getenv('SB_INSTANCE_DOMAIN')); + } + else + { + Logger::getLogger()->warning('instance.domain is required but was not set, expected SB_INSTANCE_DOMAIN environment variable'); + } + + if(getenv('SB_INSTANCE_RPC_ENDPOINT') !== false) + { + Configuration::getConfigurationLib()->set('instance.rpc_endpoint', getenv('SB_INSTANCE_RPC_ENDPOINT')); + Logger::getLogger()->info('Set instance.rpc_endpoint to ' . getenv('SB_INSTANCE_RPC_ENDPOINT')); + } + else + { + Logger::getLogger()->warning('instance.rpc_endpoint is required but was not set, expected SB_INSTANCE_RPC_ENDPOINT environment variable'); + Configuration::getConfigurationLib()->set('instance.rpc_endpoint', 'http://127.0.0.0/'); + Logger::getLogger()->info('Set instance.rpc_endpoint to http://127.0.0.0/'); + } + + if(getenv('SB_STORAGE_PATH') !== false) + { + Configuration::getConfigurationLib()->set('storage.path', getenv('SB_STORAGE_PATH')); + Logger::getLogger()->info('Set storage.path to ' . getenv('SB_STORAGE_PATH')); + } + else + { + Configuration::getConfigurationLib()->set('storage.path', '/etc/socialbox'); + Logger::getLogger()->info('storage.path was not set, defaulting to /etc/socialbox'); + } + + if(getenv('SB_DATABASE_HOST') !== false) + { + Configuration::getConfigurationLib()->set('database.host', getenv('SB_DATABASE_HOST')); + Logger::getLogger()->info('Set database.host to ' . getenv('SB_DATABASE_HOST')); + } + else + { + Logger::getLogger()->warning('database.host is required but was not set, expected SB_DATABASE_HOST environment variable'); + } + + if(getenv('SB_DATABASE_PORT') !== false) + { + Configuration::getConfigurationLib()->set('database.port', getenv('SB_DATABASE_PORT')); + Logger::getLogger()->info('Set database.port to ' . getenv('SB_DATABASE_PORT')); + } + + if(getenv('SB_DATABASE_USERNAME') !== false) + { + Configuration::getConfigurationLib()->set('database.username', getenv('SB_DATABASE_USERNAME')); + Logger::getLogger()->info('Set database.username to ' . getenv('SB_DATABASE_USERNAME')); + } + else + { + Logger::getLogger()->warning('database.username is required but was not set, expected SB_DATABASE_USERNAME environment variable'); + } + + if(getenv('SB_DATABASE_PASSWORD') !== false) + { + Configuration::getConfigurationLib()->set('database.password', getenv('SB_DATABASE_PASSWORD')); + Logger::getLogger()->info('Set database.password to ' . getenv('SB_DATABASE_PASSWORD')); + } + else + { + Logger::getLogger()->warning('database.password is required but was not set, expected SB_DATABASE_PASSWORD environment variable'); + } + + if(getenv('SB_DATABASE_NAME') !== false) + { + Configuration::getConfigurationLib()->set('database.name', getenv('SB_DATABASE_NAME')); + Logger::getLogger()->info('Set database.name to ' . getenv('SB_DATABASE_NAME')); + } + else + { + Logger::getLogger()->warning('database.name is required but was not set, expected SB_DATABASE_NAME environment variable'); + } + + if(getenv('SB_CACHE_ENABLED') !== false) + { + Configuration::getConfigurationLib()->set('cache.enabled', true); + Logger::getLogger()->info('Set cache.engine to true'); + } + else + { + Configuration::getConfigurationLib()->set('cache.enabled', false); + Logger::getLogger()->info('cache.engine is was not set, defaulting to false'); + } + + + if(getenv('SB_CACHE_ENGINE') !== false) + { + Configuration::getConfigurationLib()->set('cache.engine', getenv('SB_CACHE_ENGINE')); + Logger::getLogger()->info('Set cache.engine to ' . getenv('SB_CACHE_ENGINE')); + } + + if(getenv('SB_CACHE_HOST') !== false) + { + Configuration::getConfigurationLib()->set('cache.host', getenv('SB_CACHE_HOST')); + Logger::getLogger()->info('Set cache.host to ' . getenv('SB_CACHE_HOST')); + } + elseif(Configuration::getCacheConfiguration()->isEnabled()) + { + Logger::getLogger()->warning('cache.host is required but was not set, expected SB_CACHE_HOST environment variable'); + } + + if(getenv('SB_CACHE_PORT') !== false) + { + Configuration::getConfigurationLib()->set('cache.port', getenv('SB_CACHE_PORT')); + Logger::getLogger()->info('Set cache.port to ' . getenv('SB_CACHE_PORT')); + } + + if(getenv('SB_CACHE_PASSWORD') !== false) + { + Configuration::getConfigurationLib()->set('cache.password', getenv('SB_CACHE_PASSWORD')); + Logger::getLogger()->info('Set cache.password to ' . getenv('SB_CACHE_PASSWORD')); + } + elseif(Configuration::getCacheConfiguration()->isEnabled()) + { + Logger::getLogger()->warning('cache.password is required but was not set, expected SB_CACHE_PASSWORD environment variable'); + } + + if(getenv('SB_CACHE_DATABASE') !== false) + { + Configuration::getConfigurationLib()->set('cache.database', getenv('SB_CACHE_DATABASE')); + Logger::getLogger()->info('Set cache.database to ' . getenv('SB_CACHE_DATABASE')); + } + elseif(Configuration::getCacheConfiguration()->isEnabled()) + { + Configuration::getConfigurationLib()->set('cache.database', 0); + Logger::getLogger()->info('cache.database defaulting to 0'); + } + + Configuration::getConfigurationLib()->save(); // Save + Configuration::reload(); // Reload + } + if(Configuration::getInstanceConfiguration()->getDomain() === null) { Logger::getLogger()->error('instance.domain is required but was not set'); @@ -140,14 +302,24 @@ catch (CryptographyException $e) { Logger::getLogger()->error('Failed to generate encryption records due to a cryptography exception', $e); + return 1; } catch (DatabaseOperationException $e) { Logger::getLogger()->error('Failed to generate encryption records due to a database error', $e); + return 1; } // TODO: Create a host peer here? Logger::getLogger()->info('Socialbox has been initialized successfully'); + if(getenv('SB_MODE') === 'automated') + { + Configuration::getConfigurationLib()->set('instance.enabled', true); + Configuration::getConfigurationLib()->save(); // Save + + Logger::getLogger()->info('Automated Setup Procedure is complete, requests to the RPC server ' . Configuration::getInstanceConfiguration()->getRpcEndpoint() . ' are now accepted'); + } + return 0; } diff --git a/src/Socialbox/Classes/Configuration/DatabaseConfiguration.php b/src/Socialbox/Classes/Configuration/DatabaseConfiguration.php index 1093f68..05ff654 100644 --- a/src/Socialbox/Classes/Configuration/DatabaseConfiguration.php +++ b/src/Socialbox/Classes/Configuration/DatabaseConfiguration.php @@ -80,4 +80,15 @@ { return $this->name; } + + + /** + * Constructs and retrieves the Data Source Name (DSN) string. + * + * @return string The DSN string for the database connection. + */ + public function getDsn(): string + { + return sprintf('mysql:host=%s;dbname=%s;port=%s;charset=utf8mb4', $this->host, $this->name, $this->port); + } } \ No newline at end of file diff --git a/src/Socialbox/Classes/Database.php b/src/Socialbox/Classes/Database.php index 68b3955..93bc800 100644 --- a/src/Socialbox/Classes/Database.php +++ b/src/Socialbox/Classes/Database.php @@ -1,39 +1,41 @@ setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - self::$instance->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); - } - catch (PDOException $e) + /** + * @return PDO + * @throws DatabaseOperationException + */ + public static function getConnection(): PDO + { + if (self::$instance === null) { - throw new DatabaseOperationException('Failed to connect to the database', $e); + $dsn = Configuration::getDatabaseConfiguration()->getDsn(); + + try + { + self::$instance = new PDO($dsn, Configuration::getDatabaseConfiguration()->getUsername(), Configuration::getDatabaseConfiguration()->getPassword()); + + // Set some common PDO attributes for better error handling + self::$instance->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + self::$instance->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to connect to the database using ' . $dsn, $e); + } } + + return self::$instance; } - return self::$instance; - } - -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/Socialbox/Classes/Resources/database/authentication_passwords.sql b/src/Socialbox/Classes/Resources/database/authentication_passwords.sql new file mode 100644 index 0000000..7d91842 --- /dev/null +++ b/src/Socialbox/Classes/Resources/database/authentication_passwords.sql @@ -0,0 +1,20 @@ +create table authentication_passwords +( + peer_uuid varchar(36) not null comment 'The Universal Unique Identifier for the peer that is associated with this password record' + primary key comment 'The primary unique index of the peer uuid', + iv mediumtext not null comment 'The Initial Vector of the password record', + encrypted_password mediumtext not null comment 'The encrypted password data', + encrypted_tag mediumtext not null comment 'The encrypted tag of the password record', + updated timestamp default current_timestamp() not null comment 'The Timestamp for when this record was last updated', + constraint authentication_passwords_peer_uuid_uindex + unique (peer_uuid) comment 'The primary unique index of the peer uuid', + constraint authentication_passwords_registered_peers_uuid_fk + foreign key (peer_uuid) references registered_peers (uuid) + on update cascade on delete cascade +) + comment 'Table for housing authentication passwords for registered peers'; + +create index authentication_passwords_updated_index + on authentication_passwords (updated) + comment 'The index of the of the updated column of the record'; + diff --git a/src/Socialbox/Classes/Resources/database/captcha_images.sql b/src/Socialbox/Classes/Resources/database/captcha_images.sql new file mode 100644 index 0000000..03336da --- /dev/null +++ b/src/Socialbox/Classes/Resources/database/captcha_images.sql @@ -0,0 +1,24 @@ +create table captcha_images +( + uuid varchar(36) default uuid() not null comment 'The Unique Universal Identifier of the captcha record' + primary key comment 'The Unique index for the UUID column', + peer_uuid varchar(36) not null comment 'The UUID of the peer that is associated with this captcha challenge', + status enum ('UNSOLVED', 'SOLVED') default 'UNSOLVED' not null comment 'The status of the current captcha', + answer varchar(8) null comment 'The current answer for the captcha', + answered timestamp null comment 'The Timestamp for when this captcha was answered', + created timestamp default current_timestamp() not null comment 'The Timestamp for when this captcha record was created', + constraint captchas_peer_uuid_uindex + unique (peer_uuid) comment 'The Primary Unique Index for the peer UUID', + constraint captchas_registered_peers_uuid_fk + foreign key (peer_uuid) references registered_peers (uuid) + on update cascade on delete cascade +); + +create index captchas_status_index + on captcha_images (status) + comment 'The Index for the captcha status'; + +create index captchas_uuid_index + on captcha_images (uuid) + comment 'The Unique index for the UUID column'; + diff --git a/src/Socialbox/Classes/Resources/database/encryption_records.sql b/src/Socialbox/Classes/Resources/database/encryption_records.sql new file mode 100644 index 0000000..77c75bf --- /dev/null +++ b/src/Socialbox/Classes/Resources/database/encryption_records.sql @@ -0,0 +1,8 @@ +create table encryption_records +( + data mediumtext not null comment 'The data column', + iv mediumtext not null comment 'The initialization vector column', + tag mediumtext not null comment 'The authentication tag used to verify if the data was tampered' +) + comment 'Table for housing encryption records for the server'; + diff --git a/src/Socialbox/Classes/Resources/database/external_sessions.sql b/src/Socialbox/Classes/Resources/database/external_sessions.sql new file mode 100644 index 0000000..96b198f --- /dev/null +++ b/src/Socialbox/Classes/Resources/database/external_sessions.sql @@ -0,0 +1,35 @@ +create table external_sessions +( + uuid varchar(36) default uuid() not null comment 'The UUID of the session for the external connection' + primary key comment 'The Unique Primary Index for the session UUID', + peer_uuid varchar(36) not null comment 'The peer UUID that opened the connection', + session_uuid varchar(36) null comment 'The UUID of the parent session responsible for this external session', + server varchar(255) null comment 'The domain of the remote server that ths external session is authorized for', + created timestamp default current_timestamp() not null comment 'The Timestamp for when this record was created', + last_used timestamp default current_timestamp() not null comment 'The Timestamp for when this session was last used', + constraint external_sessions_uuid_uindex + unique (uuid) comment 'The Unique Primary Index for the session UUID', + constraint external_sessions_registered_peers_uuid_fk + foreign key (peer_uuid) references registered_peers (uuid) + on update cascade on delete cascade, + constraint external_sessions_sessions_uuid_fk + foreign key (session_uuid) references sessions (uuid) +) + comment 'Table for housing external sessions from local to remote servers'; + +create index external_sessions_created_index + on external_sessions (created) + comment 'The Index for the created column'; + +create index external_sessions_last_used_index + on external_sessions (last_used) + comment 'The inex for the last used column'; + +create index external_sessions_peer_uuid_index + on external_sessions (peer_uuid) + comment 'The Index for the peer UUID'; + +create index external_sessions_session_uuid_index + on external_sessions (session_uuid) + comment 'The index for the original session uuid'; + diff --git a/src/Socialbox/Classes/Resources/database/password_authentication.sql b/src/Socialbox/Classes/Resources/database/password_authentication.sql deleted file mode 100644 index 90bc79a..0000000 --- a/src/Socialbox/Classes/Resources/database/password_authentication.sql +++ /dev/null @@ -1,18 +0,0 @@ -create table password_authentication -( - peer_uuid varchar(36) not null comment 'The Primary unique Index for the peer UUID' - primary key, - value varchar(128) not null comment 'The hash value of the pasword', - updated timestamp default current_timestamp() not null comment 'The Timestamp for when this record was last updated', - constraint password_authentication_peer_uuid_uindex - unique (peer_uuid) comment 'The Primary unique Index for the peer UUID', - constraint password_authentication_registered_peers_uuid_fk - foreign key (peer_uuid) references registered_peers (uuid) - on update cascade on delete cascade -) - comment 'Table for housing password authentications associated with peers'; - -create index password_authentication_updated_index - on password_authentication (updated) - comment 'The Indefor the updated timestamp'; - diff --git a/src/Socialbox/Classes/Resources/database/registered_peers.sql b/src/Socialbox/Classes/Resources/database/registered_peers.sql index 01f9229..d0486b0 100644 --- a/src/Socialbox/Classes/Resources/database/registered_peers.sql +++ b/src/Socialbox/Classes/Resources/database/registered_peers.sql @@ -1,20 +1,39 @@ create table registered_peers ( - uuid varchar(36) default uuid() not null comment 'The Primary index for the peer uuid' + uuid varchar(36) default uuid() not null comment 'The Primary index for the peer uuid' primary key, - username varchar(255) not null comment 'The Unique username associated with the peer', - flags text null comment 'Comma seprted flags associated with the peer', - registered timestamp default current_timestamp() not null comment 'The Timestamp for when the peer was registered on the network', - constraint registered_peers_pk_2 - unique (username) comment 'The unique username for the peer', - constraint registered_peers_username_uindex - unique (username) comment 'The unique username for the peer', + username varchar(255) not null comment 'The Unique username associated with the peer', + server varchar(255) default 'host' not null comment 'The server that this peer is registered to', + display_name varchar(255) null comment 'Optional. The Non-Unique Display name of the peer', + display_picture varchar(36) null comment 'The UUID of the display picture that is used, null if none is set.', + flags text null comment 'Comma seprted flags associated with the peer', + enabled tinyint(1) default 0 not null comment 'Boolean column to indicate if this account is Enabled, by default it''s Disabled until the account is verified.', + updated timestamp default current_timestamp() not null comment 'The Timestamp for when this record was last updated', + created timestamp default current_timestamp() not null comment 'The Timestamp for when the peer was registered on the network', + constraint registered_peers_server_username_uindex + unique (server, username) comment 'The Unique Username + Server Index Pair', constraint registered_peers_uuid_uindex unique (uuid) comment 'The Primary index for the peer uuid' ) comment 'Table for housing registered peers under this network'; +create index registered_peers_enabled_index + on registered_peers (enabled) + comment 'The index of the enabled column for registered peers'; + create index registered_peers_registered_index on registered_peers (created) comment 'The Index for the reigstered column of the peer'; +create index registered_peers_server_index + on registered_peers (server) + comment 'The Index for the peer''s server'; + +create index registered_peers_updated_index + on registered_peers (updated) + comment 'The Index for the update column'; + +create index registered_peers_username_index + on registered_peers (username) + comment 'The index for the registered username'; + diff --git a/src/Socialbox/Classes/Resources/database/resolved_servers.sql b/src/Socialbox/Classes/Resources/database/resolved_servers.sql new file mode 100644 index 0000000..0aa906f --- /dev/null +++ b/src/Socialbox/Classes/Resources/database/resolved_servers.sql @@ -0,0 +1,12 @@ +create table resolved_servers +( + domain varchar(512) not null comment 'The domain name' + primary key comment 'Unique Index for the server domain', + endpoint text not null comment 'The endpoint of the RPC server', + public_key text not null comment 'The Public Key of the server', + updated timestamp default current_timestamp() not null comment 'The TImestamp for when this record was last updated', + constraint resolved_servers_domain_uindex + unique (domain) comment 'Unique Index for the server domain' +) + comment 'A table for housing DNS resolutions'; + diff --git a/src/Socialbox/Classes/Resources/database/sessions.sql b/src/Socialbox/Classes/Resources/database/sessions.sql index 6c247b4..d24aa40 100644 --- a/src/Socialbox/Classes/Resources/database/sessions.sql +++ b/src/Socialbox/Classes/Resources/database/sessions.sql @@ -1,21 +1,26 @@ create table sessions ( - uuid varchar(36) default uuid() not null comment 'The Unique Primary index for the session UUID' + uuid varchar(36) default uuid() not null comment 'The Unique Primary index for the session UUID' primary key, - authenticated_peer_uuid varchar(36) null comment 'The peer the session is authenticated as, null if the session isn''t authenticated', - public_key blob not null comment 'The client''s public key provided when creating the session', - state enum ('ACTIVE', 'EXPIRED', 'CLOSED') default 'ACTIVE' not null comment 'The status of the session', - created timestamp default current_timestamp() not null comment 'The Timestamp for when the session was last created', - last_request timestamp null comment 'The Timestamp for when the last request was made using this session', + peer_uuid varchar(36) not null comment 'The peer the session is identified as, null if the session isn''t identified as a peer', + client_name varchar(256) not null comment 'The name of the client that is using this session', + client_version varchar(16) not null comment 'The version of the client', + authenticated tinyint(1) default 0 not null comment 'Indicates if the session is currently authenticated as the peer', + public_key text not null comment 'The client''s public key provided when creating the session', + state enum ('AWAITING_DHE', 'ACTIVE', 'CLOSED', 'EXPIRED') default 'AWAITING_DHE' not null comment 'The status of the session', + encryption_key text null comment 'The key used for encryption that is obtained through a DHE', + flags text null comment 'The current flags that is set to the session', + created timestamp default current_timestamp() not null comment 'The Timestamp for when the session was last created', + last_request timestamp null comment 'The Timestamp for when the last request was made using this session', constraint sessions_uuid_uindex unique (uuid) comment 'The Unique Primary index for the session UUID', constraint sessions_registered_peers_uuid_fk - foreign key (authenticated_peer_uuid) references registered_peers (uuid) + foreign key (peer_uuid) references registered_peers (uuid) on update cascade on delete cascade ); create index sessions_authenticated_peer_index - on sessions (authenticated_peer_uuid) + on sessions (peer_uuid) comment 'The Index for the authenticated peer column'; create index sessions_created_index diff --git a/src/Socialbox/Classes/Resources/database/variables.sql b/src/Socialbox/Classes/Resources/database/variables.sql index 29b865d..48dbd01 100644 --- a/src/Socialbox/Classes/Resources/database/variables.sql +++ b/src/Socialbox/Classes/Resources/database/variables.sql @@ -1,11 +1,12 @@ 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', + name varchar(255) not null comment 'The unique index for the variable name' + primary key, + 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' -); \ No newline at end of file +); + diff --git a/src/Socialbox/Enums/DatabaseObjects.php b/src/Socialbox/Enums/DatabaseObjects.php index d0037e3..95700af 100644 --- a/src/Socialbox/Enums/DatabaseObjects.php +++ b/src/Socialbox/Enums/DatabaseObjects.php @@ -1,38 +1,44 @@ 0, - self::REGISTERED_PEERS => 1, - self::PASSWORD_AUTHENTICATION, self::SESSIONS => 2, - }; - } + return match ($this) + { + self::VARIABLES, self::ENCRYPTION_RECORDS, self::RESOLVED_SERVERS => 0, + self::REGISTERED_PEERS => 1, + self::AUTHENTICATION_PASSWORDS, self::CAPTCHA_IMAGES, self::SESSIONS, self::EXTERNAL_SESSIONS => 2, + }; + } - /** - * Returns an array of cases ordered by their priority. - * - * @return array The array of cases sorted by their priority. - */ - public static function casesOrdered(): array - { - $cases = self::cases(); - usort($cases, fn($a, $b) => $a->getPriority() <=> $b->getPriority()); - return $cases; + /** + * Returns an array of cases ordered by their priority. + * + * @return array The array of cases sorted by their priority. + */ + public static function casesOrdered(): array + { + $cases = self::cases(); + usort($cases, fn($a, $b) => $a->getPriority() <=> $b->getPriority()); + return $cases; + } } -} diff --git a/supervisord.conf b/supervisord.conf new file mode 100644 index 0000000..b46bbce --- /dev/null +++ b/supervisord.conf @@ -0,0 +1,47 @@ +[supervisord] +logfile=/var/logd.log +logfile_maxbytes=50MB +logfile_backups=10 +loglevel=info +user=root +pidfile=/var/run/supervisord.pid +umask=022 +nodaemon=true +minfds=1024 +minprocs=200 + +[program:php-fpm] +command=/usr/local/sbin/php-fpm --nodaemonize +autostart=true +autorestart=true +priority=20 +stdout_logfile=/var/log/fpm.log +stderr_logfile=/var/log/fpm_error.log +stdout_logfile_maxbytes=20MB +stdout_logfile_backups=5 +stderr_logfile_maxbytes=20MB +stderr_logfile_backups=5 + +[program:nginx] +command=/usr/sbin/nginx -g "daemon off;" -c /etc/nginx/nginx.conf +autostart=true +autorestart=true +priority=10 +stdout_logfile=/var/log/nginx.log +stderr_logfile=/var/log/nginx_error.log +stdout_logfile_maxbytes=20MB +stdout_logfile_backups=5 +stderr_logfile_maxbytes=20MB +stderr_logfile_backups=5 + +[program:cron] +command=cron -f -L 15 +autostart=true +autorestart=true +priority=30 +stdout_logfile=/var/log/cron.log +stderr_logfile=/var/log/cron_error.log +stdout_logfile_maxbytes=20MB +stdout_logfile_backups=5 +stderr_logfile_maxbytes=20MB +stderr_logfile_backups=5 \ No newline at end of file From 26b41904c706d8964bf8386c6d1407c5b8b91cdd Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 30 Dec 2024 15:11:35 -0500 Subject: [PATCH 077/420] Updated docker-compose & added docker-compose test --- docker-compose.test.yml | 192 ++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 2 +- 2 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 docker-compose.test.yml diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 0000000..ad9a48f --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,192 @@ +services: + alice_socialbox: + container_name: alice_socialbox + build: + context: . + dockerfile: Dockerfile + ports: + - "8087:8085" + depends_on: + alice_mariadb: + condition: service_healthy + alice_redis: + condition: service_healthy + networks: + - alice_network + restart: unless-stopped + volumes: + - ./alice_socialbox/config:/etc/config + - ./alice_socialbox/logs:/var/log + - ./alice_socialbox/data:/etc/socialbox + environment: + # No need to change these values + LOG_LEVEL: ${LOG_LEVEL:-debug} + CONFIGLIB_PATH: /etc/config + LOGGING_DIRECTORY: /var/log + SB_MODE: automated + SB_STORAGE_PATH: /etc/socialbox + # Updated environment variables for Alice + SB_INSTANCE_DOMAIN: ${SB_ALICE_DOMAIN:-localhost} + SB_INSTANCE_RPC_ENDPOINT: ${SB_ALICE_RPC_ENDPOINT:-http://127.0.0.1:8087/} + SB_DATABASE_HOST: alice_mariadb + SB_DATABASE_USERNAME: ${MYSQL_USER:-socialbox} + SB_DATABASE_PASSWORD: ${MYSQL_PASSWORD:-socialbox} + SB_DATABASE_NAME: ${MYSQL_DATABASE:-socialbox} + SB_CACHE_ENGINE: redis + SB_CACHE_HOST: alice_redis + SB_CACHE_PASSWORD: ${REDIS_PASSWORD:-root} + healthcheck: + test: ["CMD", "curl", "-f", "-H", "Request-Type: ping", "${SB_INSTANCE_RPC_ENDPOINT-http://127.0.0.0:8085/}"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + bob_socialbox: + container_name: bob_socialbox + build: + context: . + dockerfile: Dockerfile + ports: + - "8086:8085" + depends_on: + bob_mariadb: + condition: service_healthy + bob_redis: + condition: service_healthy + networks: + - bob_network + restart: unless-stopped + volumes: + - ./bob_socialbox/config:/etc/config + - ./bob_socialbox/logs:/var/log + - ./bob_socialbox/data:/etc/socialbox + environment: + # No need to change these values + LOG_LEVEL: ${LOG_LEVEL:-debug} + CONFIGLIB_PATH: /etc/config + LOGGING_DIRECTORY: /var/log + SB_MODE: automated + SB_STORAGE_PATH: /etc/socialbox + # Updated environment variables for Bob + SB_INSTANCE_DOMAIN: ${SB_BOB_DOMAIN:-localhost} + SB_INSTANCE_RPC_ENDPOINT: ${SB_BOB_RPC_ENDPOINT:-http://127.0.0.1:8086/} + SB_DATABASE_HOST: bob_mariadb + SB_DATABASE_USERNAME: ${MYSQL_USER:-socialbox} + SB_DATABASE_PASSWORD: ${MYSQL_PASSWORD:-socialbox} + SB_DATABASE_NAME: ${MYSQL_DATABASE:-socialbox} + SB_CACHE_ENGINE: redis + SB_CACHE_HOST: bob_redis + SB_CACHE_PASSWORD: ${REDIS_PASSWORD:-root} + healthcheck: + test: ["CMD", "curl", "-f", "-H", "Request-Type: ping", "${SB_INSTANCE_RPC_ENDPOINT-http://127.0.0.0:8085/}"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + alice_mariadb: + container_name: alice_mariadb + image: mariadb:10.5 + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-sb_root} + MYSQL_DATABASE: ${MYSQL_DATABASE:-socialbox} + MYSQL_USER: ${MYSQL_USER:-socialbox} + MYSQL_PASSWORD: ${MYSQL_PASSWORD:-socialbox} + volumes: + - alice_mariadb_data:/var/lib/mysql + networks: + - alice_network + expose: + - "3306" + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "alice_mariadb", "-u", "${MYSQL_USER:-socialbox}", "-p${MYSQL_PASSWORD:-socialbox}"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 30s + + bob_mariadb: + container_name: bob_mariadb + image: mariadb:10.5 + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-sb_root} + MYSQL_DATABASE: ${MYSQL_DATABASE:-socialbox} + MYSQL_USER: ${MYSQL_USER:-socialbox} + MYSQL_PASSWORD: ${MYSQL_PASSWORD:-socialbox} + volumes: + - bob_mariadb_data:/var/lib/mysql + networks: + - bob_network + expose: + - "3306" + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "bob_mariadb", "-u", "${MYSQL_USER:-socialbox}", "-p${MYSQL_PASSWORD:-socialbox}"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 30s + + alice_redis: + container_name: alice_redis + image: redis:alpine + restart: unless-stopped + command: redis-server /usr/local/etc/redis/redis.conf --appendonly yes + volumes: + - alice_redis_data:/data + - ./redis.conf:/usr/local/etc/redis/redis.conf + networks: + - alice_network + environment: + REDIS_PASSWORD: ${REDIS_PASSWORD:-root} + REDIS_DB: 0 + expose: + - "6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 5s + + bob_redis: + container_name: bob_redis + image: redis:alpine + restart: unless-stopped + command: redis-server /usr/local/etc/redis/redis.conf --appendonly yes + volumes: + - bob_redis_data:/data + - ./redis.conf:/usr/local/etc/redis/redis.conf + networks: + - bob_network + environment: + REDIS_PASSWORD: ${REDIS_PASSWORD:-root} + REDIS_DB: 0 + expose: + - "6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 5s + +volumes: + alice_mariadb_data: + driver: local + bob_mariadb_data: + driver: local + alice_redis_data: + driver: local + bob_redis_data: + driver: local + +networks: + alice_network: + driver: bridge + name: alice_network + bob_network: + driver: bridge + name: bob_network \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index b67ee76..e139f75 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,7 +36,7 @@ services: SB_CACHE_HOST: redis SB_CACHE_PASSWORD: ${REDIS_PASSWORD:-root} healthcheck: - test: ["CMD", "curl", "-f", "-H", "Request-Type: ping", "${SB_INSTANCE_DOMAIN-http://127.0.0.0:8085/}"] + test: ["CMD", "curl", "-f", "-H", "Request-Type: ping", "${SB_INSTANCE_RPC_ENDPOINT-http://127.0.0.0:8085/}"] interval: 30s timeout: 10s retries: 3 From 46ad03a54ddf4495deb8f16d47d714a941ad34ed Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 30 Dec 2024 15:12:23 -0500 Subject: [PATCH 078/420] Updated .gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e53be25..a056964 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ /build /.idea/gbrowser_project.xml .phpunit.result.cache -/socialbox \ No newline at end of file +/socialbox +/bob_socialbox +/alice_socialbox \ No newline at end of file From 367399f0fda032d6a1a61560a8a297290ccf2b96 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 3 Jan 2025 12:27:04 -0500 Subject: [PATCH 079/420] Changed Encryption to use LibSodium instead of OpenSSL, refactored many things and overall improved the code quality and performance with magic. --- .idea/socialbox-php.iml | 2 +- .idea/sqldialects.xml | 4 +- composer.json | 3 +- .../Classes/CliCommands/DnsRecordCommand.php | 67 +- .../Classes/CliCommands/InitializeCommand.php | 65 +- .../Classes/ClientCommands/ConnectCommand.php | 117 -- src/Socialbox/Classes/Configuration.php | 69 +- .../CryptographyConfiguration.php | 111 ++ .../Configuration/InstanceConfiguration.php | 75 +- .../StorageConfiguration.php | 2 +- src/Socialbox/Classes/Cryptography.php | 1013 +++++++++++------ src/Socialbox/Classes/DnsHelper.php | 41 + .../database/authentication_passwords.sql | 8 +- .../Resources/database/encryption_records.sql | 8 - .../database/resolved_dns_records.sql | 19 + .../Resources/database/resolved_servers.sql | 12 - .../Classes/Resources/database/sessions.sql | 27 +- src/Socialbox/Classes/RpcClient.php | 306 ++++- src/Socialbox/Classes/SecuredPassword.php | 96 -- src/Socialbox/Classes/ServerResolver.php | 59 +- .../SettingsSetDisplayPicture.php | 4 +- src/Socialbox/Classes/Utilities.php | 43 +- src/Socialbox/Enums/DatabaseObjects.php | 5 +- src/Socialbox/Enums/StandardError.php | 15 +- src/Socialbox/Enums/StandardHeaders.php | 4 +- src/Socialbox/Enums/Types/RequestType.php | 5 + .../Managers/EncryptionRecordsManager.php | 205 ---- src/Socialbox/Managers/PasswordManager.php | 159 ++- .../Managers/ResolvedDnsRecordsManager.php | 184 +++ .../Managers/ResolvedServersManager.php | 152 --- src/Socialbox/Managers/SessionManager.php | 110 +- src/Socialbox/Objects/ClientRequest.php | 139 ++- .../Objects/Database/DecryptedRecord.php | 41 - .../Objects/Database/EncryptionRecord.php | 83 -- .../Objects/Database/ResolvedServerRecord.php | 218 ++-- .../Objects/Database/SecurePasswordRecord.php | 108 -- .../Objects/Database/SessionRecord.php | 98 +- src/Socialbox/Objects/DnsRecord.php | 67 ++ src/Socialbox/Objects/ExportedSession.php | 242 ++-- src/Socialbox/Objects/ResolvedServer.php | 26 +- src/Socialbox/Objects/ResolvedServerOld.php | 25 + .../Objects/Standard/ServerInformation.php | 75 ++ src/Socialbox/Socialbox.php | 436 ++++--- tests/Socialbox/Classes/CryptographyTest.php | 439 +++++++ 44 files changed, 2971 insertions(+), 2016 deletions(-) delete mode 100644 src/Socialbox/Classes/ClientCommands/ConnectCommand.php create mode 100644 src/Socialbox/Classes/Configuration/CryptographyConfiguration.php rename src/Socialbox/Classes/{ClientCommands => Configuration}/StorageConfiguration.php (96%) create mode 100644 src/Socialbox/Classes/DnsHelper.php delete mode 100644 src/Socialbox/Classes/Resources/database/encryption_records.sql create mode 100644 src/Socialbox/Classes/Resources/database/resolved_dns_records.sql delete mode 100644 src/Socialbox/Classes/Resources/database/resolved_servers.sql delete mode 100644 src/Socialbox/Classes/SecuredPassword.php delete mode 100644 src/Socialbox/Managers/EncryptionRecordsManager.php create mode 100644 src/Socialbox/Managers/ResolvedDnsRecordsManager.php delete mode 100644 src/Socialbox/Managers/ResolvedServersManager.php delete mode 100644 src/Socialbox/Objects/Database/DecryptedRecord.php delete mode 100644 src/Socialbox/Objects/Database/EncryptionRecord.php delete mode 100644 src/Socialbox/Objects/Database/SecurePasswordRecord.php create mode 100644 src/Socialbox/Objects/DnsRecord.php create mode 100644 src/Socialbox/Objects/ResolvedServerOld.php create mode 100644 src/Socialbox/Objects/Standard/ServerInformation.php create mode 100644 tests/Socialbox/Classes/CryptographyTest.php diff --git a/.idea/socialbox-php.iml b/.idea/socialbox-php.iml index c5da166..11e3fb9 100644 --- a/.idea/socialbox-php.iml +++ b/.idea/socialbox-php.iml @@ -2,7 +2,7 @@ - + diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml index bb99828..db6a2e3 100644 --- a/.idea/sqldialects.xml +++ b/.idea/sqldialects.xml @@ -2,14 +2,14 @@ + - - + diff --git a/composer.json b/composer.json index 2386ce4..fc11b64 100644 --- a/composer.json +++ b/composer.json @@ -15,6 +15,7 @@ "ext-redis": "*", "ext-memcached": "*", "ext-curl": "*", - "ext-gd": "*" + "ext-gd": "*", + "ext-sodium": "*" } } \ No newline at end of file diff --git a/src/Socialbox/Classes/CliCommands/DnsRecordCommand.php b/src/Socialbox/Classes/CliCommands/DnsRecordCommand.php index a066af6..28ee6d5 100644 --- a/src/Socialbox/Classes/CliCommands/DnsRecordCommand.php +++ b/src/Socialbox/Classes/CliCommands/DnsRecordCommand.php @@ -1,46 +1,45 @@ getRpcEndpoint(), - Configuration::getInstanceConfiguration()->getPublicKey() - ); + /** + * @inheritDoc + */ + public static function execute(array $args): int + { + $txt_record = sprintf('v=socialbox;sb-rpc=%s;sb-key=%s', + Configuration::getInstanceConfiguration()->getRpcEndpoint(), + Configuration::getCryptographyConfiguration()->getHostPublicKey() + ); - Logger::getLogger()->info('Please set the following DNS TXT record for the domain:'); - Logger::getLogger()->info(sprintf(' %s', $txt_record)); - return 0; - } + Logger::getLogger()->info('Please set the following DNS TXT record for the domain:'); + Logger::getLogger()->info(sprintf(' %s', $txt_record)); + return 0; + } - /** - * @inheritDoc - */ - public static function getHelpMessage(): string - { - return <<info('cache.database defaulting to 0'); } + Logger::getLogger()->info('Updating configuration...'); Configuration::getConfigurationLib()->save(); // Save Configuration::reload(); // Reload } @@ -261,16 +260,17 @@ } if( - !Configuration::getInstanceConfiguration()->getPublicKey() || - !Configuration::getInstanceConfiguration()->getPrivateKey() || - !Configuration::getInstanceConfiguration()->getEncryptionKeys() + !Configuration::getCryptographyConfiguration()->getHostPublicKey() || + !Configuration::getCryptographyConfiguration()->getHostPrivateKey() || + !Configuration::getCryptographyConfiguration()->getHostPublicKey() ) { + $expires = time() + 31536000; + try { - Logger::getLogger()->info('Generating new key pair...'); - $keyPair = Cryptography::generateKeyPair(); - $encryptionKeys = Cryptography::randomKeyS(230, 314, Configuration::getInstanceConfiguration()->getEncryptionKeysCount()); + Logger::getLogger()->info('Generating new key pair (expires ' . date('Y-m-d H:i:s', $expires) . ')...'); + $signingKeyPair = Cryptography::generateSigningKeyPair(); } catch (CryptographyException $e) { @@ -278,40 +278,35 @@ return 1; } - Logger::getLogger()->info('Updating configuration...'); - Configuration::getConfigurationLib()->set('instance.private_key', $keyPair->getPrivateKey()); - Configuration::getConfigurationLib()->set('instance.public_key', $keyPair->getPublicKey()); - Configuration::getConfigurationLib()->set('instance.encryption_keys', $encryptionKeys); - Configuration::getConfigurationLib()->save(); // Save - Configuration::reload(); // Reload - - Logger::getLogger()->info(sprintf('Set the DNS TXT record for the domain %s to the following value:', Configuration::getInstanceConfiguration()->getDomain())); - Logger::getLogger()->info(sprintf("v=socialbox;sb-rpc=%s;sb-key=%s;", - Configuration::getInstanceConfiguration()->getRpcEndpoint(), $keyPair->getPublicKey() - )); + Configuration::getConfigurationLib()->set('cryptography.host_keypair_expires', $expires); + Configuration::getConfigurationLib()->set('cryptography.host_private_key', $signingKeyPair->getPrivateKey()); + Configuration::getConfigurationLib()->set('cryptography.host_public_key', $signingKeyPair->getPublicKey()); } - try + // If Internal Encryption keys are null or has less keys than configured, populate the configuration + // property with encryption keys. + if( + Configuration::getCryptographyConfiguration()->getInternalEncryptionKeys() === null || + count(Configuration::getCryptographyConfiguration()->getInternalEncryptionKeys()) < Configuration::getCryptographyConfiguration()->getEncryptionKeysCount()) { - if(EncryptionRecordsManager::getRecordCount() < Configuration::getInstanceConfiguration()->getEncryptionRecordsCount()) + Logger::getLogger()->info('Generating internal encryption keys...'); + $encryptionKeys = Configuration::getCryptographyConfiguration()->getInternalEncryptionKeys() ?? []; + while(count($encryptionKeys) < Configuration::getCryptographyConfiguration()->getEncryptionKeysCount()) { - Logger::getLogger()->info('Generating encryption records...'); - EncryptionRecordsManager::generateRecords(Configuration::getInstanceConfiguration()->getEncryptionRecordsCount()); + $encryptionKeys[] = Cryptography::generateEncryptionKey(Configuration::getCryptographyConfiguration()->getEncryptionKeysAlgorithm()); } - } - catch (CryptographyException $e) - { - Logger::getLogger()->error('Failed to generate encryption records due to a cryptography exception', $e); - return 1; - } - catch (DatabaseOperationException $e) - { - Logger::getLogger()->error('Failed to generate encryption records due to a database error', $e); - return 1; + + Configuration::getConfigurationLib()->set('cryptography.internal_encryption_keys', $encryptionKeys); } - // TODO: Create a host peer here? + Logger::getLogger()->info('Updating configuration...'); + Configuration::getConfigurationLib()->save();; + Configuration::reload(); + Logger::getLogger()->info('Socialbox has been initialized successfully'); + Logger::getLogger()->info(sprintf('Set the DNS TXT record for the domain %s to the following value:', Configuration::getInstanceConfiguration()->getDomain())); + Logger::getLogger()->info(Socialbox::getDnsRecord()); + if(getenv('SB_MODE') === 'automated') { Configuration::getConfigurationLib()->set('instance.enabled', true); diff --git a/src/Socialbox/Classes/ClientCommands/ConnectCommand.php b/src/Socialbox/Classes/ClientCommands/ConnectCommand.php deleted file mode 100644 index e2229b8..0000000 --- a/src/Socialbox/Classes/ClientCommands/ConnectCommand.php +++ /dev/null @@ -1,117 +0,0 @@ -error('The name argument is required, this is the name of the session'); - } - - $workingDirectory = getcwd(); - - if(isset($args['directory'])) - { - if(!is_dir($args['directory'])) - { - Logger::getLogger()->error('The directory provided does not exist'); - return 1; - } - - $workingDirectory = $args['directory']; - } - - $sessionFile = $workingDirectory . DIRECTORY_SEPARATOR . Utilities::sanitizeFileName($args['name']) . '.json'; - - if(!file_exists($sessionFile)) - { - return self::createSession($args, $sessionFile); - } - - Logger::getLogger()->info(sprintf('Session file already exists at %s', $sessionFile)); - return 0; - } - - private static function createSession(array $args, string $sessionFile): int - { - if(!isset($args['domain'])) - { - Logger::getLogger()->error('The domain argument is required, this is the domain of the socialbox instance'); - return 1; - } - - try - { - $client = new SocialClient($args['domain']); - } - catch (DatabaseOperationException $e) - { - Logger::getLogger()->error('Failed to create the client session', $e); - return 1; - } - catch (ResolutionException $e) - { - Logger::getLogger()->error('Failed to resolve the domain', $e); - return 1; - } - - try - { - $keyPair = Cryptography::generateKeyPair(); - $session = $client->createSession($keyPair); - } - catch (CryptographyException | RpcException $e) - { - Logger::getLogger()->error('Failed to create the session', $e); - return 1; - } - - $sessionData = new ClientSession([ - 'domain' => $args['domain'], - 'session_uuid' => $session, - 'public_key' => $keyPair->getPublicKey(), - 'private_key' => $keyPair->getPrivateKey() - ]); - - $sessionData->save($sessionFile); - Logger::getLogger()->info(sprintf('Session created and saved to %s', $sessionFile)); - return 0; - } - - public static function getHelpMessage(): string - { - return << --domain [--directory ] - -Creates a new session with the specified name and domain. The session will be saved to the current working directory by default, or to the specified directory if provided. - -Options: - --name The name of the session to create. - --domain The domain of the socialbox instance. - --directory The directory where the session file should be saved. - -Example: - socialbox connect --name mysession --domain socialbox.example.com -HELP; - - } - - public static function getShortHelpMessage(): string - { - return 'Connect Command - Creates a new session with the specified name and domain'; - } -} \ No newline at end of file diff --git a/src/Socialbox/Classes/Configuration.php b/src/Socialbox/Classes/Configuration.php index b1aa90b..09fd208 100644 --- a/src/Socialbox/Classes/Configuration.php +++ b/src/Socialbox/Classes/Configuration.php @@ -2,19 +2,21 @@ namespace Socialbox\Classes; - use Socialbox\Classes\ClientCommands\StorageConfiguration; use Socialbox\Classes\Configuration\CacheConfiguration; + use Socialbox\Classes\Configuration\CryptographyConfiguration; use Socialbox\Classes\Configuration\DatabaseConfiguration; use Socialbox\Classes\Configuration\InstanceConfiguration; use Socialbox\Classes\Configuration\LoggingConfiguration; use Socialbox\Classes\Configuration\RegistrationConfiguration; use Socialbox\Classes\Configuration\SecurityConfiguration; + use Socialbox\Classes\Configuration\StorageConfiguration; class Configuration { private static ?\ConfigLib\Configuration $configuration = null; private static ?InstanceConfiguration $instanceConfiguration = null; private static ?SecurityConfiguration $securityConfiguration = null; + private static ?CryptographyConfiguration $cryptographyConfiguration = null; private static ?DatabaseConfiguration $databaseConfiguration = null; private static ?LoggingConfiguration $loggingConfiguration = null; private static ?CacheConfiguration $cacheConfiguration = null; @@ -33,19 +35,47 @@ // Instance configuration $config->setDefault('instance.enabled', false); // False by default, requires the user to enable it. + $config->setDefault('instance.name', "Socialbox Server"); $config->setDefault('instance.domain', null); $config->setDefault('instance.rpc_endpoint', null); - $config->setDefault('instance.encryption_keys_count', 5); - $config->setDefault('instance.encryption_records_count', 5); - $config->setDefault('instance.private_key', null); - $config->setDefault('instance.public_key', null); - $config->setDefault('instance.encryption_keys', null); // Security Configuration $config->setDefault('security.display_internal_exceptions', false); $config->setDefault('security.resolved_servers_ttl', 600); $config->setDefault('security.captcha_ttl', 200); + // Cryptography Configuration + // The Unix Timestamp for when the host's keypair should expire + // Setting this value to 0 means the keypair never expires + // Setting this value to null will automatically set the current unix timestamp + 1 year as the value + // This means at initialization, the key is automatically set to expire in a year. + $config->setDefault('cryptography.host_keypair_expires', null); + // The host's public/private keypair in base64 encoding, when null; the initialization process + // will automatically generate a new keypair + $config->setDefault('cryptography.host_public_key', null); + $config->setDefault('cryptography.host_private_key', null); + + // The internal encryption keys used for encrypting data in the database when needed. + // When null, the initialization process will automatically generate a set of keys + // based on the `encryption_keys_count` and `encryption_keys_algorithm` configuration. + // This is an array of base64 encoded keys. + $config->setDefault('cryptography.internal_encryption_keys', null); + + // The number of encryption keys to generate and set to `instance.encryption_keys` this will be used + // to randomly encrypt/decrypt sensitive data in the database, this includes hashes. + // The higher the number the higher performance impact it will have on the server + $config->setDefault('cryptography.encryption_keys_count', 10); + // The host's encryption algorithm, this will be used to generate a set of encryption keys + // This is for internal encryption, these keys are never shared outside this configuration. + // Recommendation: Higher security over performance + $config->setDefault('cryptography.encryption_keys_algorithm', 'xchacha20'); + + // The encryption algorithm to use for encrypted message transport between the client aand the server + // This is the encryption the server tells the client to use and the client must support it. + // Recommendation: Good balance between security and performance + // For universal support & performance, use aes256gcm for best performance or for best security use xchacha20 + $config->setDefault('cryptography.transport_encryption_algorithm', 'chacha20'); + // Database configuration $config->setDefault('database.host', '127.0.0.1'); $config->setDefault('database.port', 3306); @@ -98,6 +128,7 @@ self::$configuration = $config; self::$instanceConfiguration = new InstanceConfiguration(self::$configuration->getConfiguration()['instance']); self::$securityConfiguration = new SecurityConfiguration(self::$configuration->getConfiguration()['security']); + self::$cryptographyConfiguration = new CryptographyConfiguration(self::$configuration->getConfiguration()['cryptography']); self::$databaseConfiguration = new DatabaseConfiguration(self::$configuration->getConfiguration()['database']); self::$loggingConfiguration = new LoggingConfiguration(self::$configuration->getConfiguration()['logging']); self::$cacheConfiguration = new CacheConfiguration(self::$configuration->getConfiguration()['cache']); @@ -140,6 +171,14 @@ return self::$configuration->getConfiguration(); } + /** + * Retrieves the configuration library instance. + * + * This method returns the current Configuration instance from the ConfigLib namespace. + * If the configuration has not been initialized yet, it initializes it first. + * + * @return \ConfigLib\Configuration The configuration library instance. + */ public static function getConfigurationLib(): \ConfigLib\Configuration { if(self::$configuration === null) @@ -180,6 +219,24 @@ return self::$securityConfiguration; } + /** + * Retrieves the cryptography configuration. + * + * This method returns the current CryptographyConfiguration instance. + * If the configuration has not been initialized yet, it initializes it first. + * + * @return CryptographyConfiguration|null The cryptography configuration instance or null if not available. + */ + public static function getCryptographyConfiguration(): ?CryptographyConfiguration + { + if(self::$cryptographyConfiguration === null) + { + self::initializeConfiguration(); + } + + return self::$cryptographyConfiguration; + } + /** * Retrieves the current database configuration. * diff --git a/src/Socialbox/Classes/Configuration/CryptographyConfiguration.php b/src/Socialbox/Classes/Configuration/CryptographyConfiguration.php new file mode 100644 index 0000000..34c8567 --- /dev/null +++ b/src/Socialbox/Classes/Configuration/CryptographyConfiguration.php @@ -0,0 +1,111 @@ +hostKeyPairExpires = $data['host_keypair_expires'] ?? null; + $this->hostPublicKey = $data['host_public_key'] ?? null; + $this->hostPrivateKey = $data['host_private_key'] ?? null; + $this->internalEncryptionKeys = $data['internal_encryption_keys'] ?? null; + $this->encryptionKeysCount = $data['encryption_keys_count']; + $this->encryptionKeysAlgorithm = $data['encryption_keys_algorithm']; + $this->transportEncryptionAlgorithm = $data['transport_encryption_algorithm']; + } + + /** + * Retrieves the expiration timestamp of the host key pair. + * + * @return int|null The expiration timestamp of the host key pair, or null if not set. + */ + public function getHostKeyPairExpires(): ?int + { + return $this->hostKeyPairExpires; + } + + /** + * Retrieves the host's public key. + * + * @return string|null The host's public key, or null if not set. + */ + public function getHostPublicKey(): ?string + { + return $this->hostPublicKey; + } + + /** + * Retrieves the private key associated with the host. + * + * @return string|null The host's private key, or null if not set. + */ + public function getHostPrivateKey(): ?string + { + return $this->hostPrivateKey; + } + + /** + * Retrieves the internal encryption keys. + * + * @return array|null Returns an array of internal encryption keys if set, or null if no keys are available. + */ + public function getInternalEncryptionKeys(): ?array + { + return $this->internalEncryptionKeys; + } + + /** + * Retrieves a random internal encryption key from the available set of encryption keys. + * + * @return string|null Returns a randomly selected encryption key as a string, or null if no keys are available. + */ + public function getRandomInternalEncryptionKey(): ?string + { + return $this->internalEncryptionKeys[array_rand($this->internalEncryptionKeys)]; + } + + /** + * Retrieves the total count of encryption keys. + * + * @return int The number of encryption keys. + */ + public function getEncryptionKeysCount(): int + { + return $this->encryptionKeysCount; + } + + /** + * Retrieves the algorithm used for the encryption keys. + * + * @return string The encryption keys algorithm. + */ + public function getEncryptionKeysAlgorithm(): string + { + return $this->encryptionKeysAlgorithm; + } + + /** + * Retrieves the transport encryption algorithm being used. + * + * @return string The transport encryption algorithm. + */ + public function getTransportEncryptionAlgorithm(): string + { + return $this->transportEncryptionAlgorithm; + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/Configuration/InstanceConfiguration.php b/src/Socialbox/Classes/Configuration/InstanceConfiguration.php index 7d8b3d2..431b9c4 100644 --- a/src/Socialbox/Classes/Configuration/InstanceConfiguration.php +++ b/src/Socialbox/Classes/Configuration/InstanceConfiguration.php @@ -5,13 +5,9 @@ class InstanceConfiguration { private bool $enabled; + private string $name; private ?string $domain; private ?string $rpcEndpoint; - private int $encryptionKeysCount; - private int $encryptionRecordsCount; - private ?string $privateKey; - private ?string $publicKey; - private ?array $encryptionKeys; /** * Constructor that initializes object properties with the provided data. @@ -22,13 +18,9 @@ public function __construct(array $data) { $this->enabled = (bool)$data['enabled']; + $this->name = $data['name']; $this->domain = $data['domain']; $this->rpcEndpoint = $data['rpc_endpoint']; - $this->encryptionKeysCount = $data['encryption_keys_count']; - $this->encryptionRecordsCount = $data['encryption_records_count']; - $this->privateKey = $data['private_key']; - $this->publicKey = $data['public_key']; - $this->encryptionKeys = $data['encryption_keys']; } /** @@ -41,6 +33,11 @@ return $this->enabled; } + public function getName(): string + { + return $this->name; + } + /** * Retrieves the domain. * @@ -58,62 +55,4 @@ { return $this->rpcEndpoint; } - - /** - * Retrieves the number of encryption keys. - * - * @return int The number of encryption keys. - */ - public function getEncryptionKeysCount(): int - { - return $this->encryptionKeysCount; - } - - /** - * Retrieves the number of encryption records. - * - * @return int The number of encryption records. - */ - public function getEncryptionRecordsCount(): int - { - return $this->encryptionRecordsCount; - } - - /** - * Retrieves the private key. - * - * @return string|null The private key. - */ - public function getPrivateKey(): ?string - { - return $this->privateKey; - } - - /** - * Retrieves the public key. - * - * @return string|null The public key. - */ - public function getPublicKey(): ?string - { - return $this->publicKey; - } - - /** - * Retrieves the encryption keys. - * - * @return array|null The encryption keys. - */ - public function getEncryptionKeys(): ?array - { - return $this->encryptionKeys; - } - - /** - * @return string - */ - public function getRandomEncryptionKey(): string - { - return $this->encryptionKeys[array_rand($this->encryptionKeys)]; - } } \ No newline at end of file diff --git a/src/Socialbox/Classes/ClientCommands/StorageConfiguration.php b/src/Socialbox/Classes/Configuration/StorageConfiguration.php similarity index 96% rename from src/Socialbox/Classes/ClientCommands/StorageConfiguration.php rename to src/Socialbox/Classes/Configuration/StorageConfiguration.php index 0050418..639af4d 100644 --- a/src/Socialbox/Classes/ClientCommands/StorageConfiguration.php +++ b/src/Socialbox/Classes/Configuration/StorageConfiguration.php @@ -1,6 +1,6 @@ self::ALGORITHM, - "private_key_bits" => self::KEY_SIZE, - ]; + private const KEY_TYPE_ENCRYPTION = 'enc:'; + private const KEY_TYPE_SIGNING = 'sig:'; + private const BASE64_VARIANT = SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING; - $res = openssl_pkey_new($config); - if (!$res) + /** + * Generates a new encryption key pair consisting of a public key and a secret key. + * The generated keys are encoded in a specific format and securely handled in memory. + * + * @return KeyPair Returns an instance of KeyPair containing the encoded public and secret keys. + * @throws CryptographyException If key pair generation fails. + */ + public static function generateEncryptionKeyPair(): KeyPair { - throw new CryptographyException('Failed to generate private key: ' . openssl_error_string()); - } - - openssl_pkey_export($res, $privateKeyPem); - $publicKeyPem = openssl_pkey_get_details($res)['key']; - - return new KeyPair( - Utilities::base64encode(self::pemToDer($publicKeyPem)), - Utilities::base64encode(self::pemToDer($privateKeyPem)) - ); - } - - /** - * Converts a PEM formatted key to DER format. - * - * @param string $pemKey The PEM formatted key as a string. - * - * @return string The DER formatted key as a binary string. - */ - private static function pemToDer(string $pemKey): string - { - $pemKey = preg_replace('/-----(BEGIN|END) [A-Z ]+-----/', '', $pemKey); - return Utilities::base64decode(str_replace(["\n", "\r", " "], '', $pemKey)); - } - - /** - * Converts a DER formatted key to PEM format. - * - * @param string $derKey The DER formatted key. - * @param string $type The type of key, either private or public. Default is private. - * @return string The PEM formatted key. - */ - private static function derToPem(string $derKey, string $type): string - { - $formattedKey = chunk_split(Utilities::base64encode($derKey), 64); - $headerFooter = strtoupper($type) === self::PEM_PUBLIC_HEADER - ? "PUBLIC KEY" : "PRIVATE KEY"; - - return "-----BEGIN $headerFooter-----\n$formattedKey-----END $headerFooter-----\n"; - } - - /** - * Signs the given content using the provided private key. - * - * @param string $content The content to be signed. - * @param string $privateKey The private key used to sign the content. - * @param bool $hashContent Whether to hash the content using SHA1 before signing it. Default is false. - * @return string The Base64 encoded signature of the content. - * @throws CryptographyException If the private key is invalid or if the content signing fails. - */ - public static function signContent(string $content, string $privateKey, bool $hashContent=false): string - { - $privateKey = openssl_pkey_get_private(self::derToPem(Utilities::base64decode($privateKey), self::PEM_PRIVATE_HEADER)); - if (!$privateKey) - { - throw new CryptographyException('Invalid private key: ' . openssl_error_string()); - } - - if($hashContent) - { - $content = hash('sha1', $content); - } - - if (!openssl_sign($content, $signature, $privateKey, self::HASH_ALGORITHM)) - { - throw new CryptographyException('Failed to sign content: ' . openssl_error_string()); - } - - return base64_encode($signature); - } - - /** - * Verifies the integrity of the given content using the provided digital signature and public key. - * - * @param string $content The content to be verified. - * @param string $signature The digital signature to verify against. - * @param string $publicKey The public key to use for verification. - * @param bool $hashContent Whether to hash the content using SHA1 before verifying it. Default is false. - * @return bool Returns true if the content verification is successful, false otherwise. - * @throws CryptographyException If the public key is invalid or if the signature verification fails. - */ - public static function verifyContent(string $content, string $signature, string $publicKey, bool $hashContent=false): bool - { - try - { - $publicKey = openssl_pkey_get_public(self::derToPem(Utilities::base64decode($publicKey), self::PEM_PUBLIC_HEADER)); - } - catch(InvalidArgumentException $e) - { - throw new CryptographyException('Failed to decode public key: ' . $e->getMessage()); - } - - if (!$publicKey) - { - throw new CryptographyException('Invalid public key: ' . openssl_error_string()); - } - - if($hashContent) - { - $content = hash('sha1', $content); - } - - try - { - return openssl_verify($content, Utilities::base64decode($signature), $publicKey, self::HASH_ALGORITHM) === 1; - } - catch(InvalidArgumentException $e) - { - throw new CryptographyException('Failed to verify content: ' . $e->getMessage()); - } - } - - /** - * Temporarily signs the provided content by appending a timestamp-based value and signing it. - * - * @param string $content The content to be signed. - * @param string $privateKey The private key used to sign the content. - * @return string The base64 encoded signature of the content with the appended timestamp. - * @throws CryptographyException If the private key is invalid or if the content signing fails. - */ - public static function temporarySignContent(string $content, string $privateKey): string - { - return self::signContent(sprintf('%s|%d', $content, time() / self::TIME_BLOCK), $privateKey); - } - - /** - * Verify the provided temporary signature for the given content using the public key. - * - * @param string $content The content whose signature needs to be verified. - * @param string $signature The signature associated with the content. - * @param string $publicKey The public key to be used for verifying the signature. - * @param int $frames The number of time frames to consider for validating the signature (default is 1). - * @return bool Returns true if the signature is valid within the provided time frames, otherwise false. - * @throws CryptographyException If the public key is invalid or the signature verification fails. - */ - public static function verifyTemporarySignature(string $content, string $signature, string $publicKey, int $frames = 1): bool - { - $currentTime = time() / self::TIME_BLOCK; - for ($i = 0; $i < max(1, $frames); $i++) - { - if (self::verifyContent(sprintf('%s|%d', $content, $currentTime - $i), $signature, $publicKey)) + try { - return true; + $keyPair = sodium_crypto_box_keypair(); + $publicKey = sodium_crypto_box_publickey($keyPair); + $secretKey = sodium_crypto_box_secretkey($keyPair); + + $result = new KeyPair( + self::KEY_TYPE_ENCRYPTION . sodium_bin2base64($publicKey, self::BASE64_VARIANT), + self::KEY_TYPE_ENCRYPTION . sodium_bin2base64($secretKey, self::BASE64_VARIANT) + ); + + // Clean up sensitive data + sodium_memzero($keyPair); + sodium_memzero($secretKey); + + return $result; + } + catch (Exception $e) + { + throw new CryptographyException("Failed to generate encryption keypair: " . $e->getMessage()); } } - return false; - } - /** - * Encrypts the given content using the provided public key. - * - * @param string $content The content to be encrypted. - * @param string $publicKey The public key used for encryption, in DER-encoded format. - * @return string The encrypted content, encoded in base64 format. - * @throws CryptographyException If the public key is invalid or the encryption fails. - */ - public static function encryptContent(string $content, string $publicKey): string - { - try + /** + * Validates a public encryption key to ensure it is properly formatted and of the correct length. + * + * @param string $publicKey The base64-encoded public key to validate. + * @return bool True if the public key is valid, false otherwise. + */ + public static function validatePublicEncryptionKey(string $publicKey): bool { - $publicKey = openssl_pkey_get_public(self::derToPem(Utilities::base64decode($publicKey), self::PEM_PUBLIC_HEADER)); - } - catch(Exception $e) - { - throw new CryptographyException('Failed to decode public key: ' . $e->getMessage()); + if(!str_starts_with($publicKey, 'enc:')) + { + return false; + } + + $base64Key = substr($publicKey, 4); + + try + { + $decodedKey = sodium_base642bin($base64Key, self::BASE64_VARIANT, true); + + if (strlen($decodedKey) !== SODIUM_CRYPTO_BOX_PUBLICKEYBYTES) + { + return false; + } + + return true; + } + catch (Exception) + { + return false; + } } - if (!$publicKey) + /** + * Generates a new signing key pair consisting of a public key and a secret key. + * + * @return KeyPair An object containing the base64-encoded public and secret keys, each prefixed with the signing key type identifier. + * @throws CryptographyException If the key pair generation process fails. + */ + public static function generateSigningKeyPair(): KeyPair { - throw new CryptographyException('Invalid public key: ' . openssl_error_string()); + try + { + $keyPair = sodium_crypto_sign_keypair(); + $publicKey = sodium_crypto_sign_publickey($keyPair); + $secretKey = sodium_crypto_sign_secretkey($keyPair); + + $result = new KeyPair( + self::KEY_TYPE_SIGNING . sodium_bin2base64($publicKey, self::BASE64_VARIANT), + self::KEY_TYPE_SIGNING . sodium_bin2base64($secretKey, self::BASE64_VARIANT) + ); + + // Clean up sensitive data + sodium_memzero($keyPair); + sodium_memzero($secretKey); + + return $result; + } + catch (Exception $e) + { + throw new CryptographyException("Failed to generate signing keypair: " . $e->getMessage()); + } } - if (!openssl_public_encrypt($content, $encrypted, $publicKey, self::PADDING)) + /** + * Validates a public signing key for proper format and length. + * + * @param string $publicKey The base64-encoded public signing key to be validated. + * @return bool Returns true if the key is valid, or false if it is invalid. + * @throws CryptographyException If the public key is incorrectly formatted or its length is invalid. + */ + public static function validatePublicSigningKey(string $publicKey): bool { - throw new CryptographyException('Failed to encrypt content: ' . openssl_error_string()); + // Check if the key is prefixed with "sig:" + if (!str_starts_with($publicKey, 'sig:')) + { + // If it doesn't start with "sig:", consider it invalid + return false; + } + + // Remove the "sig:" prefix + $base64Key = substr($publicKey, 4); + + try + { + // Decode the base64 key + $decodedKey = sodium_base642bin($base64Key, self::BASE64_VARIANT, true); + + // Validate the length of the decoded key + return strlen($decodedKey) === SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES; + } + catch (Exception) + { + // If decoding fails, consider the key invalid + return false; + } } - try + /** + * Performs a Diffie-Hellman Exchange (DHE) to derive a shared secret key using the provided public and private keys. + * + * @param string $publicKey The base64-encoded public key of the other party. + * @param string $privateKey The base64-encoded private key of the local party. + * @return string The base64-encoded derived shared secret key. + * @throws CryptographyException If the provided keys are invalid or the key exchange process fails. + */ + public static function performDHE(string $publicKey, string $privateKey): string { - return base64_encode($encrypted); - } - catch(Exception $e) - { - throw new CryptographyException('Failed to encode encrypted content: ' . $e->getMessage()); - } - } + try + { + if (empty($publicKey) || empty($privateKey)) + { + throw new CryptographyException("Empty key(s) provided"); + } - /** - * Decrypts the provided content using the specified private key. - * - * @param string $content The content to be decrypted, encoded in base64. - * @param string $privateKey The private key for decryption, encoded in base64. - * @return string The decrypted content, encoded in UTF-8. - * @throws CryptographyException If the private key is invalid or the decryption fails. - */ - public static function decryptContent(string $content, string $privateKey): string - { - $privateKey = openssl_pkey_get_private(self::derToPem(Utilities::base64decode($privateKey), self::PEM_PRIVATE_HEADER)); + $publicKey = self::validateAndExtractKey($publicKey, self::KEY_TYPE_ENCRYPTION); + $privateKey = self::validateAndExtractKey($privateKey, self::KEY_TYPE_ENCRYPTION); - if (!$privateKey) - { - throw new CryptographyException('Invalid private key: ' . openssl_error_string()); + $decodedPublicKey = sodium_base642bin($publicKey, self::BASE64_VARIANT, true); + $decodedPrivateKey = sodium_base642bin($privateKey, self::BASE64_VARIANT, true); + + if (strlen($decodedPublicKey) !== SODIUM_CRYPTO_BOX_PUBLICKEYBYTES) + { + throw new CryptographyException("Invalid public key length"); + } + + if (strlen($decodedPrivateKey) !== SODIUM_CRYPTO_BOX_SECRETKEYBYTES) + { + throw new CryptographyException("Invalid private key length"); + } + + $sharedSecret = sodium_crypto_scalarmult($decodedPrivateKey, $decodedPublicKey); + $derivedKey = sodium_crypto_generichash($sharedSecret, null, SODIUM_CRYPTO_SECRETBOX_KEYBYTES); + $result = sodium_bin2base64($derivedKey, self::BASE64_VARIANT); + + // Clean up sensitive data + sodium_memzero($sharedSecret); + sodium_memzero($derivedKey); + sodium_memzero($decodedPrivateKey); + + return $result; + } + catch (Exception $e) + { + throw new CryptographyException("Failed to perform DHE: " . $e->getMessage()); + } } - if (!openssl_private_decrypt(base64_decode($content), $decrypted, $privateKey, self::PADDING)) + /** + * Encrypts a message using the provided shared secret. + * + * @param string $message The message to be encrypted. + * @param string $sharedSecret The base64-encoded shared secret used for encryption. + * @return string The base64-encoded encrypted message, including a randomly generated nonce. + * @throws CryptographyException If the message or shared secret is invalid or the encryption fails. + */ + public static function encryptShared(string $message, string $sharedSecret): string { - throw new CryptographyException('Failed to decrypt content: ' . openssl_error_string()); + try + { + if (empty($message)) + { + throw new CryptographyException("Empty message provided"); + } + + if (empty($sharedSecret)) + { + throw new CryptographyException("Empty shared secret provided"); + } + + $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); + $key = sodium_base642bin($sharedSecret, self::BASE64_VARIANT, true); + + if (strlen($key) !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES) + { + throw new CryptographyException("Invalid shared secret length"); + } + + $encrypted = sodium_crypto_secretbox($message, $nonce, $key); + $result = sodium_bin2base64($nonce . $encrypted, self::BASE64_VARIANT); + + // Clean up sensitive data + sodium_memzero($key); + + return $result; + } + catch (Exception $e) + { + throw new CryptographyException("Encryption failed: " . $e->getMessage()); + } } - return mb_convert_encoding($decrypted, 'UTF-8', 'auto'); - } + /** + * Decrypts an encrypted message using the provided shared secret. + * + * @param string $encryptedMessage The base64-encoded encrypted message to be decrypted. + * @param string $sharedSecret The base64-encoded shared secret used to decrypt the message. + * @return string The decrypted message. + * @throws CryptographyException If the encrypted message or shared secret is invalid, or the decryption process fails. + */ + public static function decryptShared(string $encryptedMessage, string $sharedSecret): string + { + try + { + if (empty($encryptedMessage)) + { + throw new CryptographyException("Empty encrypted message provided"); + } - public static function validatePublicKey(string $publicKey): bool - { - try - { - $result = openssl_pkey_get_public(self::derToPem(Utilities::base64decode($publicKey), self::PEM_PUBLIC_HEADER)); - } - catch(InvalidArgumentException) - { - return false; + if (empty($sharedSecret)) + { + throw new CryptographyException("Empty shared secret provided"); + } + + $decoded = sodium_base642bin($encryptedMessage, self::BASE64_VARIANT, true); + $key = sodium_base642bin($sharedSecret, self::BASE64_VARIANT, true); + + if (strlen($key) !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES) + { + throw new CryptographyException("Invalid shared secret length"); + } + + if (strlen($decoded) < SODIUM_CRYPTO_SECRETBOX_NONCEBYTES) + { + throw new CryptographyException("Encrypted message too short"); + } + + $nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit'); + $ciphertext = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit'); + + $decrypted = sodium_crypto_secretbox_open($ciphertext, $nonce, $key); + + if ($decrypted === false) + { + throw new CryptographyException("Decryption failed: Invalid message or shared secret"); + } + + sodium_memzero($key); + return $decrypted; + } + catch (Exception $e) + { + throw new CryptographyException("Decryption failed: " . $e->getMessage()); + } } - if($result === false) + /** + * Signs a message using the provided private key. + * + * @param string $message The message to be signed. + * @param string $privateKey The base64-encoded private key used for signing. + * @return string The base64-encoded digital signature. + * @throws CryptographyException If the message or private key is invalid, or if signing fails. + */ + public static function signMessage(string $message, string $privateKey): string { - return false; + try + { + if (empty($message)) + { + throw new CryptographyException("Empty message provided"); + } + + if (empty($privateKey)) + { + throw new CryptographyException("Empty private key provided"); + } + + $privateKey = self::validateAndExtractKey($privateKey, self::KEY_TYPE_SIGNING); + $decodedKey = sodium_base642bin($privateKey, self::BASE64_VARIANT, true); + + if (strlen($decodedKey) !== SODIUM_CRYPTO_SIGN_SECRETKEYBYTES) + { + throw new CryptographyException("Invalid private key length"); + } + + $signature = sodium_crypto_sign_detached($message, $decodedKey); + + sodium_memzero($decodedKey); + return sodium_bin2base64($signature, self::BASE64_VARIANT); + } + catch (Exception $e) + { + throw new CryptographyException("Failed to sign message: " . $e->getMessage()); + } } - return true; - } + /** + * Verifies the validity of a given signature for a message using the provided public key. + * + * @param string $message The original message that was signed. + * @param string $signature The base64-encoded signature to be verified. + * @param string $publicKey The base64-encoded public key used to verify the signature. + * @return bool True if the signature is valid; false otherwise. + * @throws CryptographyException If any parameter is empty, if the public key or signature is invalid, or if the verification process fails. + */ + public static function verifyMessage(string $message, string $signature, string $publicKey): bool + { + try + { + if (empty($message) || empty($signature) || empty($publicKey)) + { + throw new CryptographyException("Empty parameter(s) provided"); + } - public static function validatePrivateKey(string $privateKey): bool - { - try - { - $result = openssl_pkey_get_private(self::derToPem(Utilities::base64decode($privateKey), self::PEM_PRIVATE_HEADER)); - } - catch(InvalidArgumentException) - { - return false; + $publicKey = self::validateAndExtractKey($publicKey, self::KEY_TYPE_SIGNING); + $decodedKey = sodium_base642bin($publicKey, self::BASE64_VARIANT, true); + $decodedSignature = sodium_base642bin($signature, self::BASE64_VARIANT, true); + + if (strlen($decodedKey) !== SODIUM_CRYPTO_SIGN_PUBLICKEYBYTES) + { + throw new CryptographyException("Invalid public key length"); + } + + if (strlen($decodedSignature) !== SODIUM_CRYPTO_SIGN_BYTES) + { + throw new CryptographyException("Invalid signature length"); + } + + return sodium_crypto_sign_verify_detached($decodedSignature, $message, $decodedKey); + } + catch (Exception $e) + { + if($e instanceof CryptographyException) + { + throw $e; + } + + throw new CryptographyException("Failed to verify signature: " . $e->getMessage()); + } } - if($result === false) + /** + * Determines if the provided algorithm is supported. + * + * @param string $algorithm The name of the algorithm to check. + * @return bool True if the algorithm is supported, false otherwise. + */ + public static function isSupportedAlgorithm(string $algorithm): bool { - return false; + return match($algorithm) + { + 'xchacha20', 'chacha20', 'aes256gcm' => true, + default => false + }; } - return true; - } + /** + * Generates a new encryption key for the specified algorithm. + * + * @param string $algorithm The encryption algorithm for which the key is generated. + * Supported values are 'xchacha20', 'chacha20', and 'aes256gcm'. + * @return string The base64-encoded encryption key. + * @throws CryptographyException If the algorithm is unsupported or if key generation fails. + */ + public static function generateEncryptionKey(string $algorithm): string + { + if(!self::isSupportedAlgorithm($algorithm)) + { + throw new CryptographyException('Unsupported Algorithm: ' . $algorithm); + } - /** - * Generates a random sequence of bytes with a length determined between the specified minimum and maximum. - * - * @param int $minLength The minimum length of the generated byte sequence. - * @param int $maxLength The maximum length of the generated byte sequence. - * @return string A hexadecimal string representing the random byte sequence. - * @throws CryptographyException If the random byte generation fails. - */ - public static function randomKey(int $minLength, int $maxLength): string - { - try - { - return bin2hex(random_bytes(random_int($minLength, $maxLength))); - } - catch(RandomException $e) - { - throw new CryptographyException('Failed to generate random bytes: ' . $e->getMessage()); - } - } + try + { + $keygenMethod = match ($algorithm) + { + 'xchacha20' => 'sodium_crypto_aead_xchacha20poly1305_ietf_keygen', + 'chacha20' => 'sodium_crypto_aead_chacha20poly1305_keygen', + 'aes256gcm' => 'sodium_crypto_aead_aes256gcm_keygen', + }; - /** - * Generates an array of random keys, each with a length within the specified range. - * - * @param int $minLength The minimum length for each random key. - * @param int $maxLength The maximum length for each random key. - * @param int $amount The number of random keys to generate. - * @return array An array of randomly generated keys. - * @throws CryptographyException If the random key generation fails. - */ - public static function randomKeys(int $minLength, int $maxLength, int $amount): array - { - $keys = []; - for($i = 0; $i < $amount; $i++) - { - $keys[] = self::randomKey($minLength, $maxLength); + return sodium_bin2base64($keygenMethod(), self::BASE64_VARIANT); + } + catch (Exception $e) + { + if($e instanceof CryptographyException) + { + throw $e; + } + + throw new CryptographyException("Failed to generate encryption key: " . $e->getMessage()); + } } - return $keys; - } + /** + * Validates the provided encryption key against the specified algorithm. + * + * @param string $encryptionKey The encryption key to be validated, encoded in Base64. + * @param string $algorithm The encryption algorithm that the key should match. + * Supported algorithms include 'xchacha20', 'chacha20', and 'aes256gcm'. + * @return bool Returns true if the encryption key is valid for the given algorithm, otherwise false. + */ + public static function validateEncryptionKey(string $encryptionKey, string $algorithm): bool + { + if (empty($encryptionKey)) + { + return false; + } - public static function generateEncryptionKey(): string - { - try - { - return base64_encode(random_bytes(32)); - } - catch (RandomException $e) - { - throw new CryptographyException('Failed to generate encryption key: ' . $e->getMessage()); - } - } + if(!self::isSupportedAlgorithm($algorithm)) + { + return false; + } - /** - * Encrypts the given content for transport using the provided encryption key. - * - * @param string $content The content to be encrypted. - * @param string $encryptionKey The encryption key used for encrypting the content. - * @return string The Base64 encoded string containing the IV and the encrypted content. - * @throws CryptographyException If the IV generation or encryption process fails. - */ - public static function encryptTransport(string $content, string $encryptionKey): string - { - try - { - $iv = random_bytes(openssl_cipher_iv_length('aes-256-cbc')); - } - catch (RandomException $e) - { - throw new CryptographyException('Failed to generate IV: ' . $e->getMessage()); + try + { + $key = sodium_base642bin($encryptionKey, self::BASE64_VARIANT, true); + $keyLength = match ($algorithm) + { + 'xchacha20' => SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES, + 'chacha20' => SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_KEYBYTES, + 'aes256gcm' => SODIUM_CRYPTO_AEAD_AES256GCM_KEYBYTES + }; + + if (strlen($key) !== $keyLength) + { + return false; + } + + return true; + } + catch (Exception) + { + return false; + } + finally + { + if (isset($key)) + { + sodium_memzero($key); + } + } } - $encrypted = openssl_encrypt($content, self::TRANSPORT_ENCRYPTION, base64_decode($encryptionKey), OPENSSL_RAW_DATA, $iv); - - if($encrypted === false) + /** + * Encrypts a message using the specified encryption algorithm and key. + * + * @param string $message The plaintext message to be encrypted. + * @param string $encryptionKey A base64-encoded encryption key. + * @param string $algorithm The name of the encryption algorithm to be used (e.g., 'xchacha20', 'chacha20', 'aes256gcm'). + * @return string The base64-encoded encrypted message including the nonce. + * @throws CryptographyException If the message, encryption key, or algorithm is invalid, or if encryption fails. + */ + public static function encryptMessage(string $message, string $encryptionKey, string $algorithm): string { - throw new CryptographyException('Failed to encrypt transport content: ' . openssl_error_string()); + try + { + if (empty($message)) + { + throw new CryptographyException("Empty message provided"); + } + + if (empty($encryptionKey)) + { + throw new CryptographyException("Empty encryption key provided"); + } + + if(!self::isSupportedAlgorithm($algorithm)) + { + throw new CryptographyException('Unsupported Algorithm: ' . $algorithm); + } + + $key = sodium_base642bin($encryptionKey, self::BASE64_VARIANT, true); + + [$nonceLength, $encryptMethod, $keyLength] = match ($algorithm) + { + 'xchacha20' => [SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES, 'sodium_crypto_aead_xchacha20poly1305_ietf_encrypt', SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES], + 'chacha20' => [SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_NPUBBYTES, 'sodium_crypto_aead_chacha20poly1305_encrypt', SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_KEYBYTES], + 'aes256gcm' => [SODIUM_CRYPTO_AEAD_AES256GCM_NPUBBYTES, 'sodium_crypto_aead_aes256gcm_encrypt', SODIUM_CRYPTO_AEAD_AES256GCM_KEYBYTES], + }; + + if (strlen($key) !== $keyLength) + { + throw new CryptographyException("Invalid encryption key length for $algorithm"); + } + + $nonce = random_bytes($nonceLength); + $encrypted = $encryptMethod($message, '', $nonce, $key); + return sodium_bin2base64($nonce . $encrypted, self::BASE64_VARIANT); + } + catch (Exception $e) + { + if($e instanceof CryptographyException) + { + throw $e; + } + + throw new CryptographyException("Message encryption failed: " . $e->getMessage()); + } + finally + { + if (isset($key)) + { + sodium_memzero($key); + } + } } - return base64_encode($iv . $encrypted); - } - - /** - * Decrypts the given encrypted transport content using the provided encryption key. - * - * @param string $encryptedContent The Base64 encoded encrypted content to be decrypted. - * @param string $encryptionKey The Base64 encoded encryption key used for decryption. - * @return string The decrypted content as a string. - * @throws CryptographyException If the decryption process fails. - */ - public static function decryptTransport(string $encryptedContent, string $encryptionKey): string - { - $decodedData = base64_decode($encryptedContent); - $ivLength = openssl_cipher_iv_length(self::TRANSPORT_ENCRYPTION); - - // Perform decryption - $decryption = openssl_decrypt(substr($decodedData, $ivLength), - self::TRANSPORT_ENCRYPTION, - base64_decode($encryptionKey), - OPENSSL_RAW_DATA, - substr($decodedData, 0, $ivLength) - ); - - if($decryption === false) + /** + * Decrypts an encrypted message using the specified encryption key and algorithm. + * + * @param string $encryptedMessage The base64-encoded encrypted message to be decrypted. + * @param string $encryptionKey The base64-encoded encryption key used for decryption. + * @param string $algorithm The encryption algorithm used to encrypt the message (e.g., 'xchacha20', 'chacha20', 'aes256gcm'). + * @return string The decrypted plaintext message. + * @throws CryptographyException If the encrypted message, encryption key, or algorithm is invalid, or if decryption fails. + */ + public static function decryptMessage(string $encryptedMessage, string $encryptionKey, string $algorithm): string { - throw new CryptographyException('Failed to decrypt transport content: ' . openssl_error_string()); + if (empty($encryptedMessage)) + { + throw new CryptographyException("Empty encrypted message provided"); + } + + if (empty($encryptionKey)) + { + throw new CryptographyException("Empty encryption key provided"); + } + + if(!self::isSupportedAlgorithm($algorithm)) + { + throw new CryptographyException('Unsupported Algorithm: ' . $algorithm); + } + + try + { + + $key = sodium_base642bin($encryptionKey, self::BASE64_VARIANT, true); + [$nonceLength, $decryptMethod, $keyLength] = match ($algorithm) + { + 'xchacha20' => [SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES, 'sodium_crypto_aead_xchacha20poly1305_ietf_decrypt', SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES], + 'chacha20' => [SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_NPUBBYTES, 'sodium_crypto_aead_chacha20poly1305_decrypt', SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_KEYBYTES], + 'aes256gcm' => [SODIUM_CRYPTO_AEAD_AES256GCM_NPUBBYTES, 'sodium_crypto_aead_aes256gcm_decrypt', SODIUM_CRYPTO_AEAD_AES256GCM_KEYBYTES] + }; + + if (strlen($key) !== $keyLength) + { + throw new CryptographyException("Invalid encryption key length for $algorithm"); + } + + $decoded = sodium_base642bin($encryptedMessage, self::BASE64_VARIANT, true); + + if (strlen($decoded) < $nonceLength) + { + throw new CryptographyException("Encrypted message is too short"); + } + + $nonce = mb_substr($decoded, 0, $nonceLength, '8bit'); + $ciphertext = mb_substr($decoded, $nonceLength, null, '8bit'); + $decrypted = $decryptMethod($ciphertext, '', $nonce, $key); + + if ($decrypted === false) + { + throw new CryptographyException("Invalid message or encryption key"); + } + + return $decrypted; + } + catch (Exception $e) + { + if($e instanceof CryptographyException) + { + throw $e; + } + + throw new CryptographyException("Message decryption failed: " . $e->getMessage()); + } + finally + { + if (isset($key)) + { + sodium_memzero($key); + } + } } - return $decryption; - } -} \ No newline at end of file + /** + * Hashes a password securely using a memory-hard, CPU-intensive hashing algorithm. + * + * @param string $password The plaintext password to be hashed. + * @return string The hashed password in a secure format. + * @throws CryptographyException If password hashing fails. + */ + public static function hashPassword(string $password): string + { + try + { + return sodium_crypto_pwhash_str($password, SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE); + } + catch (Exception $e) + { + throw new CryptographyException("Failed to hash password: " . $e->getMessage()); + } + } + + /** + * Validates the given Argon2id hash string based on its format and current security requirements. + * + * @param string $hash The hash string to be validated. + * @return bool Returns true if the hash is valid and meets current security standards. + * @throws CryptographyException If the hash format is invalid or does not meet security requirements. + */ + public static function validatePasswordHash(string $hash): bool + { + try + { + // Step 1: Check the format + $argon2id_pattern = '/^\$argon2id\$v=\d+\$m=\d+,t=\d+,p=\d+\$[A-Za-z0-9+\/=]+\$[A-Za-z0-9+\/=]+$/D'; + if (!preg_match($argon2id_pattern, $hash)) + { + throw new CryptographyException("Invalid hash format"); + } + + // Step 2: Check if it needs rehashing (validates the hash structure) + if (sodium_crypto_pwhash_str_needs_rehash($hash, SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE)) + { + throw new CryptographyException("Hash does not meet current security requirements"); + } + + // If all checks pass, the hash is valid. + return true; + } + catch (Exception $e) + { + throw new CryptographyException("Invalid hash: " . $e->getMessage()); + } + } + + /** + * Verifies a password against a stored hash. + * + * @param string $password The password to be verified. + * @param string $hash The stored password hash to be compared against. + * @return bool True if the password matches the hash; false otherwise. + * @throws CryptographyException If the password verification process fails. + */ + public static function verifyPassword(string $password, string $hash): bool + { + self::validatePasswordHash($hash); + + try + { + return sodium_crypto_pwhash_str_verify($hash, $password); + } + catch (Exception $e) + { + throw new CryptographyException("Failed to verify password: " . $e->getMessage()); + } + } + + /** + * Validates a key by ensuring it is not empty, matches the expected type, and extracts the usable portion. + * + * @param string $key The key to be validated and processed. + * @param string $expectedType The expected prefix type of the key. + * @return string The extracted portion of the key after the expected type. + * @throws CryptographyException If the key is empty, the key type is invalid, or the extracted portion is empty. + */ + private static function validateAndExtractKey(string $key, string $expectedType): string + { + if (empty($key)) + { + throw new CryptographyException("Empty key provided"); + } + + if (!str_starts_with($key, $expectedType)) + { + throw new CryptographyException("Invalid key type. Expected {$expectedType}"); + } + + $extractedKey = substr($key, strlen($expectedType)); + if (empty($extractedKey)) + { + throw new CryptographyException("Empty key after type extraction"); + } + + return $extractedKey; + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/DnsHelper.php b/src/Socialbox/Classes/DnsHelper.php new file mode 100644 index 0000000..a431579 --- /dev/null +++ b/src/Socialbox/Classes/DnsHelper.php @@ -0,0 +1,41 @@ +https?:\/\/[^;]+);sb-key=(?P[^;]+);sb-exp=(?P\d+)/'; + if (preg_match($pattern, $txtRecord, $matches)) + { + return new DnsRecord($matches['rpcEndpoint'], $matches['publicSigningKey'], (int)$matches['expirationTime']); + } + + throw new InvalidArgumentException('Invalid TXT record format.'); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/Resources/database/authentication_passwords.sql b/src/Socialbox/Classes/Resources/database/authentication_passwords.sql index 7d91842..5a7b36c 100644 --- a/src/Socialbox/Classes/Resources/database/authentication_passwords.sql +++ b/src/Socialbox/Classes/Resources/database/authentication_passwords.sql @@ -1,11 +1,9 @@ create table authentication_passwords ( - peer_uuid varchar(36) not null comment 'The Universal Unique Identifier for the peer that is associated with this password record' + peer_uuid varchar(36) not null comment 'The Universal Unique Identifier for the peer that is associated with this password record' primary key comment 'The primary unique index of the peer uuid', - iv mediumtext not null comment 'The Initial Vector of the password record', - encrypted_password mediumtext not null comment 'The encrypted password data', - encrypted_tag mediumtext not null comment 'The encrypted tag of the password record', - updated timestamp default current_timestamp() not null comment 'The Timestamp for when this record was last updated', + hash mediumtext not null comment 'The encrypted hash of the password', + updated timestamp default current_timestamp() not null comment 'The Timestamp for when this record was last updated', constraint authentication_passwords_peer_uuid_uindex unique (peer_uuid) comment 'The primary unique index of the peer uuid', constraint authentication_passwords_registered_peers_uuid_fk diff --git a/src/Socialbox/Classes/Resources/database/encryption_records.sql b/src/Socialbox/Classes/Resources/database/encryption_records.sql deleted file mode 100644 index 77c75bf..0000000 --- a/src/Socialbox/Classes/Resources/database/encryption_records.sql +++ /dev/null @@ -1,8 +0,0 @@ -create table encryption_records -( - data mediumtext not null comment 'The data column', - iv mediumtext not null comment 'The initialization vector column', - tag mediumtext not null comment 'The authentication tag used to verify if the data was tampered' -) - comment 'Table for housing encryption records for the server'; - diff --git a/src/Socialbox/Classes/Resources/database/resolved_dns_records.sql b/src/Socialbox/Classes/Resources/database/resolved_dns_records.sql new file mode 100644 index 0000000..5f4c03f --- /dev/null +++ b/src/Socialbox/Classes/Resources/database/resolved_dns_records.sql @@ -0,0 +1,19 @@ +create table resolved_dns_records +( + domain varchar(512) not null comment 'The domain name' + primary key comment 'Unique Index for the server domain', + rpc_endpoint text not null comment 'The endpoint of the RPC server', + public_key text not null comment 'The Public Key of the server', + expires bigint not null comment 'The Unix Timestamp for when the server''s keypair expires', + updated timestamp default current_timestamp() not null comment 'The Timestamp for when this record was last updated', + constraint resolved_dns_records_domain_uindex + unique (domain) comment 'Unique Index for the server domain', + constraint resolved_dns_records_pk + unique (domain) comment 'Unique Index for the server domain' +) + comment 'A table for housing DNS resolutions'; + +create index resolved_dns_records_updated_index + on resolved_dns_records (updated) + comment 'The index for the updated column'; + diff --git a/src/Socialbox/Classes/Resources/database/resolved_servers.sql b/src/Socialbox/Classes/Resources/database/resolved_servers.sql deleted file mode 100644 index 0aa906f..0000000 --- a/src/Socialbox/Classes/Resources/database/resolved_servers.sql +++ /dev/null @@ -1,12 +0,0 @@ -create table resolved_servers -( - domain varchar(512) not null comment 'The domain name' - primary key comment 'Unique Index for the server domain', - endpoint text not null comment 'The endpoint of the RPC server', - public_key text not null comment 'The Public Key of the server', - updated timestamp default current_timestamp() not null comment 'The TImestamp for when this record was last updated', - constraint resolved_servers_domain_uindex - unique (domain) comment 'Unique Index for the server domain' -) - comment 'A table for housing DNS resolutions'; - diff --git a/src/Socialbox/Classes/Resources/database/sessions.sql b/src/Socialbox/Classes/Resources/database/sessions.sql index d24aa40..ddd0b9e 100644 --- a/src/Socialbox/Classes/Resources/database/sessions.sql +++ b/src/Socialbox/Classes/Resources/database/sessions.sql @@ -1,17 +1,22 @@ create table sessions ( - uuid varchar(36) default uuid() not null comment 'The Unique Primary index for the session UUID' + uuid varchar(36) default uuid() not null comment 'The Unique Primary index for the session UUID' primary key, - peer_uuid varchar(36) not null comment 'The peer the session is identified as, null if the session isn''t identified as a peer', - client_name varchar(256) not null comment 'The name of the client that is using this session', - client_version varchar(16) not null comment 'The version of the client', - authenticated tinyint(1) default 0 not null comment 'Indicates if the session is currently authenticated as the peer', - public_key text not null comment 'The client''s public key provided when creating the session', - state enum ('AWAITING_DHE', 'ACTIVE', 'CLOSED', 'EXPIRED') default 'AWAITING_DHE' not null comment 'The status of the session', - encryption_key text null comment 'The key used for encryption that is obtained through a DHE', - flags text null comment 'The current flags that is set to the session', - created timestamp default current_timestamp() not null comment 'The Timestamp for when the session was last created', - last_request timestamp null comment 'The Timestamp for when the last request was made using this session', + peer_uuid varchar(36) not null comment 'The peer the session is identified as, null if the session isn''t identified as a peer', + client_name varchar(256) not null comment 'The name of the client that is using this session', + client_version varchar(16) not null comment 'The version of the client', + authenticated tinyint(1) default 0 not null comment 'Indicates if the session is currently authenticated as the peer', + client_public_signing_key text not null comment 'The client''s public signing key used for signing decrypted messages', + client_public_encryption_key text not null comment 'The Public Key of the client''s encryption key', + server_public_encryption_key text not null comment 'The server''s public encryption key for this session', + server_private_encryption_key text not null comment 'The server''s private encryption key for this session', + private_shared_secret text null comment 'The shared secret encryption key between the Client & Server', + client_transport_encryption_key text null comment 'The encryption key for sending messages to the client', + server_transport_encryption_key text null comment 'The encryption key for sending messages to the server', + state enum ('AWAITING_DHE', 'ACTIVE', 'CLOSED', 'EXPIRED') default 'AWAITING_DHE' not null comment 'The status of the session', + flags text null comment 'The current flags that is set to the session', + created timestamp default current_timestamp() not null comment 'The Timestamp for when the session was last created', + last_request timestamp null comment 'The Timestamp for when the last request was made using this session', constraint sessions_uuid_uindex unique (uuid) comment 'The Unique Primary index for the session UUID', constraint sessions_registered_peers_uuid_fk diff --git a/src/Socialbox/Classes/RpcClient.php b/src/Socialbox/Classes/RpcClient.php index 9c3a42e..99dc878 100644 --- a/src/Socialbox/Classes/RpcClient.php +++ b/src/Socialbox/Classes/RpcClient.php @@ -2,11 +2,10 @@ namespace Socialbox\Classes; - use Socialbox\Enums\Options\ClientOptions; + use Socialbox\Enums\StandardError; use Socialbox\Enums\StandardHeaders; use Socialbox\Enums\Types\RequestType; use Socialbox\Exceptions\CryptographyException; - use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\ResolutionException; use Socialbox\Exceptions\RpcException; use Socialbox\Objects\ExportedSession; @@ -14,6 +13,7 @@ use Socialbox\Objects\PeerAddress; use Socialbox\Objects\RpcRequest; use Socialbox\Objects\RpcResult; + use Socialbox\Objects\Standard\ServerInformation; class RpcClient { @@ -22,9 +22,14 @@ private bool $bypassSignatureVerification; private PeerAddress $peerAddress; - private KeyPair $keyPair; - private string $encryptionKey; - private string $serverPublicKey; + private string $serverPublicSigningKey; + private string $serverPublicEncryptionKey; + private KeyPair $clientSigningKeyPair; + private KeyPair $clientEncryptionKeyPair; + private string $privateSharedSecret; + private string $clientTransportEncryptionKey; + private string $serverTransportEncryptionKey; + private ServerInformation $serverInformation; private string $rpcEndpoint; private string $sessionUuid; @@ -42,14 +47,41 @@ $this->bypassSignatureVerification = false; // If an exported session is provided, no need to re-connect. + // Just use the session details provided. if($exportedSession !== null) { + // Check if the server keypair has expired from the exported session + if(time() > $exportedSession->getServerKeypairExpires()) + { + throw new RpcException('The server keypair has expired, a new session must be created'); + } + $this->peerAddress = PeerAddress::fromAddress($exportedSession->getPeerAddress()); - $this->keyPair = new KeyPair($exportedSession->getPublicKey(), $exportedSession->getPrivateKey()); - $this->encryptionKey = $exportedSession->getEncryptionKey(); - $this->serverPublicKey = $exportedSession->getServerPublicKey(); $this->rpcEndpoint = $exportedSession->getRpcEndpoint(); $this->sessionUuid = $exportedSession->getSessionUuid(); + $this->serverPublicSigningKey = $exportedSession->getServerPublicSigningKey(); + $this->serverPublicEncryptionKey = $exportedSession->getServerPublicEncryptionKey(); + $this->clientSigningKeyPair = new KeyPair($exportedSession->getClientPublicSigningKey(), $exportedSession->getClientPrivateSigningKey()); + $this->clientEncryptionKeyPair = new KeyPair($exportedSession->getClientPublicEncryptionKey(), $exportedSession->getClientPrivateEncryptionKey()); + $this->privateSharedSecret = $exportedSession->getPrivateSharedSecret(); + $this->clientTransportEncryptionKey = $exportedSession->getClientTransportEncryptionKey(); + $this->serverTransportEncryptionKey = $exportedSession->getServerTransportEncryptionKey(); + + // Still solve the server information + $this->serverInformation = self::getServerInformation(); + + // Check if the active keypair has expired + if(time() > $this->serverInformation->getServerKeypairExpires()) + { + throw new RpcException('The server keypair has expired but the server has not provided a new keypair, contact the server administrator'); + } + + // Check if the transport encryption algorithm has changed + if($this->serverInformation->getTransportEncryptionAlgorithm() !== $exportedSession->getTransportEncryptionAlgorithm()) + { + throw new RpcException('The server has changed its transport encryption algorithm, a new session must be created'); + } + return; } @@ -62,51 +94,61 @@ // Set the initial properties $this->peerAddress = $peerAddress; - // If the username is `host` and the domain is the same as this server's domain, we use our keypair - // Essentially this is a special case for the server to contact another server - if($this->peerAddress->isHost()) - { - $this->keyPair = new KeyPair(Configuration::getInstanceConfiguration()->getPublicKey(), Configuration::getInstanceConfiguration()->getPrivateKey()); - } - // Otherwise we generate a random keypair - else - { - $this->keyPair = Cryptography::generateKeyPair(); - } - - $this->encryptionKey = Cryptography::generateEncryptionKey(); - // Resolve the domain and get the server's Public Key & RPC Endpoint - try - { - $resolvedServer = ServerResolver::resolveDomain($this->peerAddress->getDomain(), false); - } - catch (DatabaseOperationException $e) - { - throw new ResolutionException('Failed to resolve domain: ' . $e->getMessage(), 0, $e); - } + $resolvedServer = ServerResolver::resolveDomain($this->peerAddress->getDomain(), false); - $this->serverPublicKey = $resolvedServer->getPublicKey(); - $this->rpcEndpoint = $resolvedServer->getEndpoint(); + // Import the RPC Endpoint & the server's public key. + $this->serverPublicSigningKey = $resolvedServer->getPublicSigningKey(); + $this->rpcEndpoint = $resolvedServer->getRpcEndpoint(); - if(empty($this->serverPublicKey)) + if(empty($this->serverPublicSigningKey)) { throw new ResolutionException('Failed to resolve domain: No public key found for the server'); } - // Attempt to create an encrypted session with the server - $this->sessionUuid = $this->createSession(); + // Resolve basic server information + $this->serverInformation = self::getServerInformation(); + + // Check if the server keypair has expired + if(time() > $this->serverInformation->getServerKeypairExpires()) + { + throw new RpcException('The server keypair has expired but the server has not provided a new keypair, contact the server administrator'); + } + + // If the username is `host` and the domain is the same as this server's domain, we use our signing keypair + // Essentially this is a special case for the server to contact another server + if($this->peerAddress->isHost()) + { + $this->clientSigningKeyPair = new KeyPair(Configuration::getCryptographyConfiguration()->getHostPublicKey(), Configuration::getCryptographyConfiguration()->getHostPrivateKey()); + } + // Otherwise we generate a random signing keypair + else + { + $this->clientSigningKeyPair = Cryptography::generateSigningKeyPair(); + } + + // Always use a random encryption keypair + $this->clientEncryptionKeyPair = Cryptography::generateEncryptionKeyPair(); + + // Create a session with the server, with the method we obtain the Session UUID + // And the server's public encryption key. + $this->createSession(); + + // Generate a transport encryption key on our end using the server's preferred algorithm + $this->clientTransportEncryptionKey = Cryptography::generateEncryptionKey($this->serverInformation->getTransportEncryptionAlgorithm()); + + // Preform the DHE so that transport encryption keys can be exchanged $this->sendDheExchange(); } /** - * Creates a new session by sending an HTTP GET request to the RPC endpoint. - * The request includes specific headers required for session initiation. + * Initiates a new session with the server and retrieves the session UUID. * - * @return string Returns the session UUID received from the server. - * @throws RpcException If the server response is invalid, the session creation fails, or no session UUID is returned. + * @return string The session UUID provided by the server upon successful session creation. + * @throws RpcException If the session cannot be created, if the server does not provide a valid response, + * or critical headers like encryption public key are missing in the server's response. */ - private function createSession(): string + private function createSession(): void { $ch = curl_init(); @@ -116,28 +158,45 @@ StandardHeaders::CLIENT_NAME->value . ': ' . self::CLIENT_NAME, StandardHeaders::CLIENT_VERSION->value . ': ' . self::CLIENT_VERSION, StandardHeaders::IDENTIFY_AS->value . ': ' . $this->peerAddress->getAddress(), + // Always provide our generated encrypted public key + StandardHeaders::ENCRYPTION_PUBLIC_KEY->value . ': ' . $this->clientEncryptionKeyPair->getPublicKey() ]; // If we're not connecting as the host, we need to provide our public key // Otherwise, the server will obtain the public key itself from DNS records rather than trusting the client if(!$this->peerAddress->isHost()) { - $headers[] = StandardHeaders::PUBLIC_KEY->value . ': ' . $this->keyPair->getPublicKey(); + $headers[] = StandardHeaders::SIGNING_PUBLIC_KEY->value . ': ' . $this->clientSigningKeyPair->getPublicKey(); } + $responseHeaders = []; curl_setopt($ch, CURLOPT_URL, $this->rpcEndpoint); curl_setopt($ch, CURLOPT_HTTPGET, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + // Capture the response headers to get the encryption public key + curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($curl, $header) use (&$responseHeaders) + { + $len = strlen($header); + $header = explode(':', $header, 2); + if (count($header) < 2) // ignore invalid headers + { + return $len; + } + $responseHeaders[strtolower(trim($header[0]))][] = trim($header[1]); + return $len; + }); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); $response = curl_exec($ch); + // If the response is false, the request failed if($response === false) { curl_close($ch); throw new RpcException(sprintf('Failed to create the session at %s, no response received', $this->rpcEndpoint)); } + // If the response code is not 201, the request failed $responseCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); if($responseCode !== 201) { @@ -151,14 +210,44 @@ throw new RpcException(sprintf('Failed to create the session at %s, server responded with ' . $responseCode . ': ' . $response, $this->rpcEndpoint)); } + // If the response is empty, the server did not provide a session UUID if(empty($response)) { curl_close($ch); throw new RpcException(sprintf('Failed to create the session at %s, server did not return a session UUID', $this->rpcEndpoint)); } + // Get the Encryption Public Key from the server's response headers + $serverPublicEncryptionKey = $responseHeaders[strtolower(StandardHeaders::ENCRYPTION_PUBLIC_KEY->value)][0] ?? null; + + // null check + if($serverPublicEncryptionKey === null) + { + curl_close($ch); + throw new RpcException('Failed to create session at %s, the server did not return a public encryption key'); + } + + // Validate the server's encryption public key + if(!Cryptography::validatePublicEncryptionKey($serverPublicEncryptionKey)) + { + curl_close($ch); + throw new RpcException('The server did not provide a valid encryption public key'); + } + + // If the server did not provide an encryption public key, the response is invalid + // We can't preform the DHE without the server's encryption key. + if ($serverPublicEncryptionKey === null) + { + curl_close($ch); + throw new RpcException('The server did not provide a signature for the response'); + } + curl_close($ch); - return $response; + + // Set the server's encryption key + $this->serverPublicEncryptionKey = $serverPublicEncryptionKey; + // Set the session UUID + $this->sessionUuid = $response; } /** @@ -168,15 +257,26 @@ */ private function sendDheExchange(): void { + // First preform the DHE + try + { + $this->privateSharedSecret = Cryptography::performDHE($this->serverPublicEncryptionKey, $this->clientEncryptionKeyPair->getPrivateKey()); + } + catch(CryptographyException $e) + { + throw new RpcException('Failed to preform DHE: ' . $e->getMessage(), StandardError::CRYPTOGRAPHIC_ERROR->value, $e); + } + // Request body should contain the encrypted key, the client's public key, and the session UUID // Upon success the server should return 204 without a body try { - $encryptedKey = Cryptography::encryptContent($this->encryptionKey, $this->serverPublicKey); + $encryptedKey = Cryptography::encryptShared($this->clientTransportEncryptionKey, $this->privateSharedSecret); + $signature = Cryptography::signMessage($this->clientTransportEncryptionKey, $this->clientSigningKeyPair->getPrivateKey()); } catch (CryptographyException $e) { - throw new RpcException('Failed to encrypt DHE exchange data', 0, $e); + throw new RpcException('Failed to encrypt DHE exchange data', StandardError::CRYPTOGRAPHIC_ERROR->value, $e); } $ch = curl_init(); @@ -186,6 +286,7 @@ curl_setopt($ch, CURLOPT_HTTPHEADER, [ StandardHeaders::REQUEST_TYPE->value . ': ' . RequestType::DHE_EXCHANGE->value, StandardHeaders::SESSION_UUID->value . ': ' . $this->sessionUuid, + StandardHeaders::SIGNATURE->value . ': ' . $signature ]); curl_setopt($ch, CURLOPT_POSTFIELDS, $encryptedKey); @@ -194,17 +295,28 @@ if($response === false) { curl_close($ch); - throw new RpcException('Failed to send DHE exchange, no response received'); + throw new RpcException('Failed to send DHE exchange, no response received', StandardError::CRYPTOGRAPHIC_ERROR->value); } $responseCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); - if($responseCode !== 204) + if($responseCode !== 200) { curl_close($ch); - throw new RpcException('Failed to send DHE exchange, server responded with ' . $responseCode . ': ' . $response); + throw new RpcException('Failed to send DHE exchange, server responded with ' . $responseCode . ': ' . $response, StandardError::CRYPTOGRAPHIC_ERROR->value); } - curl_close($ch); + try + { + $this->serverTransportEncryptionKey = Cryptography::decryptShared($response, $this->privateSharedSecret); + } + catch(CryptographyException $e) + { + throw new RpcException('Failed to decrypt DHE exchange data', 0, $e); + } + finally + { + curl_close($ch); + } } /** @@ -218,8 +330,16 @@ { try { - $encryptedData = Cryptography::encryptTransport($jsonData, $this->encryptionKey); - $signature = Cryptography::signContent($jsonData, $this->keyPair->getPrivateKey(), true); + $encryptedData = Cryptography::encryptMessage( + message: $jsonData, + encryptionKey: $this->serverTransportEncryptionKey, + algorithm: $this->serverInformation->getTransportEncryptionAlgorithm() + ); + + $signature = Cryptography::signMessage( + message: $jsonData, + privateKey: $this->clientSigningKeyPair->getPrivateKey(), + ); } catch (CryptographyException $e) { @@ -289,7 +409,11 @@ try { - $decryptedResponse = Cryptography::decryptTransport($responseString, $this->encryptionKey); + $decryptedResponse = Cryptography::decryptMessage( + encryptedMessage: $responseString, + encryptionKey: $this->clientTransportEncryptionKey, + algorithm: $this->serverInformation->getTransportEncryptionAlgorithm() + ); } catch (CryptographyException $e) { @@ -298,7 +422,7 @@ if (!$this->bypassSignatureVerification) { - $signature = $headers['signature'][0] ?? null; + $signature = $headers[strtolower(StandardHeaders::SIGNATURE->value)][0] ?? null; if ($signature === null) { throw new RpcException('The server did not provide a signature for the response'); @@ -306,7 +430,11 @@ try { - if (!Cryptography::verifyContent($decryptedResponse, $signature, $this->serverPublicKey, true)) + if(!Cryptography::verifyMessage( + message: $decryptedResponse, + signature: $signature, + publicKey: $this->serverPublicSigningKey, + )) { throw new RpcException('Failed to verify the response signature'); } @@ -333,6 +461,59 @@ } } + /** + * Retrieves server information by making an RPC request. + * + * @return ServerInformation The parsed server information received in the response. + * @throws RpcException If the request fails, no response is received, or the server returns an error status code or invalid data. + */ + public function getServerInformation(): ServerInformation + { + $ch = curl_init(); + + // Basic session details + $headers = [ + StandardHeaders::REQUEST_TYPE->value . ': ' . RequestType::INFO->value, + StandardHeaders::CLIENT_NAME->value . ': ' . self::CLIENT_NAME, + StandardHeaders::CLIENT_VERSION->value . ': ' . self::CLIENT_VERSION, + ]; + + curl_setopt($ch, CURLOPT_URL, $this->rpcEndpoint); + curl_setopt($ch, CURLOPT_HTTPGET, true); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + + $response = curl_exec($ch); + + if($response === false) + { + curl_close($ch); + throw new RpcException(sprintf('Failed to get server information at %s, no response received', $this->rpcEndpoint)); + } + + $responseCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); + if($responseCode !== 200) + { + curl_close($ch); + + if(empty($response)) + { + throw new RpcException(sprintf('Failed to get server information at %s, server responded with ' . $responseCode, $this->rpcEndpoint)); + } + + throw new RpcException(sprintf('Failed to get server information at %s, server responded with ' . $responseCode . ': ' . $response, $this->rpcEndpoint)); + } + + if(empty($response)) + { + curl_close($ch); + throw new RpcException(sprintf('Failed to get server information at %s, server returned an empty response', $this->rpcEndpoint)); + } + + curl_close($ch); + return ServerInformation::fromArray(json_decode($response, true)); + } + /** * Sends an RPC request and retrieves the corresponding RPC response. * @@ -395,12 +576,19 @@ { return new ExportedSession([ 'peer_address' => $this->peerAddress->getAddress(), - 'private_key' => $this->keyPair->getPrivateKey(), - 'public_key' => $this->keyPair->getPublicKey(), - 'encryption_key' => $this->encryptionKey, - 'server_public_key' => $this->serverPublicKey, 'rpc_endpoint' => $this->rpcEndpoint, - 'session_uuid' => $this->sessionUuid + 'session_uuid' => $this->sessionUuid, + 'transport_encryption_algorithm' => $this->serverInformation->getTransportEncryptionAlgorithm(), + 'server_keypair_expires' => $this->serverInformation->getServerKeypairExpires(), + 'server_public_signing_key' => $this->serverPublicSigningKey, + 'server_public_encryption_key' => $this->serverPublicEncryptionKey, + 'client_public_signing_key' => $this->clientSigningKeyPair->getPublicKey(), + 'client_private_signing_key' => $this->clientSigningKeyPair->getPrivateKey(), + 'client_public_encryption_key' => $this->clientEncryptionKeyPair->getPublicKey(), + 'client_private_encryption_key' => $this->clientEncryptionKeyPair->getPrivateKey(), + 'private_shared_secret' => $this->privateSharedSecret, + 'client_transport_encryption_key' => $this->clientTransportEncryptionKey, + 'server_transport_encryption_key' => $this->serverTransportEncryptionKey ]); } } \ No newline at end of file diff --git a/src/Socialbox/Classes/SecuredPassword.php b/src/Socialbox/Classes/SecuredPassword.php deleted file mode 100644 index 30e031f..0000000 --- a/src/Socialbox/Classes/SecuredPassword.php +++ /dev/null @@ -1,96 +0,0 @@ -decrypt(); - $saltedPassword = $decrypted->getSalt() . $password; - $derivedKey = hash_pbkdf2('sha512', $saltedPassword, $decrypted->getPepper(), self::ITERATIONS, self::KEY_LENGTH / 8, true); - - try - { - $iv = random_bytes(openssl_cipher_iv_length(self::ENCRYPTION_ALGORITHM)); - } - catch (RandomException $e) - { - throw new CryptographyException("Failed to generate IV for password encryption", $e); - } - - $tag = null; - $encryptedPassword = openssl_encrypt($derivedKey, self::ENCRYPTION_ALGORITHM, base64_decode($decrypted->getKey()), OPENSSL_RAW_DATA, $iv, $tag); - - if ($encryptedPassword === false) - { - throw new CryptographyException("Password encryption failed"); - } - - return new SecurePasswordRecord([ - 'peer_uuid' => $peerUuid, - 'iv' => base64_encode($iv), - 'encrypted_password' => base64_encode($encryptedPassword), - 'encrypted_tag' => base64_encode($tag), - 'updated' => (new DateTime())->setTimestamp(time()) - ]); - } - - /** - * Verifies the provided password against the secured data and encryption records. - * - * @param string $input The user-provided password to be verified. - * @param SecurePasswordRecord $secured An array containing encrypted data required for verification. - * @param EncryptionRecord[] $encryptionRecords An array of encryption records used to perform decryption and validation. - * @return bool Returns true if the password matches the secured data; otherwise, returns false. - * @throws CryptographyException - */ - public static function verifyPassword(string $input, SecurePasswordRecord $secured, array $encryptionRecords): bool - { - foreach ($encryptionRecords as $record) - { - $decrypted = $record->decrypt(); - $saltedInput = $decrypted->getSalt() . $input; - $derivedKey = hash_pbkdf2('sha512', $saltedInput, $decrypted->getPepper(), self::ITERATIONS, self::KEY_LENGTH / 8, true); - - // Validation by re-encrypting and comparing - $encryptedTag = base64_decode($secured->getEncryptedTag()); - $reEncryptedPassword = openssl_encrypt($derivedKey, - self::ENCRYPTION_ALGORITHM, base64_decode($decrypted->getKey()), OPENSSL_RAW_DATA, - base64_decode($secured->getIv()), $encryptedTag - ); - - if ($reEncryptedPassword !== false && hash_equals($reEncryptedPassword, base64_decode($secured->getEncryptedPassword()))) - { - return true; - } - } - - return false; - } - } diff --git a/src/Socialbox/Classes/ServerResolver.php b/src/Socialbox/Classes/ServerResolver.php index 8d18f94..c74a656 100644 --- a/src/Socialbox/Classes/ServerResolver.php +++ b/src/Socialbox/Classes/ServerResolver.php @@ -2,32 +2,31 @@ namespace Socialbox\Classes; - use Socialbox\Exceptions\DatabaseOperationException; + use InvalidArgumentException; use Socialbox\Exceptions\ResolutionException; - use Socialbox\Managers\ResolvedServersManager; - use Socialbox\Objects\ResolvedServer; + use Socialbox\Managers\ResolvedDnsRecordsManager; + use Socialbox\Objects\DnsRecord; class ServerResolver { - private const string PATTERN = '/v=socialbox;sb-rpc=(https?:\/\/[^;]+);sb-key=([^;]+)/'; - /** - * Resolves a given domain to fetch the RPC endpoint and public key from its DNS TXT records. + * Resolves a domain by retrieving and parsing its DNS TXT records. + * Optionally checks a database for cached resolution data before performing a DNS query. * - * @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. - * @throws DatabaseOperationException + * @param string $domain The domain name to resolve. + * @param bool $useDatabase Whether to check the database for cached resolution data; defaults to true. + * @return DnsRecord The parsed DNS record for the given domain. + * @throws ResolutionException If the DNS TXT records cannot be retrieved or parsed. */ - public static function resolveDomain(string $domain, bool $useDatabase=true): ResolvedServer + public static function resolveDomain(string $domain, bool $useDatabase=true): DnsRecord { - // First query the database to check if the domain is already resolved - if($useDatabase) + // Check the database if enabled + if ($useDatabase) { - $resolvedServer = ResolvedServersManager::getResolvedServer($domain); - if($resolvedServer !== null) + $resolvedServer = ResolvedDnsRecordsManager::getDnsRecord($domain); + if ($resolvedServer !== null) { - return $resolvedServer->toResolvedServer(); + return $resolvedServer; } } @@ -38,23 +37,23 @@ } $fullRecord = self::concatenateTxtRecords($txtRecords); - if (preg_match(self::PATTERN, $fullRecord, $matches)) + + try { - $endpoint = trim($matches[1]); - $publicKey = trim(str_replace(' ', '', $matches[2])); - if (empty($endpoint)) + // Parse the TXT record using DnsHelper + $record = DnsHelper::parseTxt($fullRecord); + + // Cache the resolved server record in the database + if($useDatabase) { - throw new ResolutionException(sprintf("Failed to resolve RPC endpoint for %s", $domain)); + ResolvedDnsRecordsManager::addResolvedServer($domain, $record); } - if (empty($publicKey)) - { - throw new ResolutionException(sprintf("Failed to resolve public key for %s", $domain)); - } - return new ResolvedServer($endpoint, $publicKey); + + return $record; } - else + catch (InvalidArgumentException $e) { - throw new ResolutionException(sprintf("Failed to find valid SocialBox record for %s", $domain)); + throw new ResolutionException(sprintf("Failed to find valid SocialBox record for %s: %s", $domain, $e->getMessage())); } } @@ -64,9 +63,9 @@ * @param string $domain The domain name to fetch TXT records for. * @return array|false An array of DNS TXT records on success, or false on failure. */ - private static function dnsGetTxtRecords(string $domain) + private static function dnsGetTxtRecords(string $domain): array|false { - return dns_get_record($domain, DNS_TXT); + return @dns_get_record($domain, DNS_TXT); } /** diff --git a/src/Socialbox/Classes/StandardMethods/SettingsSetDisplayPicture.php b/src/Socialbox/Classes/StandardMethods/SettingsSetDisplayPicture.php index ddd77d2..e79b2ce 100644 --- a/src/Socialbox/Classes/StandardMethods/SettingsSetDisplayPicture.php +++ b/src/Socialbox/Classes/StandardMethods/SettingsSetDisplayPicture.php @@ -38,14 +38,14 @@ if($decodedImage === false) { - return $rpcRequest->produceError(StandardError::BAD_REQUEST, "Failed to decode JPEG image base64 data"); + return $rpcRequest->produceError(StandardError::RPC_BAD_REQUEST, "Failed to decode JPEG image base64 data"); } $sanitizedImage = Utilities::resizeImage(Utilities::sanitizeJpeg($decodedImage), 126, 126); } catch(Exception $e) { - throw new StandardException('Failed to process JPEG image: ' . $e->getMessage(), StandardError::BAD_REQUEST, $e); + throw new StandardException('Failed to process JPEG image: ' . $e->getMessage(), StandardError::RPC_BAD_REQUEST, $e); } try diff --git a/src/Socialbox/Classes/Utilities.php b/src/Socialbox/Classes/Utilities.php index 351acc5..ce7c985 100644 --- a/src/Socialbox/Classes/Utilities.php +++ b/src/Socialbox/Classes/Utilities.php @@ -122,22 +122,37 @@ return $headers; } - /** - * Converts a Throwable object into a formatted string. - * - * @param Throwable $e The throwable to be converted into a string. - * @return string The formatted string representation of the throwable, including the exception class, message, file, line, and stack trace. - */ - public static function throwableToString(Throwable $e): string + public static function throwableToString(Throwable $e, int $level=0): string { - return sprintf( - "%s: %s in %s:%d\nStack trace:\n%s", - get_class($e), - $e->getMessage(), - $e->getFile(), - $e->getLine(), - $e->getTraceAsString() + // Indentation for nested exceptions + $indentation = str_repeat(' ', $level); + + // Basic information about the Throwable + $type = get_class($e); + $message = $e->getMessage() ?: 'No message'; + $file = $e->getFile() ?: 'Unknown file'; + $line = $e->getLine() ?? 'Unknown line'; + + // Compose the base string representation of this Throwable + $result = sprintf("%s%s: %s\n%s in %s on line %s\n", + $indentation, $type, $message, $indentation, $file, $line ); + + // Append stack trace if available + $stackTrace = $e->getTraceAsString(); + if (!empty($stackTrace)) + { + $result .= $indentation . " Stack trace:\n" . $indentation . " " . str_replace("\n", "\n" . $indentation . " ", $stackTrace) . "\n"; + } + + // Recursively append the cause if it exists + $previous = $e->getPrevious(); + if ($previous) + { + $result .= $indentation . "Caused by:\n" . self::throwableToString($previous, $level + 1); + } + + return $result; } /** diff --git a/src/Socialbox/Enums/DatabaseObjects.php b/src/Socialbox/Enums/DatabaseObjects.php index 95700af..14d7f8f 100644 --- a/src/Socialbox/Enums/DatabaseObjects.php +++ b/src/Socialbox/Enums/DatabaseObjects.php @@ -5,8 +5,7 @@ enum DatabaseObjects : string { case VARIABLES = 'variables.sql'; - case ENCRYPTION_RECORDS = 'encryption_records.sql'; - case RESOLVED_SERVERS = 'resolved_servers.sql'; + case RESOLVED_DNS_RECORDS = 'resolved_dns_records.sql'; case REGISTERED_PEERS = 'registered_peers.sql'; @@ -24,7 +23,7 @@ { return match ($this) { - self::VARIABLES, self::ENCRYPTION_RECORDS, self::RESOLVED_SERVERS => 0, + self::VARIABLES, self::RESOLVED_DNS_RECORDS => 0, self::REGISTERED_PEERS => 1, self::AUTHENTICATION_PASSWORDS, self::CAPTCHA_IMAGES, self::SESSIONS, self::EXTERNAL_SESSIONS => 2, }; diff --git a/src/Socialbox/Enums/StandardError.php b/src/Socialbox/Enums/StandardError.php index 6b7544a..ef54bfd 100644 --- a/src/Socialbox/Enums/StandardError.php +++ b/src/Socialbox/Enums/StandardError.php @@ -7,16 +7,21 @@ enum StandardError : int // Fallback Codes case UNKNOWN = -1; + // Server/Request Errors + case INTERNAL_SERVER_ERROR = -100; + case SERVER_UNAVAILABLE = -101; + case BAD_REQUEST = -102; + case FORBIDDEN = -103; + case UNAUTHORIZED = -104; + case RESOLUTION_FAILED = -105; + case CRYPTOGRAPHIC_ERROR = -106; + // RPC Errors case RPC_METHOD_NOT_FOUND = -1000; case RPC_INVALID_ARGUMENTS = -1001; - - // Server Errors - case INTERNAL_SERVER_ERROR = -2000; - case SERVER_UNAVAILABLE = -2001; + CASE RPC_BAD_REQUEST = -1002; // Client Errors - case BAD_REQUEST = -3000; case METHOD_NOT_ALLOWED = -3001; // Authentication/Cryptography Errors diff --git a/src/Socialbox/Enums/StandardHeaders.php b/src/Socialbox/Enums/StandardHeaders.php index 931280d..630b2fb 100644 --- a/src/Socialbox/Enums/StandardHeaders.php +++ b/src/Socialbox/Enums/StandardHeaders.php @@ -8,10 +8,12 @@ enum StandardHeaders : string { case REQUEST_TYPE = 'Request-Type'; + case ERROR_CODE = 'Error-Code'; case IDENTIFY_AS = 'Identify-As'; case CLIENT_NAME = 'Client-Name'; case CLIENT_VERSION = 'Client-Version'; - case PUBLIC_KEY = 'Public-Key'; + case SIGNING_PUBLIC_KEY = 'Signing-Public-Key'; + case ENCRYPTION_PUBLIC_KEY = 'Encryption-Public-Key'; case SESSION_UUID = 'Session-UUID'; case SIGNATURE = 'Signature'; diff --git a/src/Socialbox/Enums/Types/RequestType.php b/src/Socialbox/Enums/Types/RequestType.php index 0beef55..f908aea 100644 --- a/src/Socialbox/Enums/Types/RequestType.php +++ b/src/Socialbox/Enums/Types/RequestType.php @@ -4,6 +4,11 @@ enum RequestType : string { + /** + * Represents the action of getting server information (Non-RPC Request) + */ + case INFO = 'info'; + /** * Represents the action of initiating a session. */ diff --git a/src/Socialbox/Managers/EncryptionRecordsManager.php b/src/Socialbox/Managers/EncryptionRecordsManager.php deleted file mode 100644 index 3d2cc90..0000000 --- a/src/Socialbox/Managers/EncryptionRecordsManager.php +++ /dev/null @@ -1,205 +0,0 @@ -prepare('SELECT COUNT(*) FROM encryption_records'); - $stmt->execute(); - return $stmt->fetchColumn(); - } - catch (PDOException $e) - { - throw new DatabaseOperationException('Failed to retrieve encryption record count', $e); - } - } - - /** - * Inserts a new encryption record into the encryption_records table. - * - * @param EncryptionRecord $record The encryption record to insert, containing data, IV, and tag. - * @return void - * @throws DatabaseOperationException If the insertion into the database fails. - */ - private static function insertRecord(EncryptionRecord $record): void - { - try - { - $stmt = Database::getConnection()->prepare('INSERT INTO encryption_records (data, iv, tag) VALUES (?, ?, ?)'); - - $data = $record->getData(); - $stmt->bindParam(1, $data); - - $iv = $record->getIv(); - $stmt->bindParam(2, $iv); - - $tag = $record->getTag(); - $stmt->bindParam(3, $tag); - - $stmt->execute(); - } - catch(PDOException $e) - { - - throw new DatabaseOperationException('Failed to insert encryption record into the database', $e); - } - } - - /** - * Retrieves a random encryption record from the database. - * - * @return EncryptionRecord An instance of EncryptionRecord containing the data of a randomly selected record. - * @throws DatabaseOperationException If an error occurs while attempting to retrieve the record from the database. - */ - public static function getRandomRecord(): EncryptionRecord - { - try - { - $stmt = Database::getConnection()->prepare('SELECT * FROM encryption_records ORDER BY RAND() LIMIT 1'); - $stmt->execute(); - $data = $stmt->fetch(); - - return new EncryptionRecord($data); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to retrieve a random encryption record', $e); - } - } - - /** - * Retrieves all encryption records from the database. - * - * @return EncryptionRecord[] An array of EncryptionRecord instances, each representing a record from the database. - * @throws DatabaseOperationException If an error occurs while attempting to retrieve the records from the database. - */ - public static function getAllRecords(): array - { - try - { - $stmt = Database::getConnection()->prepare('SELECT * FROM encryption_records'); - $stmt->execute(); - $data = $stmt->fetchAll(); - - $records = []; - foreach ($data as $record) - { - $records[] = new EncryptionRecord($record); - } - - return $records; - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to retrieve all encryption records', $e); - } - } - - /** - * Generates encryption records and inserts them into the database until the specified total count is reached. - * - * @param int $count The total number of encryption records desired in the database. - * @return int The number of new records that were created and inserted. - * @throws CryptographyException - * @throws DatabaseOperationException - */ - public static function generateRecords(int $count): int - { - $currentCount = self::getRecordCount(); - if($currentCount >= $count) - { - return 0; - } - - $created = 0; - for($i = 0; $i < $count - $currentCount; $i++) - { - self::insertRecord(self::generateEncryptionRecord()); - $created++; - } - - return $created; - } - - /** - * Generates a new encryption record containing a key, pepper, and salt. - * - * @return EncryptionRecord An instance of EncryptionRecord containing an encrypted structure - * with the generated key, pepper, and salt. - * @throws CryptographyException If random byte generation fails during the creation of the encryption record. - */ - private static function generateEncryptionRecord(): EncryptionRecord - { - try - { - $key = random_bytes(self::KEY_LENGTH / 8); - $pepper = bin2hex(random_bytes(SecuredPassword::PEPPER_LENGTH / 2)); - $salt = bin2hex(random_bytes(self::KEY_LENGTH / 16)); - - } - catch (RandomException $e) - { - throw new CryptographyException("Random bytes generation failed", $e->getCode(), $e); - } - - return self::encrypt(['key' => base64_encode($key), 'pepper' => $pepper, 'salt' => $salt,]); - } - - /** - * Encrypts the given vault item and returns an EncryptionRecord containing the encrypted data. - * - * @param array $vaultItem The associative array representing the vault item to be encrypted. - * @return EncryptionRecord An instance of EncryptionRecord containing the encrypted vault data, initialization vector (IV), and authentication tag. - * @throws CryptographyException If the initialization vector generation or vault encryption process fails. - */ - private static function encrypt(array $vaultItem): EncryptionRecord - { - $serializedVault = json_encode($vaultItem); - - try - { - $iv = random_bytes(openssl_cipher_iv_length(SecuredPassword::ENCRYPTION_ALGORITHM)); - } - catch (RandomException $e) - { - throw new CryptographyException("IV generation failed", $e->getCode(), $e); - } - $tag = null; - - $encryptedVault = openssl_encrypt($serializedVault, SecuredPassword::ENCRYPTION_ALGORITHM, - Configuration::getInstanceConfiguration()->getRandomEncryptionKey(), OPENSSL_RAW_DATA, $iv, $tag - ); - - if ($encryptedVault === false) - { - throw new CryptographyException("Vault encryption failed"); - } - - return new EncryptionRecord([ - 'data' => base64_encode($encryptedVault), - 'iv' => base64_encode($iv), - 'tag' => base64_encode($tag), - ]); - } - } \ No newline at end of file diff --git a/src/Socialbox/Managers/PasswordManager.php b/src/Socialbox/Managers/PasswordManager.php index 6de3a9f..313cc4f 100644 --- a/src/Socialbox/Managers/PasswordManager.php +++ b/src/Socialbox/Managers/PasswordManager.php @@ -2,13 +2,15 @@ namespace Socialbox\Managers; + use DateTime; use PDO; + use PDOException; + use Socialbox\Classes\Configuration; + use Socialbox\Classes\Cryptography; use Socialbox\Classes\Database; - use Socialbox\Classes\SecuredPassword; use Socialbox\Exceptions\CryptographyException; use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Objects\Database\RegisteredPeerRecord; - use Socialbox\Objects\Database\SecurePasswordRecord; class PasswordManager { @@ -34,154 +36,139 @@ return $stmt->fetchColumn() > 0; } - catch (\PDOException $e) + catch (PDOException $e) { throw new DatabaseOperationException('An error occurred while checking the password usage in the database', $e); } } /** - * Sets a password for a given user or peer record by securely encrypting it - * and storing it in the authentication_passwords database table. + * Sets a secured password for the given peer UUID or registered peer record. * - * @param string|RegisteredPeerRecord $peerUuid The UUID of the peer or an instance of RegisteredPeerRecord. - * @param string $password The plaintext password to be securely stored. - * @throws CryptographyException If an error occurs while securing the password. - * @throws DatabaseOperationException If an error occurs while attempting to store the password in the database. - * @throws \DateMalformedStringException If the updated timestamp cannot be formatted. + * @param string|RegisteredPeerRecord $peerUuid The unique identifier or registered peer record of the user. + * @param string $hash The plaintext password to be securely stored. * @return void + * @throws DatabaseOperationException If an error occurs while storing the password in the database. + * @throws CryptographyException If an error occurs during password encryption or hashing. */ - public static function setPassword(string|RegisteredPeerRecord $peerUuid, string $password): void + public static function setPassword(string|RegisteredPeerRecord $peerUuid, string $hash): void { if($peerUuid instanceof RegisteredPeerRecord) { $peerUuid = $peerUuid->getUuid(); } - - $encryptionRecord = EncryptionRecordsManager::getRandomRecord(); - $securedPassword = SecuredPassword::securePassword($peerUuid, $password, $encryptionRecord); + + // Throws an exception if the hash is invalid + Cryptography::validatePasswordHash($hash); + + $encryptionKey = Configuration::getCryptographyConfiguration()->getRandomInternalEncryptionKey(); + $securedPassword = Cryptography::encryptMessage($hash, $encryptionKey, Configuration::getCryptographyConfiguration()->getEncryptionKeysAlgorithm()); try { - $stmt = Database::getConnection()->prepare("INSERT INTO authentication_passwords (peer_uuid, iv, encrypted_password, encrypted_tag) VALUES (:peer_uuid, :iv, :encrypted_password, :encrypted_tag)"); + $stmt = Database::getConnection()->prepare("INSERT INTO authentication_passwords (peer_uuid, hash) VALUES (:peer_uuid, :hash)"); $stmt->bindParam(":peer_uuid", $peerUuid); - - $iv = $securedPassword->getIv(); - $stmt->bindParam(':iv', $iv); - - $encryptedPassword = $securedPassword->getEncryptedPassword(); - $stmt->bindParam(':encrypted_password', $encryptedPassword); - - $encryptedTag = $securedPassword->getEncryptedTag(); - $stmt->bindParam(':encrypted_tag', $encryptedTag); + $stmt->bindParam(':hash', $securedPassword); $stmt->execute(); } - catch(\PDOException $e) + catch(PDOException $e) { throw new DatabaseOperationException(sprintf('Failed to set password for user %s', $peerUuid), $e); } } /** - * Updates the password for a given peer identified by their UUID or a RegisteredPeerRecord. + * Updates the secured password associated with the given peer UUID. * - * @param string|RegisteredPeerRecord $peerUuid The UUID of the peer or an instance of RegisteredPeerRecord. - * @param string $newPassword The new password to be set for the peer. - * @throws CryptographyException If an error occurs while securing the new password. - * @throws DatabaseOperationException If the update operation fails due to a database error. - * @throws \DateMalformedStringException If the updated timestamp cannot be formatted. - * @returns void + * @param string|RegisteredPeerRecord $peerUuid The unique identifier or registered peer record of the user. + * @param string $hash The new password to be stored securely. + * @return void + * @throws DatabaseOperationException If an error occurs while updating the password in the database. + * @throws CryptographyException If an error occurs while encrypting the password or validating the hash. */ - public static function updatePassword(string|RegisteredPeerRecord $peerUuid, string $newPassword): void + public static function updatePassword(string|RegisteredPeerRecord $peerUuid, string $hash): void { if($peerUuid instanceof RegisteredPeerRecord) { $peerUuid = $peerUuid->getUuid(); } + Cryptography::validatePasswordHash($hash); - $encryptionRecord = EncryptionRecordsManager::getRandomRecord(); - $securedPassword = SecuredPassword::securePassword($peerUuid, $newPassword, $encryptionRecord); + $encryptionKey = Configuration::getCryptographyConfiguration()->getRandomInternalEncryptionKey(); + $securedPassword = Cryptography::encryptMessage($hash, $encryptionKey, Configuration::getCryptographyConfiguration()->getEncryptionKeysAlgorithm()); try { - $stmt = Database::getConnection()->prepare("UPDATE authentication_passwords SET iv=:iv, encrypted_password=:encrypted_password, encrypted_tag=:encrypted_tag, updated=:updated WHERE peer_uuid=:peer_uuid"); - $stmt->bindParam(":peer_uuid", $peerUuid); - - $iv = $securedPassword->getIv(); - $stmt->bindParam(':iv', $iv); - - $encryptedPassword = $securedPassword->getEncryptedPassword(); - $stmt->bindParam(':encrypted_password', $encryptedPassword); - - $encryptedTag = $securedPassword->getEncryptedTag(); - $stmt->bindParam(':encrypted_tag', $encryptedTag); - - $updated = $securedPassword->getUpdated()->format('Y-m-d H:i:s'); + $stmt = Database::getConnection()->prepare("UPDATE authentication_passwords SET hash=:hash, updated=:updated WHERE peer_uuid=:peer_uuid"); + $updated = (new DateTime())->setTimestamp(time()); + $stmt->bindParam(':hash', $securedPassword); $stmt->bindParam(':updated', $updated); + $stmt->bindParam(':peer_uuid', $peerUuid); $stmt->execute(); } - catch(\PDOException $e) + catch(PDOException $e) { throw new DatabaseOperationException(sprintf('Failed to update password for user %s', $peerUuid), $e); } } /** - * Retrieves the password record associated with the given peer UUID. + * Verifies a given password against a stored password hash for a specific peer. * - * @param string|RegisteredPeerRecord $peerUuid The UUID of the peer or an instance of RegisteredPeerRecord. - * @return SecurePasswordRecord|null Returns a SecurePasswordRecord if found, or null if no record is present. - * @throws DatabaseOperationException If a database operation error occurs during the retrieval process. + * @param string|RegisteredPeerRecord $peerUuid The unique identifier of the peer, or an instance of RegisteredPeerRecord. + * @param string $hash The password hash to verify. + * @return bool Returns true if the password matches the stored hash; false otherwise. + * @throws CryptographyException If the password hash is invalid or an error occurs during the cryptographic operation. + * @throws DatabaseOperationException If an error occurs during the database operation. */ - private static function getPassword(string|RegisteredPeerRecord $peerUuid): ?SecurePasswordRecord + public static function verifyPassword(string|RegisteredPeerRecord $peerUuid, string $hash): bool { if($peerUuid instanceof RegisteredPeerRecord) { $peerUuid = $peerUuid->getUuid(); } + Cryptography::validatePasswordHash($hash); + try { - $statement = Database::getConnection()->prepare("SELECT * FROM authentication_passwords WHERE peer_uuid=:peer_uuid LIMIT 1"); - $statement->bindParam(':peer_uuid', $peerUuid); + $stmt = Database::getConnection()->prepare('SELECT hash FROM authentication_passwords WHERE peer_uuid=:uuid'); + $stmt->bindParam(':uuid', $peerUuid); + $stmt->execute(); - $statement->execute(); - $data = $statement->fetch(PDO::FETCH_ASSOC); - - if ($data === false) + $record = $stmt->fetch(PDO::FETCH_ASSOC); + if($record === false) { - return null; + return false; } - return SecurePasswordRecord::fromArray($data); - } - catch(\PDOException $e) - { - throw new DatabaseOperationException(sprintf('Failed to retrieve password record for user %s', $peerUuid), $e); - } - } + $encryptedHash = $record['hash']; + $decryptedHash = null; + foreach(Configuration::getCryptographyConfiguration()->getInternalEncryptionKeys() as $key) + { + try + { + $decryptedHash = Cryptography::decryptMessage($encryptedHash, $key, Configuration::getCryptographyConfiguration()->getEncryptionKeysAlgorithm()); + } + catch(CryptographyException) + { + continue; + } + } - /** - * Verifies if the provided password matches the secured password associated with the given peer UUID. - * - * @param string|RegisteredPeerRecord $peerUuid The unique identifier or registered peer record of the user. - * @param string $password The password to be verified. - * @return bool Returns true if the password is verified successfully; otherwise, false. - * @throws DatabaseOperationException If an error occurs while retrieving the password record from the database. - * @throws CryptographyException If an error occurs while verifying the password. - */ - public static function verifyPassword(string|RegisteredPeerRecord $peerUuid, string $password): bool - { - $securedPassword = self::getPassword($peerUuid); - if($securedPassword === null) - { - return false; - } + if($decryptedHash === null) + { + throw new CryptographyException('Cannot decrypt hashed password'); + } - $encryptionRecords = EncryptionRecordsManager::getAllRecords(); - return SecuredPassword::verifyPassword($password, $securedPassword, $encryptionRecords); + return Cryptography::verifyPassword($hash, $decryptedHash); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('An error occurred while verifying the password', $e); + } } } \ No newline at end of file diff --git a/src/Socialbox/Managers/ResolvedDnsRecordsManager.php b/src/Socialbox/Managers/ResolvedDnsRecordsManager.php new file mode 100644 index 0000000..b277462 --- /dev/null +++ b/src/Socialbox/Managers/ResolvedDnsRecordsManager.php @@ -0,0 +1,184 @@ +prepare("SELECT COUNT(*) FROM resolved_dns_records WHERE domain=?"); + $statement->bindParam(1, $domain); + $statement->execute(); + return $statement->fetchColumn() > 0; + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to check if a resolved server exists in the database', $e); + } + } + + /** + * Deletes a resolved server record from the database for the provided domain. + * + * @param string $domain The domain name of the resolved server to be deleted. + * @return void + * @throws DatabaseOperationException If the deletion process encounters a database error. + */ + public static function deleteResolvedServer(string $domain): void + { + try + { + $statement = Database::getConnection()->prepare("DELETE FROM resolved_dns_records WHERE domain=?"); + $statement->bindParam(1, $domain); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to delete a resolved server from the database', $e); + } + } + + /** + * Retrieves the last updated timestamp of a resolved server from the database for a given domain. + * + * This method queries the database to fetch the timestamp indicating when the resolved server + * associated with the specified domain was last updated. + * + * @param string $domain The domain name for which the last updated timestamp is to be retrieved. + * @return DateTime The DateTime object representing the last updated timestamp of the resolved server. + * + * @throws DatabaseOperationException If the operation to retrieve the updated timestamp from the + * database fails. + */ + public static function getResolvedServerUpdated(string $domain): DateTime + { + try + { + $statement = Database::getConnection()->prepare("SELECT updated FROM resolved_dns_records WHERE domain=?"); + $statement->bindParam(1, $domain); + $statement->execute(); + $result = $statement->fetchColumn(); + return new DateTime($result); + } + catch(Exception $e) + { + throw new DatabaseOperationException('Failed to get the updated date of a resolved server from the database', $e); + } + } + + /** + * Retrieves a DNS record for the specified domain from the database. + * + * This method fetches the DNS record details, such as the RPC endpoint, public key, + * and expiration details, associated with the provided domain. If no record is found, + * it returns null. + * + * @param string $domain The domain name for which the DNS record is to be retrieved. + * @return DnsRecord|null The DNS record object if found, or null if no record exists for the given domain. + * + * @throws DatabaseOperationException If the operation to retrieve the DNS record from + * the database fails. + */ + public static function getDnsRecord(string $domain): ?DnsRecord + { + try + { + $statement = Database::getConnection()->prepare("SELECT * FROM resolved_dns_records WHERE domain=?"); + $statement->bindParam(1, $domain); + $statement->execute(); + $result = $statement->fetch(); + + if($result === false) + { + return null; + } + + return DnsRecord::fromArray([ + 'rpc_endpoint' => $result['rpc_endpoint'], + 'public_key' => $result['public_key'], + 'expires' => $result['expires'] + ]); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to get a resolved server from the database', $e); + } + } + + /** + * Adds or updates a resolved server in the database based on the provided domain and DNS record. + * + * If a resolved server for the given domain already exists in the database, the server's details + * will be updated. Otherwise, a new record will be inserted into the database. + * + * @param string $domain The domain name associated with the resolved server. + * @param DnsRecord $dnsRecord An object containing DNS record details such as the RPC endpoint, + * public key, and expiration details. + * @return void + * @throws DatabaseOperationException If the operation to add or update the resolved server in + * the database fails. + */ + public static function addResolvedServer(string $domain, DnsRecord $dnsRecord): void + { + $endpoint = $dnsRecord->getRpcEndpoint(); + $publicKey = $dnsRecord->getPublicSigningKey(); + + if(self::resolvedServerExists($domain)) + { + $statement = Database::getConnection()->prepare("UPDATE resolved_dns_records SET rpc_endpoint=?, public_key=?, expires=?, updated=? WHERE domain=?"); + $statement->bindParam(1, $endpoint); + $statement->bindParam(2, $publicKey); + $expires = (new DateTime())->setTimestamp($dnsRecord->getExpires()); + $statement->bindParam(3, $expires); + $updated = new DateTime(); + $statement->bindParam(4, $updated); + $statement->bindParam(5, $domain); + $statement->execute(); + + if($statement->rowCount() === 0) + { + throw new DatabaseOperationException('Failed to update a resolved server in the database'); + } + + return; + } + + try + { + $statement = Database::getConnection()->prepare("INSERT INTO resolved_dns_records (domain, rpc_endpoint, public_key, expires, updated) VALUES (?, ?, ?, ?, ?)"); + $statement->bindParam(1, $domain); + $statement->bindParam(2, $endpoint); + $statement->bindParam(3, $publicKey); + $expires = (new DateTime())->setTimestamp($dnsRecord->getExpires()); + $statement->bindParam(4, $expires); + $updated = new DateTime(); + $statement->bindParam(5, $updated); + $statement->execute(); + + if($statement->rowCount() === 0) + { + throw new DatabaseOperationException('Failed to add a resolved server to the database'); + } + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to add a resolved server to the database', $e); + } + } + } \ No newline at end of file diff --git a/src/Socialbox/Managers/ResolvedServersManager.php b/src/Socialbox/Managers/ResolvedServersManager.php deleted file mode 100644 index 781f6f4..0000000 --- a/src/Socialbox/Managers/ResolvedServersManager.php +++ /dev/null @@ -1,152 +0,0 @@ -prepare("SELECT COUNT(*) FROM resolved_servers WHERE domain=?"); - $statement->bindParam(1, $domain); - $statement->execute(); - return $statement->fetchColumn() > 0; - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to check if a resolved server exists in the database', $e); - } - } - - /** - * Deletes a resolved server from the database. - * - * @param string $domain The domain name of the server to be deleted. - * @return void - * @throws DatabaseOperationException If the deletion operation fails. - */ - public static function deleteResolvedServer(string $domain): void - { - try - { - $statement = Database::getConnection()->prepare("DELETE FROM resolved_servers WHERE domain=?"); - $statement->bindParam(1, $domain); - $statement->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to delete a resolved server from the database', $e); - } - } - - /** - * Retrieves the last updated date of a resolved server based on its domain. - * - * @param string $domain The domain of the resolved server. - * @return DateTime The last updated date and time of the resolved server. - */ - public static function getResolvedServerUpdated(string $domain): DateTime - { - try - { - $statement = Database::getConnection()->prepare("SELECT updated FROM resolved_servers WHERE domain=?"); - $statement->bindParam(1, $domain); - $statement->execute(); - $result = $statement->fetchColumn(); - return new DateTime($result); - } - catch(Exception $e) - { - throw new DatabaseOperationException('Failed to get the updated date of a resolved server from the database', $e); - } - } - - /** - * Retrieves the resolved server record from the database for a given domain. - * - * @param string $domain The domain name for which to retrieve the resolved server record. - * @return ResolvedServerRecord|null The resolved server record associated with the given domain. - * @throws DatabaseOperationException If there is an error retrieving the resolved server record from the database. - * @throws \DateMalformedStringException If the date string is malformed. - */ - public static function getResolvedServer(string $domain): ?ResolvedServerRecord - { - try - { - $statement = Database::getConnection()->prepare("SELECT * FROM resolved_servers WHERE domain=?"); - $statement->bindParam(1, $domain); - $statement->execute(); - $result = $statement->fetch(); - - if($result === false) - { - return null; - } - - return ResolvedServerRecord::fromArray($result); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to get a resolved server from the database', $e); - } - } - - /** - * Adds or updates a resolved server in the database. - * - * @param string $domain The domain name of the resolved server. - * @param ResolvedServer $resolvedServer The resolved server object containing endpoint and public key. - * @return void - * @throws DatabaseOperationException If a database operation fails. - */ - public static function addResolvedServer(string $domain, ResolvedServer $resolvedServer): void - { - $endpoint = $resolvedServer->getEndpoint(); - $publicKey = $resolvedServer->getPublicKey(); - - if(self::resolvedServerExists($domain)) - { - try - { - $statement = Database::getConnection()->prepare("UPDATE resolved_servers SET endpoint=?, public_key=?, updated=NOW() WHERE domain=?"); - $statement->bindParam(1, $endpoint); - $statement->bindParam(2, $publicKey); - $statement->bindParam(3, $domain); - $statement->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to update a resolved server in the database', $e); - } - } - - try - { - $statement = Database::getConnection()->prepare("INSERT INTO resolved_servers (domain, endpoint, public_key) VALUES (?, ?, ?)"); - $statement->bindParam(1, $domain); - $statement->bindParam(2, $endpoint); - $statement->bindParam(3, $publicKey); - $statement->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to add a resolved server to the database', $e); - } - } -} \ No newline at end of file diff --git a/src/Socialbox/Managers/SessionManager.php b/src/Socialbox/Managers/SessionManager.php index 4111e8d..88e4b57 100644 --- a/src/Socialbox/Managers/SessionManager.php +++ b/src/Socialbox/Managers/SessionManager.php @@ -19,34 +19,27 @@ use Socialbox\Exceptions\StandardException; use Socialbox\Objects\Database\RegisteredPeerRecord; use Socialbox\Objects\Database\SessionRecord; + use Socialbox\Objects\KeyPair; use Symfony\Component\Uid\Uuid; class SessionManager { - /** - * Creates a new session with the given public key. - * - * @param string $publicKey The public key to associate with the new session. - * @param RegisteredPeerRecord $peer - * - * @throws InvalidArgumentException If the public key is empty or invalid. - * @throws DatabaseOperationException If there is an error while creating the session in the database. - */ - public static function createSession(string $publicKey, RegisteredPeerRecord $peer, string $clientName, string $clientVersion): string + public static function createSession(RegisteredPeerRecord $peer, string $clientName, string $clientVersion, string $clientPublicSigningKey, string $clientPublicEncryptionKey, KeyPair $serverEncryptionKeyPair): string { - if($publicKey === '') + if($clientPublicSigningKey === '' || Cryptography::validatePublicSigningKey($clientPublicSigningKey) === false) { - throw new InvalidArgumentException('The public key cannot be empty'); + throw new InvalidArgumentException('The public key is not a valid Ed25519 public key'); } - if(!Cryptography::validatePublicKey($publicKey)) + if($clientPublicEncryptionKey === '' || Cryptography::validatePublicEncryptionKey($clientPublicEncryptionKey) === false) { - throw new InvalidArgumentException('The given public key is invalid'); + throw new InvalidArgumentException('The public key is not a valid X25519 public key'); } $uuid = Uuid::v4()->toRfc4122(); $flags = []; + // TODO: Update this to support `host` peers if($peer->isEnabled()) { $flags[] = SessionFlags::AUTHENTICATION_REQUIRED; @@ -119,13 +112,18 @@ try { - $statement = Database::getConnection()->prepare("INSERT INTO sessions (uuid, peer_uuid, client_name, client_version, public_key, flags) VALUES (?, ?, ?, ?, ?, ?)"); - $statement->bindParam(1, $uuid); - $statement->bindParam(2, $peerUuid); - $statement->bindParam(3, $clientName); - $statement->bindParam(4, $clientVersion); - $statement->bindParam(5, $publicKey); - $statement->bindParam(6, $implodedFlags); + $statement = Database::getConnection()->prepare("INSERT INTO sessions (uuid, peer_uuid, client_name, client_version, client_public_signing_key, client_public_encryption_key, server_public_encryption_key, server_private_encryption_key, flags) VALUES (:uuid, :peer_uuid, :client_name, :client_version, :client_public_signing_key, :client_public_encryption_key, :server_public_encryption_key, :server_private_encryption_key, :flags)"); + $statement->bindParam(':uuid', $uuid); + $statement->bindParam(':peer_uuid', $peerUuid); + $statement->bindParam(':client_name', $clientName); + $statement->bindParam(':client_version', $clientVersion); + $statement->bindParam(':client_public_signing_key', $clientPublicSigningKey); + $statement->bindParam(':client_public_encryption_key', $clientPublicEncryptionKey); + $serverPublicEncryptionKey = $serverEncryptionKeyPair->getPublicKey(); + $statement->bindParam(':server_public_encryption_key', $serverPublicEncryptionKey); + $serverPrivateEncryptionKey = $serverEncryptionKeyPair->getPrivateKey(); + $statement->bindParam(':server_private_encryption_key', $serverPrivateEncryptionKey); + $statement->bindParam(':flags', $implodedFlags); $statement->execute(); } catch(PDOException $e) @@ -186,7 +184,6 @@ // Convert the timestamp fields to DateTime objects $data['created'] = new DateTime($data['created']); - if(isset($data['last_request']) && $data['last_request'] !== null) { $data['last_request'] = new DateTime($data['last_request']); @@ -205,53 +202,6 @@ } } - /** - * Update the authenticated peer associated with the given session UUID. - * - * @param string $uuid The UUID of the session to update. - * @param RegisteredPeerRecord|string $registeredPeerUuid - * @return void - * @throws DatabaseOperationException - */ - public static function updatePeer(string $uuid, RegisteredPeerRecord|string $registeredPeerUuid): void - { - if($registeredPeerUuid instanceof RegisteredPeerRecord) - { - $registeredPeerUuid = $registeredPeerUuid->getUuid(); - } - - Logger::getLogger()->verbose(sprintf("Assigning peer %s to session %s", $registeredPeerUuid, $uuid)); - - try - { - $statement = Database::getConnection()->prepare("UPDATE sessions SET peer_uuid=? WHERE uuid=?"); - $statement->bindParam(1, $registeredPeerUuid); - $statement->bindParam(2, $uuid); - $statement->execute(); - } - catch (PDOException $e) - { - throw new DatabaseOperationException('Failed to update authenticated peer', $e); - } - } - - public static function updateAuthentication(string $uuid, bool $authenticated): void - { - Logger::getLogger()->verbose(sprintf("Marking session %s as authenticated: %s", $uuid, $authenticated ? 'true' : 'false')); - - try - { - $statement = Database::getConnection()->prepare("UPDATE sessions SET authenticated=? WHERE uuid=?"); - $statement->bindParam(1, $authenticated); - $statement->bindParam(2, $uuid); - $statement->execute(); - } - catch (PDOException $e) - { - throw new DatabaseOperationException('Failed to update authenticated peer', $e); - } - } - /** * Updates the last request timestamp for a given session by its UUID. * @@ -305,24 +255,28 @@ } /** - * Updates the encryption key for the specified session. + * Updates the encryption keys and session state for a specific session UUID in the database. * - * @param string $uuid The unique identifier of the session for which the encryption key is to be set. - * @param string $encryptionKey The new encryption key to be assigned. + * @param string $uuid The unique identifier for the session to update. + * @param string $privateSharedSecret The private shared secret to secure communication. + * @param string $clientEncryptionKey The client's encryption key used for transport security. + * @param string $serverEncryptionKey The server's encryption key used for transport security. * @return void - * @throws DatabaseOperationException If the database operation fails. + * @throws DatabaseOperationException If an error occurs during the database operation. */ - public static function setEncryptionKey(string $uuid, string $encryptionKey): void + public static function setEncryptionKeys(string $uuid, string $privateSharedSecret, string $clientEncryptionKey, string $serverEncryptionKey): void { Logger::getLogger()->verbose(sprintf('Setting the encryption key for %s', $uuid)); try { $state_value = SessionState::ACTIVE->value; - $statement = Database::getConnection()->prepare('UPDATE sessions SET state=?, encryption_key=? WHERE uuid=?'); - $statement->bindParam(1, $state_value); - $statement->bindParam(2, $encryptionKey); - $statement->bindParam(3, $uuid); + $statement = Database::getConnection()->prepare('UPDATE sessions SET state=:state, private_shared_secret=:private_shared_secret, client_transport_encryption_key=:client_transport_encryption_key, server_transport_encryption_key=:server_transport_encryption_key WHERE uuid=:uuid'); + $statement->bindParam(':state', $state_value); + $statement->bindParam(':private_shared_secret', $privateSharedSecret); + $statement->bindParam(':client_transport_encryption_key', $clientEncryptionKey); + $statement->bindParam(':server_transport_encryption_key', $serverEncryptionKey); + $statement->bindParam(':uuid', $uuid); $statement->execute(); } diff --git a/src/Socialbox/Objects/ClientRequest.php b/src/Socialbox/Objects/ClientRequest.php index 00bd0fc..15e60cd 100644 --- a/src/Socialbox/Objects/ClientRequest.php +++ b/src/Socialbox/Objects/ClientRequest.php @@ -2,10 +2,7 @@ namespace Socialbox\Objects; - use InvalidArgumentException; use Socialbox\Classes\Cryptography; - use Socialbox\Classes\Utilities; - use Socialbox\Enums\SessionState; use Socialbox\Enums\StandardHeaders; use Socialbox\Enums\Types\RequestType; use Socialbox\Exceptions\CryptographyException; @@ -18,7 +15,7 @@ class ClientRequest { private array $headers; - private RequestType $requestType; + private ?RequestType $requestType; private ?string $requestBody; private ?string $clientName; @@ -27,6 +24,14 @@ private ?string $sessionUuid; private ?string $signature; + /** + * Initializes the instance with the provided request headers and optional request body. + * + * @param array $headers An associative array of request headers used to set properties such as client name, version, and others. + * @param string|null $requestBody The optional body of the request, or null if not provided. + * + * @return void + */ public function __construct(array $headers, ?string $requestBody) { $this->headers = $headers; @@ -34,17 +39,28 @@ $this->clientName = $headers[StandardHeaders::CLIENT_NAME->value] ?? null; $this->clientVersion = $headers[StandardHeaders::CLIENT_VERSION->value] ?? null; - $this->requestType = RequestType::from($headers[StandardHeaders::REQUEST_TYPE->value]); + $this->requestType = RequestType::tryFrom($headers[StandardHeaders::REQUEST_TYPE->value]); $this->identifyAs = $headers[StandardHeaders::IDENTIFY_AS->value] ?? null; $this->sessionUuid = $headers[StandardHeaders::SESSION_UUID->value] ?? null; $this->signature = $headers[StandardHeaders::SIGNATURE->value] ?? null; } + /** + * Retrieves the headers. + * + * @return array Returns an array of headers. + */ public function getHeaders(): array { return $this->headers; } + /** + * Checks if the specified header exists in the collection of headers. + * + * @param StandardHeaders|string $header The header to check, either as a StandardHeaders enum or a string. + * @return bool Returns true if the header exists, otherwise false. + */ public function headerExists(StandardHeaders|string $header): bool { if(is_string($header)) @@ -55,6 +71,12 @@ return isset($this->headers[$header->value]); } + /** + * Retrieves the value of a specified header. + * + * @param StandardHeaders|string $header The header to retrieve, provided as either a StandardHeaders enum or a string key. + * @return string|null Returns the header value if it exists, or null if the header does not exist. + */ public function getHeader(StandardHeaders|string $header): ?string { if(!$this->headerExists($header)) @@ -70,26 +92,51 @@ return $this->headers[$header->value]; } + /** + * Retrieves the request body. + * + * @return string|null Returns the request body as a string if available, or null if not set. + */ public function getRequestBody(): ?string { return $this->requestBody; } + /** + * Retrieves the name of the client. + * + * @return string|null Returns the client's name if set, or null if not available. + */ public function getClientName(): ?string { return $this->clientName; } + /** + * Retrieves the client version. + * + * @return string|null Returns the client version if available, or null if not set. + */ public function getClientVersion(): ?string { return $this->clientVersion; } - public function getRequestType(): RequestType + /** + * Retrieves the request type associated with the current instance. + * + * @return RequestType|null Returns the associated RequestType if available, or null if not set. + */ + public function getRequestType(): ?RequestType { return $this->requestType; } + /** + * Retrieves the peer address the instance identifies as. + * + * @return PeerAddress|null Returns a PeerAddress instance if the identification address is set, or null otherwise. + */ public function getIdentifyAs(): ?PeerAddress { if($this->identifyAs === null) @@ -100,11 +147,21 @@ return PeerAddress::fromAddress($this->identifyAs); } + /** + * Retrieves the UUID of the current session. + * + * @return string|null Returns the session UUID if available, or null if it is not set. + */ public function getSessionUuid(): ?string { return $this->sessionUuid; } + /** + * Retrieves the current session associated with the session UUID. + * + * @return SessionRecord|null Returns the associated SessionRecord if the session UUID exists, or null if no session UUID is set. + */ public function getSession(): ?SessionRecord { if($this->sessionUuid === null) @@ -115,6 +172,11 @@ return SessionManager::getSession($this->sessionUuid); } + /** + * Retrieves the associated peer for the current session. + * + * @return RegisteredPeerRecord|null Returns the associated RegisteredPeerRecord if available, or null if no session exists. + */ public function getPeer(): ?RegisteredPeerRecord { $session = $this->getSession(); @@ -127,11 +189,22 @@ return RegisteredPeerManager::getPeer($session->getPeerUuid()); } + /** + * Retrieves the signature value. + * + * @return string|null The signature value or null if not set + */ public function getSignature(): ?string { return $this->signature; } + /** + * Verifies the signature of the provided decrypted content. + * + * @param string $decryptedContent The decrypted content to verify the signature against. + * @return bool True if the signature is valid, false otherwise. + */ private function verifySignature(string $decryptedContent): bool { if($this->getSignature() == null || $this->getSessionUuid() == null) @@ -141,7 +214,11 @@ try { - return Cryptography::verifyContent($decryptedContent, $this->getSignature(), $this->getSession()->getPublicKey(), true); + return Cryptography::verifyMessage( + message: $decryptedContent, + signature: $this->getSignature(), + publicKey: $this->getSession()->getClientPublicSigningKey() + ); } catch(CryptographyException) { @@ -156,52 +233,12 @@ * @return RpcRequest[] The parsed RpcRequest objects * @throws RequestException Thrown if the request is invalid */ - public function getRpcRequests(): array + public function getRpcRequests(string $json): array { - if($this->getSessionUuid() === null) + $body = json_decode($json, true); + if($body === false) { - throw new RequestException("Session UUID required", 400); - } - - // Get the existing session - $session = $this->getSession(); - - // If we're awaiting a DHE, encryption is not possible at this point - if($session->getState() === SessionState::AWAITING_DHE) - { - throw new RequestException("DHE exchange required", 400); - } - - // If the session is not active, we can't serve these requests - if($session->getState() !== SessionState::ACTIVE) - { - throw new RequestException("Session is not active", 400); - } - - // Attempt to decrypt the content and verify the signature of the request - try - { - $decrypted = Cryptography::decryptTransport($this->requestBody, $session->getEncryptionKey()); - - if(!$this->verifySignature($decrypted)) - { - throw new RequestException("Invalid request signature", 401); - } - } - catch (CryptographyException $e) - { - throw new RequestException("Failed to decrypt request body", 400, $e); - } - - // At this stage, all checks has passed; we can try parsing the RPC request - try - { - // Decode the request body - $body = Utilities::jsonDecode($decrypted); - } - catch(InvalidArgumentException $e) - { - throw new RequestException("Invalid JSON in request body: " . $e->getMessage(), 400, $e); + throw new RequestException('Malformed JSON', 400); } // If the body only contains a method, we assume it's a single request diff --git a/src/Socialbox/Objects/Database/DecryptedRecord.php b/src/Socialbox/Objects/Database/DecryptedRecord.php deleted file mode 100644 index 3757949..0000000 --- a/src/Socialbox/Objects/Database/DecryptedRecord.php +++ /dev/null @@ -1,41 +0,0 @@ -key = $data['key']; - $this->pepper = $data['pepper']; - $this->salt = $data['salt']; - } - - /** - * @return string - */ - public function getKey(): string - { - return $this->key; - } - - /** - * @return string - */ - public function getPepper(): string - { - return $this->pepper; - } - - /** - * @return string - */ - public function getSalt(): string - { - return $this->salt; - } - } \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/EncryptionRecord.php b/src/Socialbox/Objects/Database/EncryptionRecord.php deleted file mode 100644 index 707c39d..0000000 --- a/src/Socialbox/Objects/Database/EncryptionRecord.php +++ /dev/null @@ -1,83 +0,0 @@ -data = $data['data']; - $this->iv = $data['iv']; - $this->tag = $data['tag']; - } - - /** - * Retrieves the stored data. - * - * @return string The stored data. - */ - public function getData(): string - { - return $this->data; - } - - /** - * Retrieves the initialization vector (IV). - * - * @return string The initialization vector. - */ - public function getIv(): string - { - return $this->iv; - } - - /** - * Retrieves the tag. - * - * @return string The tag. - */ - public function getTag(): string - { - return $this->tag; - } - - /** - * Decrypts the encrypted record using available encryption keys. - * - * Iterates through the configured encryption keys to attempt decryption of the data. - * If successful, returns a DecryptedRecord object with the decrypted data. - * Throws an exception if decryption fails with all available keys. - * - * @return DecryptedRecord The decrypted record containing the original data. - * @throws CryptographyException If decryption fails with all provided keys. - */ - public function decrypt(): DecryptedRecord - { - foreach(Configuration::getInstanceConfiguration()->getEncryptionKeys() as $encryptionKey) - { - $decryptedVault = openssl_decrypt(base64_decode($this->data), SecuredPassword::ENCRYPTION_ALGORITHM, - $encryptionKey, OPENSSL_RAW_DATA, base64_decode($this->iv), base64_decode($this->tag) - ); - - if ($decryptedVault !== false) - { - return new DecryptedRecord(json_decode($decryptedVault, true)); - } - } - - throw new CryptographyException("Decryption failed"); - } - } \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/ResolvedServerRecord.php b/src/Socialbox/Objects/Database/ResolvedServerRecord.php index 12a31fb..5488c87 100644 --- a/src/Socialbox/Objects/Database/ResolvedServerRecord.php +++ b/src/Socialbox/Objects/Database/ResolvedServerRecord.php @@ -1,110 +1,144 @@ domain = (string)$data['domain']; - $this->endpoint = (string)$data['endpoint']; - $this->publicKey = (string)$data['public_key']; + private string $domain; + private string $endpoint; + private string $publicKey; + private DateTime $expires; + private DateTime $updated; - if(is_null($data['updated'])) + /** + * Constructs a new instance of the class. + * + * @param array $data An associative array containing the domain, endpoint, public_key, and updated values. + * @throws \DateMalformedStringException + */ + public function __construct(array $data) { - $this->updated = new DateTime(); + $this->domain = (string)$data['domain']; + $this->endpoint = (string)$data['endpoint']; + $this->publicKey = (string)$data['public_key']; + + if(is_null($data['expires'])) + { + $this->expires = new DateTime(); + } + elseif (is_int($data['expires'])) + { + $this->expires = (new DateTime())->setTimestamp($data['expires']); + } + elseif (is_string($data['expires'])) + { + $this->expires = new DateTime($data['expires']); + } + else + { + $this->expires = $data['expires']; + } + + if(is_null($data['updated'])) + { + $this->updated = new DateTime(); + } + elseif (is_int($data['updated'])) + { + $this->updated = (new DateTime())->setTimestamp($data['updated']); + } + elseif (is_string($data['updated'])) + { + $this->updated = new DateTime($data['updated']); + } + else + { + $this->updated = $data['updated']; + } } - elseif (is_string($data['updated'])) + + /** + * Retrieves the domain value. + * + * @return string The domain as a string. + */ + public function getDomain(): string { - $this->updated = new DateTime($data['updated']); + return $this->domain; } - else + + /** + * Retrieves the configured endpoint. + * + * @return string The endpoint as a string. + */ + public function getEndpoint(): string { - $this->updated = $data['updated']; + return $this->endpoint; } - } - /** - * - * @return string The domain value. - */ - public function getDomain(): string - { - return $this->domain; - } + /** + * Retrieves the public key. + * + * @return string The public key as a string. + */ + public function getPublicKey(): string + { + return $this->publicKey; + } - /** - * - * @return string The endpoint value. - */ - public function getEndpoint(): string - { - return $this->endpoint; - } + /** + * Retrieves the expiration timestamp. + * + * @return DateTime The DateTime object representing the expiration time. + */ + public function getExpires(): DateTime + { + return $this->expires; + } - /** - * - * @return string The public key. - */ - public function getPublicKey(): string - { - return $this->publicKey; - } + /** + * Retrieves the timestamp of the last update. + * + * @return DateTime The DateTime object representing the last update time. + */ + public function getUpdated(): DateTime + { + return $this->updated; + } - /** - * Retrieves the timestamp of the last update. - * - * @return DateTime The DateTime object representing the last update time. - */ - public function getUpdated(): DateTime - { - return $this->updated; - } + /** + * Fetches the DNS record based on the provided endpoint, public key, and expiration time. + * + * @return DnsRecord An instance of the DnsRecord containing the endpoint, public key, and expiration timestamp. + */ + public function getDnsRecord(): DnsRecord + { + return new DnsRecord($this->endpoint, $this->publicKey, $this->expires->getTimestamp()); + } - /** - * Converts the record to a ResolvedServer object. - * - * @return ResolvedServer The ResolvedServer object. - */ - public function toResolvedServer(): ResolvedServer - { - return new ResolvedServer($this->endpoint, $this->publicKey); - } + /** + * @inheritDoc + */ + public static function fromArray(array $data): object + { + return new self($data); + } - /** - * @inheritDoc - * @throws \DateMalformedStringException - */ - public static function fromArray(array $data): object - { - return new self($data); - } - - /** - * @inheritDoc - */ - public function toArray(): array - { - return [ - 'domain' => $this->domain, - 'endpoint' => $this->endpoint, - 'public_key' => $this->publicKey, - 'updated' => $this->updated->format('Y-m-d H:i:s') - ]; - } -} \ No newline at end of file + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'domain' => $this->domain, + 'endpoint' => $this->endpoint, + 'public_key' => $this->publicKey, + 'updated' => $this->updated->format('Y-m-d H:i:s') + ]; + } + } \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/SecurePasswordRecord.php b/src/Socialbox/Objects/Database/SecurePasswordRecord.php deleted file mode 100644 index 239fafa..0000000 --- a/src/Socialbox/Objects/Database/SecurePasswordRecord.php +++ /dev/null @@ -1,108 +0,0 @@ -peerUuid = $data['peer_uuid']; - $this->iv = $data['iv']; - $this->encryptedPassword = $data['encrypted_password']; - $this->encryptedTag = $data['encrypted_tag']; - - if($data['updated'] instanceof DateTime) - { - $this->updated = $data['updated']; - } - else - { - $this->updated = new DateTime($data['updated']); - } - } - - /** - * Retrieves the UUID of the peer. - * - * @return string The UUID of the peer. - */ - public function getPeerUuid(): string - { - return $this->peerUuid; - } - - /** - * Retrieves the initialization vector (IV) value. - * - * @return string The initialization vector. - */ - public function getIv(): string - { - return $this->iv; - } - - /** - * Retrieves the encrypted password. - * - * @return string The encrypted password. - */ - public function getEncryptedPassword(): string - { - return $this->encryptedPassword; - } - - /** - * Retrieves the encrypted tag. - * - * @return string The encrypted tag. - */ - public function getEncryptedTag(): string - { - return $this->encryptedTag; - } - - /** - * Retrieves the updated timestamp. - * - * @return DateTime The updated timestamp. - */ - public function getUpdated(): DateTime - { - return $this->updated; - } - - public function toArray(): array - { - return [ - 'peer_uuid' => $this->peerUuid, - 'iv' => $this->iv, - 'encrypted_password' => $this->encryptedPassword, - 'encrypted_tag' => $this->encryptedTag, - 'updated' => $this->updated->format('Y-m-d H:i:s') - ]; - } - - public static function fromArray(array $data): SecurePasswordRecord - { - return new SecurePasswordRecord($data); - } - } \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/SessionRecord.php b/src/Socialbox/Objects/Database/SessionRecord.php index 77d75df..ec21c7c 100644 --- a/src/Socialbox/Objects/Database/SessionRecord.php +++ b/src/Socialbox/Objects/Database/SessionRecord.php @@ -15,9 +15,13 @@ private string $clientName; private string $clientVersion; private bool $authenticated; - private string $publicKey; + private string $clientPublicSigningKey; + public string $clientPublicEncryptionKey; + private string $serverPublicEncryptionKey; + private string $serverPrivateEncryptionKey; + private ?string $clientTransportEncryptionKey; + private ?string $serverTransportEncryptionKey; private SessionState $state; - private ?string $encryptionKey; /** * @var SessionFlags[] */ @@ -42,10 +46,14 @@ $this->clientName = $data['client_name']; $this->clientVersion = $data['client_version']; $this->authenticated = $data['authenticated'] ?? false; - $this->publicKey = $data['public_key']; + $this->clientPublicSigningKey = $data['client_public_signing_key']; + $this->clientPublicEncryptionKey = $data['client_public_encryption_key']; + $this->serverPublicEncryptionKey = $data['server_public_encryption_key']; + $this->serverPrivateEncryptionKey = $data['server_private_encryption_key']; + $this->clientTransportEncryptionKey = $data['client_transport_encryption_key'] ?? null; + $this->serverTransportEncryptionKey = $data['server_transport_encryption_key'] ?? null; $this->created = $data['created']; $this->lastRequest = $data['last_request']; - $this->encryptionKey = $data['encryption_key'] ?? null; $this->flags = SessionFlags::fromString($data['flags']); if(SessionState::tryFrom($data['state']) == null) @@ -99,9 +107,55 @@ * * @return string Returns the public key as a string. */ - public function getPublicKey(): string + public function getClientPublicSigningKey(): string { - return $this->publicKey; + return $this->clientPublicSigningKey; + } + + /** + * Retrieves the encryption key associated with the instance. + * + * @return string|null Returns the encryption key as a string, or null if not set. + */ + public function getClientPublicEncryptionKey(): ?string + { + return $this->clientPublicEncryptionKey; + } + + /** + * @return string + */ + public function getServerPublicEncryptionKey(): string + { + return $this->serverPublicEncryptionKey; + } + + /** + * @return string + */ + public function getServerPrivateEncryptionKey(): string + { + return $this->serverPrivateEncryptionKey; + } + + /** + * Retrieves the client encryption key associated with the instance. + * + * @return string|null Returns the client encryption key as a string, or null if not set. + */ + public function getClientTransportEncryptionKey(): ?string + { + return $this->clientTransportEncryptionKey; + } + + /** + * Retrieves the server encryption key associated with the instance. + * + * @return string|null Returns the server encryption key as a string, or null if not set. + */ + public function getServerTransportEncryptionKey(): ?string + { + return $this->serverTransportEncryptionKey; } /** @@ -114,16 +168,6 @@ return $this->state; } - /** - * Retrieves the encryption key associated with the instance. - * - * @return string|null Returns the encryption key as a string. - */ - public function getEncryptionKey(): ?string - { - return $this->encryptionKey; - } - /** * Retrieves the creation date and time of the object. * @@ -194,6 +238,11 @@ return $this->clientVersion; } + /** + * Converts the current session state into a standard session state object. + * + * @return \Socialbox\Objects\Standard\SessionState The standardized session state object. + */ public function toStandardSessionState(): \Socialbox\Objects\Standard\SessionState { return new \Socialbox\Objects\Standard\SessionState([ @@ -207,10 +256,7 @@ /** - * Creates a new instance of the class using the provided array data. - * - * @param array $data An associative array of data used to initialize the object properties. - * @return object Returns a newly created object instance. + * @inheritDoc */ public static function fromArray(array $data): object { @@ -218,10 +264,7 @@ } /** - * Converts the object's properties to an associative array. - * - * @return array An associative array representing the object's data, including keys 'uuid', 'peer_uuid', - * 'authenticated', 'public_key', 'state', 'flags', 'created', and 'last_request'. + * @inheritDoc */ public function toArray(): array { @@ -229,7 +272,12 @@ 'uuid' => $this->uuid, 'peer_uuid' => $this->peerUuid, 'authenticated' => $this->authenticated, - 'public_key' => $this->publicKey, + 'client_public_signing_key' => $this->clientPublicSigningKey, + 'client_public_encryption_key' => $this->clientPublicEncryptionKey, + 'server_public_encryption_key' => $this->serverPublicEncryptionKey, + 'server_private_encryption_key' => $this->serverPrivateEncryptionKey, + 'client_transport_encryption_key' => $this->clientTransportEncryptionKey, + 'server_transport_encryption_key' => $this->serverTransportEncryptionKey, 'state' => $this->state->value, 'flags' => SessionFlags::toString($this->flags), 'created' => $this->created, diff --git a/src/Socialbox/Objects/DnsRecord.php b/src/Socialbox/Objects/DnsRecord.php new file mode 100644 index 0000000..6a1ca33 --- /dev/null +++ b/src/Socialbox/Objects/DnsRecord.php @@ -0,0 +1,67 @@ +rpcEndpoint = $rpcEndpoint; + $this->publicSigningKey = $publicSigningKey; + $this->expires = $expires; + } + + /** + * Retrieves the RPC endpoint. + * + * @return string The RPC endpoint. + */ + public function getRpcEndpoint(): string + { + return $this->rpcEndpoint; + } + + /** + * Retrieves the public signing key. + * + * @return string Returns the public signing key as a string. + */ + public function getPublicSigningKey(): string + { + return $this->publicSigningKey; + } + + /** + * Retrieves the expiration time. + * + * @return int The expiration timestamp as an integer. + */ + public function getExpires(): int + { + return $this->expires; + } + + /** + * Creates a new instance of DnsRecord from the provided array of data. + * + * @param array $data An associative array containing the keys 'rpc_endpoint', 'public_key', and 'expires' + * required to instantiate a DnsRecord object. + * @return DnsRecord Returns a new DnsRecord instance populated with the data from the array. + */ + public static function fromArray(array $data): DnsRecord + { + return new DnsRecord($data['rpc_endpoint'], $data['public_key'], $data['expires']); + } + } \ No newline at end of file diff --git a/src/Socialbox/Objects/ExportedSession.php b/src/Socialbox/Objects/ExportedSession.php index e1ca7be..84be108 100644 --- a/src/Socialbox/Objects/ExportedSession.php +++ b/src/Socialbox/Objects/ExportedSession.php @@ -2,49 +2,63 @@ namespace Socialbox\Objects; + use Socialbox\Interfaces\SerializableInterface; + /** * Represents an exported session containing cryptographic keys, identifiers, and endpoints. */ - class ExportedSession + class ExportedSession implements SerializableInterface { private string $peerAddress; - private string $privateKey; - private string $publicKey; - private string $encryptionKey; - private string $serverPublicKey; private string $rpcEndpoint; - private string $sessionUuid; + private string $sessionUUID; + private string $transportEncryptionAlgorithm; + private int $serverKeypairExpires; + private string $serverPublicSigningKey; + private string $serverPublicEncryptionKey; + private string $clientPublicSigningKey; + private string $clientPrivateSigningKey; + private string $clientPublicEncryptionKey; + private string $clientPrivateEncryptionKey; + private string $privateSharedSecret; + private string $clientTransportEncryptionKey; + private string $serverTransportEncryptionKey; /** - * Initializes a new instance of the class with the provided data. + * Constructor method to initialize class properties from the provided data array. * - * @param array $data An associative array containing the configuration data. - * Expected keys: - * - 'peer_address': The address of the peer. - * - 'private_key': The private key for secure communication. - * - 'public_key': The public key for secure communication. - * - 'encryption_key': The encryption key used for communication. - * - 'server_public_key': The server's public key. - * - 'rpc_endpoint': The RPC endpoint for network communication. - * - 'session_uuid': The unique identifier for the session. + * @param array $data Associative array containing the required properties such as: + * 'peer_address', 'rpc_endpoint', 'session_uuid', + * 'server_public_signing_key', 'server_public_encryption_key', + * 'client_public_signing_key', 'client_private_signing_key', + * 'client_public_encryption_key', 'client_private_encryption_key', + * 'private_shared_secret', 'client_transport_encryption_key', + * 'server_transport_encryption_key'. * * @return void */ public function __construct(array $data) { $this->peerAddress = $data['peer_address']; - $this->privateKey = $data['private_key']; - $this->publicKey = $data['public_key']; - $this->encryptionKey = $data['encryption_key']; - $this->serverPublicKey = $data['server_public_key']; $this->rpcEndpoint = $data['rpc_endpoint']; - $this->sessionUuid = $data['session_uuid']; + $this->sessionUUID = $data['session_uuid']; + $this->transportEncryptionAlgorithm = $data['transport_encryption_algorithm']; + $this->serverKeypairExpires = $data['server_keypair_expires']; + $this->serverPublicSigningKey = $data['server_public_signing_key']; + $this->serverPublicEncryptionKey = $data['server_public_encryption_key']; + $this->clientPublicSigningKey = $data['client_public_signing_key']; + $this->clientPrivateSigningKey = $data['client_private_signing_key']; + $this->clientPublicEncryptionKey = $data['client_public_encryption_key']; + $this->clientPrivateEncryptionKey = $data['client_private_encryption_key']; + $this->privateSharedSecret = $data['private_shared_secret']; + $this->clientTransportEncryptionKey = $data['client_transport_encryption_key']; + $this->serverTransportEncryptionKey = $data['server_transport_encryption_key']; } /** - * Retrieves the address of the peer. + * Retrieves the peer address associated with the current instance. * - * @return string The peer's address as a string. + * @return string The peer address. */ public function getPeerAddress(): string { @@ -52,47 +66,7 @@ } /** - * Retrieves the private key. - * - * @return string The private key. - */ - public function getPrivateKey(): string - { - return $this->privateKey; - } - - /** - * Retrieves the public key. - * - * @return string The public key. - */ - public function getPublicKey(): string - { - return $this->publicKey; - } - - /** - * Retrieves the encryption key. - * - * @return string The encryption key. - */ - public function getEncryptionKey(): string - { - return $this->encryptionKey; - } - - /** - * Retrieves the public key of the server. - * - * @return string The server's public key. - */ - public function getServerPublicKey(): string - { - return $this->serverPublicKey; - } - - /** - * Retrieves the RPC endpoint URL. + * Retrieves the RPC endpoint. * * @return string The RPC endpoint. */ @@ -102,38 +76,150 @@ } /** - * Retrieves the unique identifier for the current session. + * Retrieves the session UUID associated with the current instance. * * @return string The session UUID. */ - public function getSessionUuid(): string + public function getSessionUUID(): string { - return $this->sessionUuid; + return $this->sessionUUID; } /** - * Converts the current instance into an array representation. + * Retrieves the transport encryption algorithm being used. * - * @return array An associative array containing the instance properties and their respective values. + * @return string The transport encryption algorithm. + */ + public function getTransportEncryptionAlgorithm(): string + { + return $this->transportEncryptionAlgorithm; + } + + /** + * Retrieves the expiration time of the server key pair. + * + * @return int The expiration timestamp of the server key pair. + */ + public function getServerKeypairExpires(): int + { + return $this->serverKeypairExpires; + } + + /** + * Retrieves the public signing key of the server. + * + * @return string The server's public signing key. + */ + public function getServerPublicSigningKey(): string + { + return $this->serverPublicSigningKey; + } + + /** + * Retrieves the server's public encryption key. + * + * @return string The server's public encryption key. + */ + public function getServerPublicEncryptionKey(): string + { + return $this->serverPublicEncryptionKey; + } + + /** + * Retrieves the client's public signing key. + * + * @return string The client's public signing key. + */ + public function getClientPublicSigningKey(): string + { + return $this->clientPublicSigningKey; + } + + /** + * Retrieves the client's private signing key. + * + * @return string The client's private signing key. + */ + public function getClientPrivateSigningKey(): string + { + return $this->clientPrivateSigningKey; + } + + /** + * Retrieves the public encryption key of the client. + * + * @return string The client's public encryption key. + */ + public function getClientPublicEncryptionKey(): string + { + return $this->clientPublicEncryptionKey; + } + + /** + * Retrieves the client's private encryption key. + * + * @return string The client's private encryption key. + */ + public function getClientPrivateEncryptionKey(): string + { + return $this->clientPrivateEncryptionKey; + } + + /** + * Retrieves the private shared secret associated with the current instance. + * + * @return string The private shared secret. + */ + public function getPrivateSharedSecret(): string + { + return $this->privateSharedSecret; + } + + /** + * Retrieves the client transport encryption key. + * + * @return string The client transport encryption key. + */ + public function getClientTransportEncryptionKey(): string + { + return $this->clientTransportEncryptionKey; + } + + /** + * Retrieves the server transport encryption key associated with the current instance. + * + * @return string The server transport encryption key. + */ + public function getServerTransportEncryptionKey(): string + { + return $this->serverTransportEncryptionKey; + } + + /** + * @inheritDoc */ public function toArray(): array { return [ 'peer_address' => $this->peerAddress, - 'private_key' => $this->privateKey, - 'public_key' => $this->publicKey, - 'encryption_key' => $this->encryptionKey, - 'server_public_key' => $this->serverPublicKey, 'rpc_endpoint' => $this->rpcEndpoint, - 'session_uuid' => $this->sessionUuid + 'session_uuid' => $this->sessionUUID, + 'transport_encryption_algorithm' => $this->transportEncryptionAlgorithm, + 'server_keypair_expires' => $this->serverKeypairExpires, + 'server_public_signing_key' => $this->serverPublicSigningKey, + 'server_public_encryption_key' => $this->serverPublicEncryptionKey, + 'client_public_signing_key' => $this->clientPublicSigningKey, + 'client_private_signing_key' => $this->clientPrivateSigningKey, + 'client_public_encryption_key' => $this->clientPublicEncryptionKey, + 'client_private_encryption_key' => $this->clientPrivateEncryptionKey, + 'private_shared_secret' => $this->privateSharedSecret, + 'client_transport_encryption_key' => $this->clientTransportEncryptionKey, + 'server_transport_encryption_key' => $this->serverTransportEncryptionKey, ]; } /** - * Creates an instance of ExportedSession from the provided array. - * - * @param array $data The input data used to construct the ExportedSession instance. - * @return ExportedSession The new ExportedSession instance created from the given data. + * @inheritDoc */ public static function fromArray(array $data): ExportedSession { diff --git a/src/Socialbox/Objects/ResolvedServer.php b/src/Socialbox/Objects/ResolvedServer.php index 4ecd4e5..8683aa6 100644 --- a/src/Socialbox/Objects/ResolvedServer.php +++ b/src/Socialbox/Objects/ResolvedServer.php @@ -1,25 +1,11 @@ endpoint = $endpoint; - $this->publicKey = $publicKey; - } - - public function getEndpoint(): string - { - return $this->endpoint; - } - - public function getPublicKey(): string - { - return $this->publicKey; - } -} \ No newline at end of file + private DnsRecord $dnsRecord; + private ServerInformation $serverInformation; + } \ No newline at end of file diff --git a/src/Socialbox/Objects/ResolvedServerOld.php b/src/Socialbox/Objects/ResolvedServerOld.php new file mode 100644 index 0000000..4ecd4e5 --- /dev/null +++ b/src/Socialbox/Objects/ResolvedServerOld.php @@ -0,0 +1,25 @@ +endpoint = $endpoint; + $this->publicKey = $publicKey; + } + + public function getEndpoint(): string + { + return $this->endpoint; + } + + public function getPublicKey(): string + { + return $this->publicKey; + } +} \ No newline at end of file diff --git a/src/Socialbox/Objects/Standard/ServerInformation.php b/src/Socialbox/Objects/Standard/ServerInformation.php new file mode 100644 index 0000000..101d33a --- /dev/null +++ b/src/Socialbox/Objects/Standard/ServerInformation.php @@ -0,0 +1,75 @@ +serverName = $data['server_name']; + $this->serverKeypairExpires = $data['server_keypair_expires']; + $this->transportEncryptionAlgorithm = $data['transport_encryption_algorithm']; + } + + /** + * Retrieves the name of the server. + * + * @return string The server name. + */ + public function getServerName(): string + { + return $this->serverName; + } + + /** + * Retrieves the expiration time of the server key pair. + * + * @return int The expiration timestamp of the server key pair. + */ + public function getServerKeypairExpires(): int + { + return $this->serverKeypairExpires; + } + + /** + * Retrieves the transport encryption algorithm being used. + * + * @return string The transport encryption algorithm. + */ + public function getTransportEncryptionAlgorithm(): string + { + return $this->transportEncryptionAlgorithm; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): ServerInformation + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'server_name' => $this->serverName, + 'server_keypair_expires' => $this->serverKeypairExpires, + 'transport_encryption_algorithm' => $this->transportEncryptionAlgorithm, + ]; + } + } \ No newline at end of file diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index 1cde47b..3895268 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -6,6 +6,7 @@ use InvalidArgumentException; use Socialbox\Classes\Configuration; use Socialbox\Classes\Cryptography; + use Socialbox\Classes\DnsHelper; use Socialbox\Classes\Logger; use Socialbox\Classes\ServerResolver; use Socialbox\Classes\Utilities; @@ -16,6 +17,7 @@ use Socialbox\Enums\StandardHeaders; use Socialbox\Enums\StandardMethods; use Socialbox\Enums\Types\RequestType; + use Socialbox\Exceptions\CryptographyException; use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\RequestException; use Socialbox\Exceptions\StandardException; @@ -23,34 +25,37 @@ use Socialbox\Managers\SessionManager; use Socialbox\Objects\ClientRequest; use Socialbox\Objects\PeerAddress; + use Socialbox\Objects\Standard\ServerInformation; + use Throwable; class Socialbox { /** - * Handles incoming client requests by validating required headers and processing - * the request based on its type. The method ensures proper handling of - * specific request types like RPC, session initiation, and DHE exchange, - * while returning an appropriate HTTP response for invalid or missing data. + * Handles incoming client requests by parsing request headers, determining the request type, + * and routing the request to the appropriate handler method. Implements error handling for + * missing or invalid request types. * * @return void */ public static function handleRequest(): void { $requestHeaders = Utilities::getRequestHeaders(); - if(!isset($requestHeaders[StandardHeaders::REQUEST_TYPE->value])) { - http_response_code(400); - print('Missing required header: ' . StandardHeaders::REQUEST_TYPE->value); + self::returnError(400, StandardError::BAD_REQUEST, 'Missing required header: ' . StandardHeaders::REQUEST_TYPE->value); return; } $clientRequest = new ClientRequest($requestHeaders, file_get_contents('php://input') ?? null); - // Handle the request type, only `init` and `dhe` are not encrypted using the session's encrypted key + // Handle the request type, only `init` and `dhe` are not encrypted using the session's encrypted key // RPC Requests must be encrypted and signed by the client, vice versa for server responses. - switch(RequestType::tryFrom($clientRequest->getHeader(StandardHeaders::REQUEST_TYPE))) + switch($clientRequest->getRequestType()) { + case RequestType::INFO: + self::handleInformationRequest(); + break; + case RequestType::INITIATE_SESSION: self::handleInitiateSession($clientRequest); break; @@ -64,58 +69,66 @@ break; default: - http_response_code(400); - print('Invalid Request-Type header'); - break; + self::returnError(400, StandardError::BAD_REQUEST, 'Invalid Request-Type header'); } } /** - * Validates the headers in an initialization request to ensure that all - * required information is present and properly formatted. This includes - * checking for headers such as Client Name, Client Version, Public Key, - * and Identify-As, as well as validating the Identify-As header value. - * If any validation fails, a corresponding HTTP response code and message - * are returned. + * Handles an information request by setting the appropriate HTTP response code, + * content type headers, and printing the server information in JSON format. * - * @param ClientRequest $clientRequest The client request containing headers to validate. + * @return void + */ + private static function handleInformationRequest(): void + { + http_response_code(200); + header('Content-Type: application/json'); + Logger::getLogger()->info(json_encode(self::getServerInformation()->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + print(json_encode(self::getServerInformation()->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + } + + /** + * Validates the initial headers of a client request to ensure all required headers exist + * and contain valid values. If any validation fails, an error response is returned. * + * @param ClientRequest $clientRequest The client request containing headers to be validated. * @return bool Returns true if all required headers are valid, otherwise false. */ private static function validateInitHeaders(ClientRequest $clientRequest): bool { if(!$clientRequest->getClientName()) { - http_response_code(400); - print('Missing required header: ' . StandardHeaders::CLIENT_NAME->value); + self::returnError(400, StandardError::BAD_REQUEST, 'Missing required header: ' . StandardHeaders::CLIENT_NAME->value); return false; } if(!$clientRequest->getClientVersion()) { - http_response_code(400); - print('Missing required header: ' . StandardHeaders::CLIENT_VERSION->value); + self::returnError(400, StandardError::BAD_REQUEST, 'Missing required header: ' . StandardHeaders::CLIENT_VERSION->value); return false; } - if(!$clientRequest->headerExists(StandardHeaders::PUBLIC_KEY)) + if(!$clientRequest->headerExists(StandardHeaders::SIGNING_PUBLIC_KEY)) { - http_response_code(400); - print('Missing required header: ' . StandardHeaders::PUBLIC_KEY->value); + self::returnError(400, StandardError::BAD_REQUEST, 'Missing required header: ' . StandardHeaders::SIGNING_PUBLIC_KEY->value); + return false; + } + + if(!$clientRequest->headerExists(StandardHeaders::ENCRYPTION_PUBLIC_KEY)) + { + self::returnError(400, StandardError::BAD_REQUEST, 'Missing required header: ' . StandardHeaders::ENCRYPTION_PUBLIC_KEY->value); return false; } if(!$clientRequest->headerExists(StandardHeaders::IDENTIFY_AS)) { - http_response_code(400); - print('Missing required header: ' . StandardHeaders::IDENTIFY_AS->value); + self::returnError(400, StandardError::BAD_REQUEST, 'Missing required header: ' . StandardHeaders::IDENTIFY_AS->value); return false; } if(!Validator::validatePeerAddress($clientRequest->getHeader(StandardHeaders::IDENTIFY_AS))) { - http_response_code(400); - print('Invalid Identify-As header: ' . $clientRequest->getHeader(StandardHeaders::IDENTIFY_AS)); + self::returnError(400, StandardError::BAD_REQUEST, 'Invalid Identify-As header: ' . $clientRequest->getHeader(StandardHeaders::IDENTIFY_AS)); return false; } @@ -123,24 +136,25 @@ } /** - * Processes a client request to initiate a session. Validates required headers, - * ensures the peer is authorized and enabled, and creates a new session UUID - * if all checks pass. Handles edge cases like missing headers, invalid inputs, - * or unauthorized peers. + * Handles the initiation of a session for a client request. This involves validating headers, + * verifying peer identities, resolving domains, registering peers if necessary, and finally + * creating a session while providing the required session UUID as a response. * - * @param ClientRequest $clientRequest The request from the client containing - * the required headers and information. + * @param ClientRequest $clientRequest The incoming client request containing all necessary headers + * and identification information required to initiate the session. * @return void */ private static function handleInitiateSession(ClientRequest $clientRequest): void { + // This is only called for the `init` request type if(!self::validateInitHeaders($clientRequest)) { return; } // We always accept the client's public key at first - $publicKey = $clientRequest->getHeader(StandardHeaders::PUBLIC_KEY); + $clientPublicSigningKey = $clientRequest->getHeader(StandardHeaders::SIGNING_PUBLIC_KEY); + $clientPublicEncryptionKey = $clientRequest->getHeader(StandardHeaders::ENCRYPTION_PUBLIC_KEY); // If the peer is identifying as the same domain if($clientRequest->getIdentifyAs()->getDomain() === Configuration::getInstanceConfiguration()->getDomain()) @@ -148,9 +162,8 @@ // Prevent the peer from identifying as the host unless it's coming from an external domain if($clientRequest->getIdentifyAs()->getUsername() === ReservedUsernames::HOST->value) { - http_response_code(403); - print('Unauthorized: The requested peer is not allowed to identify as the host'); - return; + self::returnError(403, StandardError::FORBIDDEN, 'Unauthorized: Not allowed to identify as the host'); + return; } } // If the peer is identifying as an external domain @@ -159,64 +172,49 @@ // Only allow the host to identify as an external peer if($clientRequest->getIdentifyAs()->getUsername() !== ReservedUsernames::HOST->value) { - http_response_code(403); - print('Unauthorized: The requested peer is not allowed to identify as an external peer'); + self::returnError(403, StandardError::FORBIDDEN, 'Forbidden: Any external peer must identify as the host, only the host can preform actions on behalf of it\'s peers'); return; } try { - // We need to obtain the public key of the host, since we can't trust the client + // We need to obtain the public key of the host, since we can't trust the client (Use database) $resolvedServer = ServerResolver::resolveDomain($clientRequest->getIdentifyAs()->getDomain()); - // Override the public key with the resolved server's public key - $publicKey = $resolvedServer->getPublicKey(); - } - catch (Exceptions\ResolutionException $e) - { - Logger::getLogger()->error('Failed to resolve the host domain', $e); - http_response_code(409); - print('Conflict: Failed to resolve the host domain: ' . $e->getMessage()); - return; + // Override the public signing key with the resolved server's public key + // Encryption key can be left as is. + $clientPublicSigningKey = $resolvedServer->getPublicSigningKey(); } catch (Exception $e) { - Logger::getLogger()->error('An internal error occurred while resolving the host domain', $e); - http_response_code(500); - if(Configuration::getSecurityConfiguration()->isDisplayInternalExceptions()) - { - print(Utilities::throwableToString($e)); - } - else - { - print('An internal error occurred'); - } - + self::returnError(502, StandardError::RESOLUTION_FAILED, 'Conflict: Failed to resolve the host domain: ' . $e->getMessage(), $e); return; } } try { + // Check if we have a registered peer with the same address $registeredPeer = RegisteredPeerManager::getPeerByAddress($clientRequest->getIdentifyAs()); // If the peer is registered, check if it is enabled if($registeredPeer !== null && !$registeredPeer->isEnabled()) { - // Refuse to create a session if the peer is disabled/banned - // This also prevents multiple sessions from being created for the same peer - // A cron job should be used to clean up disabled peers - http_response_code(403); - print('Unauthorized: The requested peer is disabled/banned'); + // Refuse to create a session if the peer is disabled/banned, this usually happens when + // a peer gets banned or more commonly when a client attempts to register as this peer but + // destroyed the session before it was created. + // This is to prevent multiple sessions from being created for the same peer, this is cleaned up + // with a cron job using `socialbox clean-sessions` + self::returnError(403, StandardError::FORBIDDEN, 'Unauthorized: The requested peer is disabled/banned'); return; } + // Otherwise the peer isn't registered, so we need to register it else { // Check if registration is enabled if(!Configuration::getRegistrationConfiguration()->isRegistrationEnabled()) { - http_response_code(403); - print('Unauthorized: Registration is disabled'); + self::returnError(401, StandardError::UNAUTHORIZED, 'Unauthorized: Registration is disabled'); return; } @@ -226,141 +224,220 @@ $registeredPeer = RegisteredPeerManager::getPeer($peerUuid); } - // Create the session UUID - $sessionUuid = SessionManager::createSession($publicKey, $registeredPeer, $clientRequest->getClientName(), $clientRequest->getClientVersion()); + // Generate server's encryption keys for this session + $serverEncryptionKey = Cryptography::generateEncryptionKeyPair(); + + // Create the session passing on the registered peer, client name, version, and public keys + $sessionUuid = SessionManager::createSession($registeredPeer, $clientRequest->getClientName(), $clientRequest->getClientVersion(), $clientPublicSigningKey, $clientPublicEncryptionKey, $serverEncryptionKey); + + // The server responds back with the session UUID & The server's public encryption key as the header http_response_code(201); // Created + header('Content-Type: text/plain'); + header(StandardHeaders::ENCRYPTION_PUBLIC_KEY->value . ': ' . $serverEncryptionKey->getPublicKey()); print($sessionUuid); // Return the session UUID } catch(InvalidArgumentException $e) { - http_response_code(412); // Precondition failed - print($e->getMessage()); // Why the request failed + // This is usually thrown due to an invalid input + self::returnError(400, StandardError::BAD_REQUEST, $e->getMessage(), $e); } catch(Exception $e) { - Logger::getLogger()->error('An internal error occurred while initiating the session', $e); - http_response_code(500); // Internal server error - if(Configuration::getSecurityConfiguration()->isDisplayInternalExceptions()) - { - print(Utilities::throwableToString($e)); - } - else - { - print('An internal error occurred'); - } + self::returnError(500, StandardError::INTERNAL_SERVER_ERROR, 'An internal error occurred while initiating the session', $e); } } /** - * Handles the Diffie-Hellman key exchange by decrypting the encrypted key passed on from the client using - * the server's private key and setting the encryption key to the session. + * Handles the Diffie-Hellman Ephemeral (DHE) key exchange process between the client and server, + * ensuring secure transport encryption key negotiation. The method validates request headers, + * session state, and cryptographic operations, and updates the session with the resulting keys + * and state upon successful negotiation. * - * 412: Headers malformed - * 400: Bad request - * 500: Internal server error - * 204: Success, no content. + * @param ClientRequest $clientRequest The request object containing headers, body, and session details + * required to perform the DHE exchange. * - * @param ClientRequest $clientRequest * @return void */ private static function handleDheExchange(ClientRequest $clientRequest): void { - // Check if the session UUID is set in the headers + // Check if the session UUID is set in the headers, bad request if not if(!$clientRequest->headerExists(StandardHeaders::SESSION_UUID)) { - Logger::getLogger()->verbose('Missing required header: ' . StandardHeaders::SESSION_UUID->value); - - http_response_code(412); - print('Missing required header: ' . StandardHeaders::SESSION_UUID->value); + self::returnError(400, StandardError::BAD_REQUEST, 'Missing required header: ' . StandardHeaders::SESSION_UUID->value); return; } - // Check if the request body is empty + if(!$clientRequest->headerExists(StandardHeaders::SIGNATURE)) + { + self::returnError(400, StandardError::BAD_REQUEST, 'Missing required header: ' . StandardHeaders::SIGNATURE->value); + return; + } + + if(empty($clientRequest->getHeader(StandardHeaders::SIGNATURE))) + { + self::returnError(400, StandardError::BAD_REQUEST, 'Bad request: The signature is empty'); + return; + } + + // Check if the request body is empty, bad request if so if(empty($clientRequest->getRequestBody())) { - Logger::getLogger()->verbose('Bad request: The key exchange request body is empty'); - - http_response_code(400); - print('Bad request: The key exchange request body is empty'); + self::returnError(400, StandardError::BAD_REQUEST, 'Bad request: The key exchange request body is empty'); return; } - // Check if the session is awaiting a DHE exchange - if($clientRequest->getSession()->getState() !== SessionState::AWAITING_DHE) + // Check if the session is awaiting a DHE exchange, forbidden if not + $session = $clientRequest->getSession(); + if($session->getState() !== SessionState::AWAITING_DHE) { - Logger::getLogger()->verbose('Bad request: The session is not awaiting a DHE exchange'); - - http_response_code(400); - print('Bad request: The session is not awaiting a DHE exchange'); + self::returnError(403, StandardError::FORBIDDEN, 'Bad request: The session is not awaiting a DHE exchange'); return; } + + // DHE STAGE: CLIENT -> SERVER + // Server & Client: Begin the DHE exchange using the exchanged public keys. + // On the client's side, same method but with the server's public key & client's private key try { - // Attempt to decrypt the encrypted key passed on from the client - $encryptionKey = Cryptography::decryptContent($clientRequest->getRequestBody(), Configuration::getInstanceConfiguration()->getPrivateKey()); + $sharedSecret = Cryptography::performDHE($session->getClientPublicEncryptionKey(), $session->getServerPrivateEncryptionKey()); } - catch (Exceptions\CryptographyException $e) + catch (CryptographyException $e) { - Logger::getLogger()->error(sprintf('Bad Request: Failed to decrypt the key for session %s', $clientRequest->getSessionUuid()), $e); - - http_response_code(400); - print('Bad Request: Cryptography error, make sure you have encrypted the key using the server\'s public key; ' . $e->getMessage()); + Logger::getLogger()->error('Failed to perform DHE exchange', $e); + self::returnError(422, StandardError::CRYPTOGRAPHIC_ERROR, 'DHE exchange failed', $e); return; } + // STAGE 1: CLIENT -> SERVER try { - // Finally set the encryption key to the session - SessionManager::setEncryptionKey($clientRequest->getSessionUuid(), $encryptionKey); + // Attempt to decrypt the encrypted key passed on from the client using the shared secret + $clientTransportEncryptionKey = Cryptography::decryptShared($clientRequest->getRequestBody(), $sharedSecret); + } + catch (CryptographyException $e) + { + self::returnError(400, StandardError::CRYPTOGRAPHIC_ERROR, 'Failed to decrypt the key', $e); + return; + } + + // Get the signature from the client and validate it against the decrypted key + $clientSignature = $clientRequest->getHeader(StandardHeaders::SIGNATURE); + if(!Cryptography::verifyMessage($clientTransportEncryptionKey, $clientSignature, $session->getClientPublicSigningKey())) + { + self::returnError(401, StandardError::UNAUTHORIZED, 'Invalid signature'); + return; + } + + // Validate the encryption key given by the client + if(!Cryptography::validateEncryptionKey($clientTransportEncryptionKey, Configuration::getCryptographyConfiguration()->getTransportEncryptionAlgorithm())) + { + self::returnError(400, StandardError::BAD_REQUEST, 'The transport encryption key is invalid and does not meet the server\'s requirements'); + return; + } + + // Receive stage complete, now we move on to the server's response + + // STAGE 2: SERVER -> CLIENT + try + { + // Generate the server's transport encryption key (our side) + $serverTransportEncryptionKey = Cryptography::generateEncryptionKey(Configuration::getCryptographyConfiguration()->getTransportEncryptionAlgorithm()); + + // Sign the shared secret using the server's private key + $signature = Cryptography::signMessage($serverTransportEncryptionKey, Configuration::getCryptographyConfiguration()->getHostPrivateKey()); + // Encrypt the server's transport key using the shared secret + $encryptedServerTransportKey = Cryptography::encryptShared($serverTransportEncryptionKey, $sharedSecret); + } + catch (CryptographyException $e) + { + Logger::getLogger()->error('Failed to generate the server\'s transport encryption key', $e); + self::returnError(500, StandardError::INTERNAL_SERVER_ERROR, 'There was an error while trying to process the DHE exchange', $e); + return; + } + + // Now update the session details with all the encryption keys and the state + try + { + SessionManager::setEncryptionKeys($clientRequest->getSessionUuid(), $sharedSecret, $clientTransportEncryptionKey, $serverTransportEncryptionKey); + SessionManager::updateState($clientRequest->getSessionUuid(), SessionState::ACTIVE); } catch (DatabaseOperationException $e) { Logger::getLogger()->error('Failed to set the encryption key for the session', $e); - http_response_code(500); - - if(Configuration::getSecurityConfiguration()->isDisplayInternalExceptions()) - { - print(Utilities::throwableToString($e)); - } - else - { - print('Internal Server Error: Failed to set the encryption key for the session'); - } - + self::returnError(500, StandardError::INTERNAL_SERVER_ERROR, 'Failed to set the encryption key for the session', $e); return; } - Logger::getLogger()->info(sprintf('DHE exchange completed for session %s', $clientRequest->getSessionUuid())); - http_response_code(204); // Success, no content + // Return the encrypted transport key for the server back to the client. + http_response_code(200); + header('Content-Type: application/octet-stream'); + header(StandardHeaders::SIGNATURE->value . ': ' . $signature); + print($encryptedServerTransportKey); } /** - * Handles incoming RPC requests from a client, processes each request, - * and returns the appropriate response(s) or error(s). + * Handles a Remote Procedure Call (RPC) request, ensuring proper decryption, + * signature verification, and response encryption, while processing one or more + * RPC methods as specified in the request. + * + * @param ClientRequest $clientRequest The RPC client request containing headers, body, and session information. * - * @param ClientRequest $clientRequest The client's request containing one or multiple RPC calls. * @return void */ private static function handleRpc(ClientRequest $clientRequest): void { + // Client: Encrypt the request body using the server's encryption key & sign it using the client's private key + // Server: Decrypt the request body using the servers's encryption key & verify the signature using the client's public key + // Server: Encrypt the response using the client's encryption key & sign it using the server's private key + if(!$clientRequest->headerExists(StandardHeaders::SESSION_UUID)) { - Logger::getLogger()->verbose('Missing required header: ' . StandardHeaders::SESSION_UUID->value); + self::returnError(400, StandardError::BAD_REQUEST, 'Missing required header: ' . StandardHeaders::SESSION_UUID->value); + return; + } - http_response_code(412); - print('Missing required header: ' . StandardHeaders::SESSION_UUID->value); + if(!$clientRequest->headerExists(StandardHeaders::SIGNATURE)) + { + self::returnError(400, StandardError::BAD_REQUEST, 'Missing required header: ' . StandardHeaders::SIGNATURE->value); + return; + } + + // Get the client session + $session = $clientRequest->getSession(); + + // Verify if the session is active + if($session->getState() !== SessionState::ACTIVE) + { + self::returnError(403, StandardError::FORBIDDEN, 'Session is not active'); return; } try { - $clientRequests = $clientRequest->getRpcRequests(); + // Attempt to decrypt the request body using the server's encryption key + $decryptedContent = Cryptography::decryptMessage($clientRequest->getRequestBody(), $session->getServerTransportEncryptionKey(), Configuration::getCryptographyConfiguration()->getTransportEncryptionAlgorithm()); + } + catch(CryptographyException $e) + { + self::returnError(400, StandardError::CRYPTOGRAPHIC_ERROR, 'Failed to decrypt request', $e); + return; + } + + // Attempt to verify the decrypted content using the client's public signing key + if(!Cryptography::verifyMessage($decryptedContent, $clientRequest->getSignature(), $session->getClientPublicSigningKey())) + { + self::returnError(400, StandardError::CRYPTOGRAPHIC_ERROR, 'Signature verification failed'); + return; + } + + try + { + $clientRequests = $clientRequest->getRpcRequests($decryptedContent); } catch (RequestException $e) { - http_response_code($e->getCode()); - print($e->getMessage()); + self::returnError($e->getCode(), $e->getStandardError(), $e->getMessage()); return; } @@ -442,16 +519,24 @@ return; } + $session = $clientRequest->getSession(); + try { - $encryptedResponse = Cryptography::encryptTransport($response, $clientRequest->getSession()->getEncryptionKey()); - $signature = Cryptography::signContent($response, Configuration::getInstanceConfiguration()->getPrivateKey(), true); + $encryptedResponse = Cryptography::encryptMessage( + message: $response, + encryptionKey: $session->getClientTransportEncryptionKey(), + algorithm: Configuration::getCryptographyConfiguration()->getTransportEncryptionAlgorithm() + ); + + $signature = Cryptography::signMessage( + message: $response, + privateKey: Configuration::getCryptographyConfiguration()->getHostPrivateKey() + ); } catch (Exceptions\CryptographyException $e) { - Logger::getLogger()->error('Failed to encrypt the response', $e); - http_response_code(500); - print('Internal Server Error: Failed to encrypt the response'); + self::returnError(500, StandardError::INTERNAL_SERVER_ERROR, 'Failed to encrypt the server response', $e); return; } @@ -460,4 +545,69 @@ header(StandardHeaders::SIGNATURE->value . ': ' . $signature); print($encryptedResponse); } + + /** + * Sends an error response by setting the HTTP response code, headers, and printing an error message. + * Optionally includes exception details in the response if enabled in the configuration. + * Logs the error message and any associated exception. + * + * @param int $responseCode The HTTP response code to send. + * @param StandardError $standardError The standard error containing error details. + * @param string|null $message An optional error message to display. Defaults to the message from the StandardError instance. + * @param Throwable|null $e An optional throwable to include in logs and the response, if enabled. + * + * @return void + */ + private static function returnError(int $responseCode, StandardError $standardError, ?string $message=null, ?Throwable $e=null): void + { + if($message === null) + { + $message = $standardError->getMessage(); + } + + http_response_code($responseCode); + header('Content-Type: text/plain'); + header(StandardHeaders::ERROR_CODE->value . ': ' . $standardError->value); + print($message); + + if(Configuration::getSecurityConfiguration()->isDisplayInternalExceptions() && $e !== null) + { + print(PHP_EOL . PHP_EOL . Utilities::throwableToString($e)); + } + + if($e !== null) + { + Logger::getLogger()->error($message, $e); + } + } + + /** + * Retrieves the server information by assembling data from the configuration settings. + * + * @return ServerInformation An instance of ServerInformation containing details such as server name, hashing algorithm, + * transport AES mode, and AES key length. + */ + public static function getServerInformation(): ServerInformation + { + return ServerInformation::fromArray([ + 'server_name' => Configuration::getInstanceConfiguration()->getName(), + 'server_keypair_expires' => Configuration::getCryptographyConfiguration()->getHostKeyPairExpires(), + 'transport_encryption_algorithm' => Configuration::getCryptographyConfiguration()->getTransportEncryptionAlgorithm() + ]); + } + + /** + * Retrieves the DNS record by generating a TXT record using the RPC endpoint, + * host public key, and host key pair expiration from the configuration. + * + * @return string The generated DNS TXT record. + */ + public static function getDnsRecord(): string + { + return DnsHelper::generateTxt( + Configuration::getInstanceConfiguration()->getRpcEndpoint(), + Configuration::getCryptographyConfiguration()->getHostPublicKey(), + Configuration::getCryptographyConfiguration()->getHostKeyPairExpires() + ); + } } \ No newline at end of file diff --git a/tests/Socialbox/Classes/CryptographyTest.php b/tests/Socialbox/Classes/CryptographyTest.php new file mode 100644 index 0000000..882787d --- /dev/null +++ b/tests/Socialbox/Classes/CryptographyTest.php @@ -0,0 +1,439 @@ +assertInstanceOf(KeyPair::class, $keyPair); + $this->assertNotEmpty($keyPair->getPublicKey()); + $this->assertNotEmpty($keyPair->getPrivateKey()); + } + + /** + * Test that the generated public key starts with the defined encryption key type prefix. + */ + public function testGeneratedPublicKeyHasEncryptionPrefix(): void + { + $keyPair = Cryptography::generateEncryptionKeyPair(); + + $this->assertStringStartsWith('enc:', $keyPair->getPublicKey()); + } + + /** + * Test that the generated private key starts with the defined encryption key type prefix. + */ + public function testGeneratedPrivateKeyHasEncryptionPrefix(): void + { + $keyPair = Cryptography::generateEncryptionKeyPair(); + + $this->assertStringStartsWith('enc:', $keyPair->getPrivateKey()); + } + + /** + * Test that the generated keys are of different base64-encoded string values. + */ + public function testPublicAndPrivateKeysAreDifferent(): void + { + $keyPair = Cryptography::generateEncryptionKeyPair(); + + $this->assertNotEquals($keyPair->getPublicKey(), $keyPair->getPrivateKey()); + } + + + /** + * Test that generateSigningKeyPair generates a KeyPair with valid keys. + */ + public function testGenerateSigningKeyPairProducesValidKeyPair(): void + { + $keyPair = Cryptography::generateSigningKeyPair(); + + $this->assertInstanceOf(KeyPair::class, $keyPair); + $this->assertNotEmpty($keyPair->getPublicKey()); + $this->assertNotEmpty($keyPair->getPrivateKey()); + } + + /** + * Test that the generated public key starts with the defined signing key type prefix. + */ + public function testGeneratedPublicKeyHasSigningPrefix(): void + { + $keyPair = Cryptography::generateSigningKeyPair(); + + $this->assertStringStartsWith('sig:', $keyPair->getPublicKey()); + } + + /** + * Test that the generated private key starts with the defined signing key type prefix. + */ + public function testGeneratedPrivateKeyHasSigningPrefix(): void + { + $keyPair = Cryptography::generateSigningKeyPair(); + + $this->assertStringStartsWith('sig:', $keyPair->getPrivateKey()); + } + + /** + * Test that performDHE successfully calculates a shared secret with valid keys. + */ + public function testPerformDHESuccessfullyCalculatesSharedSecret(): void + { + $aliceKeyPair = Cryptography::generateEncryptionKeyPair(); + $aliceSigningKeyPair = Cryptography::generateSigningKeyPair(); + $bobKeyPair = Cryptography::generateEncryptionKeyPair(); + $bobSigningKeyPair = Cryptography::generateSigningKeyPair(); + + // Alice performs DHE with Bob + $aliceSharedSecret = Cryptography::performDHE($bobKeyPair->getPublicKey(), $aliceKeyPair->getPrivateKey()); + // Bob performs DHE with Alice + $bobSharedSecret = Cryptography::performDHE($aliceKeyPair->getPublicKey(), $bobKeyPair->getPrivateKey()); + $this->assertEquals($aliceSharedSecret, $bobSharedSecret); + + // Alice sends "Hello, Bob!" to Bob, signing the message and encrypting it with the shared secret + $message = "Hello, Bob!"; + $aliceSignature = Cryptography::signMessage($message, $aliceSigningKeyPair->getPrivateKey()); + $encryptedMessage = Cryptography::encryptShared($message, $aliceSharedSecret); + + // Bob decrypts the message and verifies the signature + $decryptedMessage = Cryptography::decryptShared($encryptedMessage, $bobSharedSecret); + $isValid = Cryptography::verifyMessage($decryptedMessage, $aliceSignature, $aliceSigningKeyPair->getPublicKey()); + $this->assertEquals($message, $decryptedMessage); + $this->assertTrue($isValid); + + // Bob sends "Hello, Alice!" to Alice, signing the message and encrypting it with the shared secret + $message = "Hello, Alice!"; + $bobSignature = Cryptography::signMessage($message, $bobSigningKeyPair->getPrivateKey()); + $encryptedMessage = Cryptography::encryptShared($message, $bobSharedSecret); + + // Alice decrypts the message and verifies the signature + $decryptedMessage = Cryptography::decryptShared($encryptedMessage, $aliceSharedSecret); + $isValid = Cryptography::verifyMessage($decryptedMessage, $bobSignature, $bobSigningKeyPair->getPublicKey()); + $this->assertEquals($message, $decryptedMessage); + $this->assertTrue($isValid); + } + + /** + * Test that performDHE throws an exception when an invalid public key is used. + */ + public function testPerformDHEThrowsExceptionForInvalidPublicKey(): void + { + $encryptionKeyPair = Cryptography::generateEncryptionKeyPair(); + $invalidPublicKey = 'invalid_key'; + + $this->expectException(CryptographyException::class); + $this->expectExceptionMessage('Invalid key type. Expected enc:'); + + Cryptography::performDHE($invalidPublicKey, $encryptionKeyPair->getPrivateKey()); + } + + /** + * Test that performDHE throws an exception when an invalid private key is used. + */ + public function testPerformDHEThrowsExceptionForInvalidPrivateKey(): void + { + $encryptionKeyPair = Cryptography::generateEncryptionKeyPair(); + $invalidPrivateKey = 'invalid_key'; + + $this->expectException(CryptographyException::class); + $this->expectExceptionMessage('Invalid key type. Expected enc:'); + + Cryptography::performDHE($encryptionKeyPair->getPublicKey(), $invalidPrivateKey); + } + + + /** + * Test that encrypt correctly encrypts a message with a valid shared secret. + */ + public function testEncryptSuccessfullyEncryptsMessage(): void + { + $sharedSecret = Cryptography::performDHE( + Cryptography::generateEncryptionKeyPair()->getPublicKey(), + Cryptography::generateEncryptionKeyPair()->getPrivateKey() + ); + $message = "Test message"; + + $encryptedMessage = Cryptography::encryptShared($message, $sharedSecret); + + $this->assertNotEmpty($encryptedMessage); + $this->assertNotEquals($message, $encryptedMessage); + } + + /** + * Test that encrypt throws an exception when given an invalid shared secret. + */ + public function testEncryptThrowsExceptionForInvalidSharedSecret(): void + { + $invalidSharedSecret = "invalid_secret"; + $message = "Test message"; + + $this->expectException(CryptographyException::class); + $this->expectExceptionMessage("Encryption failed"); + + Cryptography::encryptShared($message, $invalidSharedSecret); + } + + /** + * Test that the encrypted message is different from the original message. + */ + public function testEncryptProducesDifferentMessage(): void + { + $sharedSecret = Cryptography::performDHE( + Cryptography::generateEncryptionKeyPair()->getPublicKey(), + Cryptography::generateEncryptionKeyPair()->getPrivateKey() + ); + $message = "Another test message"; + + $encryptedMessage = Cryptography::encryptShared($message, $sharedSecret); + + $this->assertNotEquals($message, $encryptedMessage); + } + + /** + * Test that decrypt successfully decrypts an encrypted message with a valid shared secret. + */ + public function testDecryptSuccessfullyDecryptsMessage(): void + { + $sharedSecret = Cryptography::performDHE( + Cryptography::generateEncryptionKeyPair()->getPublicKey(), + Cryptography::generateEncryptionKeyPair()->getPrivateKey() + ); + $message = "Decryption test message"; + + $encryptedMessage = Cryptography::encryptShared($message, $sharedSecret); + $decryptedMessage = Cryptography::decryptShared($encryptedMessage, $sharedSecret); + + $this->assertEquals($message, $decryptedMessage); + } + + /** + * Test that decrypt throws an exception when given an invalid shared secret. + */ + public function testDecryptThrowsExceptionForInvalidSharedSecret(): void + { + $sharedSecret = Cryptography::performDHE( + Cryptography::generateEncryptionKeyPair()->getPublicKey(), + Cryptography::generateEncryptionKeyPair()->getPrivateKey() + ); + $invalidSharedSecret = "invalid_shared_secret"; + $message = "Decryption failure case"; + + $encryptedMessage = Cryptography::encryptShared($message, $sharedSecret); + + $this->expectException(CryptographyException::class); + $this->expectExceptionMessage("Decryption failed"); + + Cryptography::decryptShared($encryptedMessage, $invalidSharedSecret); + } + + /** + * Test that decrypt throws an exception when the encrypted data is tampered with. + */ + public function testDecryptThrowsExceptionForTamperedEncryptedMessage(): void + { + $sharedSecret = Cryptography::performDHE( + Cryptography::generateEncryptionKeyPair()->getPublicKey(), + Cryptography::generateEncryptionKeyPair()->getPrivateKey() + ); + $message = "Tampered message"; + + $encryptedMessage = Cryptography::encryptShared($message, $sharedSecret); + $tamperedMessage = $encryptedMessage . "tampered_data"; + + $this->expectException(CryptographyException::class); + $this->expectExceptionMessage("Decryption failed"); + + Cryptography::decryptShared($tamperedMessage, $sharedSecret); + } + + /** + * Test that sign successfully signs a message with a valid private key. + */ + public function testSignSuccessfullySignsMessage(): void + { + $keyPair = Cryptography::generateSigningKeyPair(); + $message = "Message to sign"; + + $signature = Cryptography::signMessage($message, $keyPair->getPrivateKey()); + + $this->assertNotEmpty($signature); + } + + /** + * Test that sign throws an exception when an invalid private key is used. + */ + public function testSignThrowsExceptionForInvalidPrivateKey(): void + { + $invalidPrivateKey = "invalid_key"; + $message = "Message to sign"; + + $this->expectException(CryptographyException::class); + $this->expectExceptionMessage("Failed to sign message"); + + Cryptography::signMessage($message, $invalidPrivateKey); + } + + /** + * Test that verify successfully validates a correct signature with a valid message and public key. + */ + public function testVerifySuccessfullyValidatesSignature(): void + { + $keyPair = Cryptography::generateSigningKeyPair(); + $message = "Message to verify"; + $signature = Cryptography::signMessage($message, $keyPair->getPrivateKey()); + + $isValid = Cryptography::verifyMessage($message, $signature, $keyPair->getPublicKey()); + + $this->assertTrue($isValid); + } + + /** + * Test that verify fails for an invalid signature. + */ + public function testVerifyFailsForInvalidSignature(): void + { + $keyPair = Cryptography::generateSigningKeyPair(); + $message = "Message to verify"; + $signature = "invalid_signature"; + + $this->expectException(Exception::class); + + Cryptography::verifyMessage($message, $signature, $keyPair->getPublicKey()); + } + + /** + * Test that verify throws an exception for an invalid public key. + */ + public function testVerifyThrowsExceptionForInvalidPublicKey(): void + { + $keyPair = Cryptography::generateSigningKeyPair(); + $message = "Message to verify"; + $signature = Cryptography::signMessage($message, $keyPair->getPrivateKey()); + $invalidPublicKey = "invalid_public_key"; + + $this->expectException(CryptographyException::class); + $this->expectExceptionMessage("Failed to verify signature"); + + Cryptography::verifyMessage($message, $signature, $invalidPublicKey); + } + + /** + * Test that verify throws an exception for a public key with the wrong type prefix. + */ + public function testVerifyThrowsExceptionForInvalidKeyType(): void + { + $encryptionKeyPair = Cryptography::generateEncryptionKeyPair(); + $message = "Message to verify"; + $signature = "invalid_signature"; + + $this->expectException(CryptographyException::class); + $this->expectExceptionMessage("Invalid key type. Expected sig:"); + + Cryptography::verifyMessage($message, $signature, $encryptionKeyPair->getPublicKey()); + } + + /** + * Test that generateTransportKey creates a valid transport key for the default algorithm. + */ + public function testGenerateTransportKeyCreatesValidKeyForDefaultAlgo(): void + { + $transportKey = Cryptography::generateEncryptionKey(); + $decodedKey = sodium_base642bin($transportKey, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING, true); + + $this->assertNotEmpty($transportKey); + $this->assertEquals(SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES, strlen($decodedKey)); + } + + /** + * Test that generateTransportKey creates valid keys for specific supported algorithms. + */ + public function testGenerateTransportKeyCreatesValidKeysForAlgorithms(): void + { + $algorithms = [ + 'xchacha20' => SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES, + 'chacha20' => SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_KEYBYTES, + 'aes256gcm' => SODIUM_CRYPTO_AEAD_AES256GCM_KEYBYTES + ]; + + foreach ($algorithms as $algorithm => $expectedKeyLength) { + $transportKey = Cryptography::generateEncryptionKey($algorithm); + $decodedKey = sodium_base642bin($transportKey, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING, true); + + $this->assertNotEmpty($transportKey); + $this->assertEquals($expectedKeyLength, strlen($decodedKey)); + } + } + + /** + * Test that generateTransportKey throws an exception when given an invalid algorithm. + */ + public function testGenerateTransportKeyThrowsExceptionForInvalidAlgorithm(): void + { + $this->expectException(CryptographyException::class); + $this->expectExceptionMessage("Unsupported algorithm"); + + Cryptography::generateEncryptionKey("invalid_algorithm"); + } + + /** + * Test that generateTransportKey creates valid keys for other supported algorithms. + */ + public function testGenerateTransportKeyCreatesValidKeyForOtherSupportedAlgorithms(): void + { + $algorithms = [ + 'xchacha20' => SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES, + 'chacha20' => SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_KEYBYTES, + 'aes256gcm' => SODIUM_CRYPTO_AEAD_AES256GCM_KEYBYTES + ]; + + foreach ($algorithms as $algorithm => $expectedKeyLength) { + $transportKey = Cryptography::generateEncryptionKey($algorithm); + $decodedKey = sodium_base642bin($transportKey, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING, true); + + $this->assertNotEmpty($transportKey); + $this->assertEquals($expectedKeyLength, strlen($decodedKey)); + } + } + + /** + * Test that generateTransportKey throws an exception for unsupported algorithms. + */ + public function testGenerateTransportKeyThrowsExceptionForUnsupportedAlgorithm(): void + { + $this->expectException(CryptographyException::class); + $this->expectExceptionMessage("Unsupported algorithm"); + + Cryptography::generateEncryptionKey('invalid_algo'); + } + + public function testEncryptTransportMessageSuccessfullyEncryptsMessage(): void + { + $algorithms = [ + 'xchacha20' => SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES, + 'chacha20' => SODIUM_CRYPTO_AEAD_CHACHA20POLY1305_KEYBYTES, + 'aes256gcm' => SODIUM_CRYPTO_AEAD_AES256GCM_KEYBYTES + ]; + + foreach ($algorithms as $algorithm => $keyLength) { + $transportKey = Cryptography::generateEncryptionKey($algorithm); + $this->assertNotEmpty($transportKey); + $this->assertEquals($keyLength, strlen(sodium_base642bin($transportKey, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING, true))); + $message = "Test message"; + + $encryptedMessage = Cryptography::encryptMessage($message, $transportKey); + $decryptedMessage = Cryptography::decryptMessage($encryptedMessage, $transportKey); + + $this->assertEquals($message, $decryptedMessage); + } + } + } \ No newline at end of file From 9b33890b10dfb6e2538709a5fcb36f8f9a0658d2 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 3 Jan 2025 13:26:17 -0500 Subject: [PATCH 080/420] Update Cryptography tests for algorithm-specific handling --- tests/Socialbox/Classes/CryptographyTest.php | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/Socialbox/Classes/CryptographyTest.php b/tests/Socialbox/Classes/CryptographyTest.php index 882787d..425af15 100644 --- a/tests/Socialbox/Classes/CryptographyTest.php +++ b/tests/Socialbox/Classes/CryptographyTest.php @@ -322,7 +322,6 @@ $invalidPublicKey = "invalid_public_key"; $this->expectException(CryptographyException::class); - $this->expectExceptionMessage("Failed to verify signature"); Cryptography::verifyMessage($message, $signature, $invalidPublicKey); } @@ -347,7 +346,7 @@ */ public function testGenerateTransportKeyCreatesValidKeyForDefaultAlgo(): void { - $transportKey = Cryptography::generateEncryptionKey(); + $transportKey = Cryptography::generateEncryptionKey('xchacha20'); $decodedKey = sodium_base642bin($transportKey, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING, true); $this->assertNotEmpty($transportKey); @@ -380,7 +379,6 @@ public function testGenerateTransportKeyThrowsExceptionForInvalidAlgorithm(): void { $this->expectException(CryptographyException::class); - $this->expectExceptionMessage("Unsupported algorithm"); Cryptography::generateEncryptionKey("invalid_algorithm"); } @@ -411,7 +409,6 @@ public function testGenerateTransportKeyThrowsExceptionForUnsupportedAlgorithm(): void { $this->expectException(CryptographyException::class); - $this->expectExceptionMessage("Unsupported algorithm"); Cryptography::generateEncryptionKey('invalid_algo'); } @@ -430,8 +427,8 @@ $this->assertEquals($keyLength, strlen(sodium_base642bin($transportKey, SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING, true))); $message = "Test message"; - $encryptedMessage = Cryptography::encryptMessage($message, $transportKey); - $decryptedMessage = Cryptography::decryptMessage($encryptedMessage, $transportKey); + $encryptedMessage = Cryptography::encryptMessage($message, $transportKey, $algorithm); + $decryptedMessage = Cryptography::decryptMessage($encryptedMessage, $transportKey, $algorithm); $this->assertEquals($message, $decryptedMessage); } From 25dcf66755d8e58ae570617d1c4cab4dc194d2d4 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 3 Jan 2025 13:59:46 -0500 Subject: [PATCH 081/420] Refactor environment variable handling and initialization --- .env | 12 + docker-compose.yml | 16 +- .../Classes/CliCommands/InitializeCommand.php | 402 +++++++++++------- src/Socialbox/Classes/Cryptography.php | 5 +- 4 files changed, 266 insertions(+), 169 deletions(-) diff --git a/.env b/.env index e6e3e7b..5dab48f 100644 --- a/.env +++ b/.env @@ -2,7 +2,19 @@ LOG_LEVEL=debug SB_MODE=automated SB_DOMAIN=localhost +SB_INSTANCE_NAME=Socialbox SB_RPC_ENDPOINT=http://127.0.0.0:8085/ +SB_LOGGING_CONSOLE_ENABLED=true +SB_LOGGING_CONSOLE_LEVEL=info +SB_SECURITY_DISPLAY_INTERNAL_EXCEPTIONS=false +SB_CRYPTO_KEYPAIR_EXPIRES= +SB_CRYPTO_ENCRYPTION_KEYS_ALGORITHM=xchacha20 +SB_CRYPTO_TRANSPORT_ENCRYPTION_ALGORITHM=chacha20 +SB_CACHE_ENABLED=true +SB_CACHE_PORT=6379 +SB_CACHE_USERNAME=root +SB_CACHE_PASSWORD=root +SB_CACHE_DATABASE=0 # MariaDB Configuration MYSQL_ROOT_PASSWORD=sb_root diff --git a/docker-compose.yml b/docker-compose.yml index e139f75..d6e28fc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,15 +26,29 @@ services: SB_MODE: automated SB_STORAGE_PATH: /etc/socialbox # Change these values to match your environment or update the .env file + SB_INSTANCE_NAME: ${SB_INSTANCE_NAME:-socialbox} SB_INSTANCE_DOMAIN: ${SB_DOMAIN:-localhost} SB_INSTANCE_RPC_ENDPOINT: ${SB_RPC_ENDPOINT:-http://127.0.0.0:8085/} + SB_LOGGING_CONSOLE_ENABLED: ${SB_LOGGING_CONSOLE_ENABLED:-true} + SB_LOGGING_CONSOLE_LEVEL: ${SB_LOGGING_CONSOLE_LEVEL:-info} + SB_LOGGING_FILE_ENABLED: ${SB_LOGGING_FILE_ENABLED:-true} + SB_LOGGING_FILE_LEVEL: ${SB_LOGGING_FILE_LEVEL:-error} + SB_SECURITY_DISPLAY_INTERNAL_EXCEPTIONS: ${SB_SECURITY_DISPLAY_INTERNAL_EXCEPTIONS:-false} + SB_CRYPTO_KEYPAIR_EXPIRES: ${SB_CRYPTO_KEYPAIR_EXPIRES} + SB_CRYPTO_ENCRYPTION_KEYS_COUNT: ${SB_CRYPTO_ENCRYPTION_KEYS_COUNT:-10} + SB_CRYPTO_ENCRYPTION_KEYS_ALGORITHM: ${SB_CRYPTO_ENCRYPTION_KEYS_ALGORITHM:-xchacha20} + SB_CRYPTO_TRANSPORT_ENCRYPTION_ALGORITHM: ${SB_CRYPTO_TRANSPORT_ENCRYPTION_ALGORITHM:-chacha20} SB_DATABASE_HOST: mariadb SB_DATABASE_USERNAME: ${MYSQL_USER:-socialbox} SB_DATABASE_PASSWORD: ${MYSQL_PASSWORD:-socialbox} SB_DATABASE_NAME: ${MYSQL_DATABASE:-socialbox} + SB_CACHE_ENABLED: ${SB_CACHE_ENABLED:-true} SB_CACHE_ENGINE: redis SB_CACHE_HOST: redis - SB_CACHE_PASSWORD: ${REDIS_PASSWORD:-root} + SB_CACHE_PORT: ${SB_CACHE_PORT:-6379} + SB_CACHE_USERNAME: ${SB_CACHE_USERNAME:-root} + SB_CACHE_PASSWORD: ${SB_CACHE_PASSWORD:-root} + SB_CACHE_DATABASE: ${SB_CACHE_DATABASE:-0} healthcheck: test: ["CMD", "curl", "-f", "-H", "Request-Type: ping", "${SB_INSTANCE_RPC_ENDPOINT-http://127.0.0.0:8085/}"] interval: 30s diff --git a/src/Socialbox/Classes/CliCommands/InitializeCommand.php b/src/Socialbox/Classes/CliCommands/InitializeCommand.php index ca897b8..6a2f412 100644 --- a/src/Socialbox/Classes/CliCommands/InitializeCommand.php +++ b/src/Socialbox/Classes/CliCommands/InitializeCommand.php @@ -46,22 +46,21 @@ Logger::getLogger()->info(' configlib --conf socialbox -e nano'); Logger::getLogger()->info('Or manually at:'); Logger::getLogger()->info(sprintf(' %s', Configuration::getConfigurationLib()->getPath())); - Logger::getLogger()->info('Automated Setup Procedure is done using environment variables:'); - Logger::getLogger()->info(' - SB_MODE=automated'); - Logger::getLogger()->info(' - SB_INSTANCE_DOMAIN=example.com => The Domain Name'); - Logger::getLogger()->info(' - SB_INSTANCE_RPC_ENDPOINT=http://localhost => The RPC Endpoint, must be publicly accessible'); - Logger::getLogger()->info(' - SB_DATABASE_HOST=localhost => The MySQL Host'); - Logger::getLogger()->info(' - SB_DATABASE_PORT=3306 => The MySQL Port'); - Logger::getLogger()->info(' - SB_DATABASE_USER=root => The MySQL Username'); - Logger::getLogger()->info(' - SB_DATABASE_PASSWORD=pass => The MySQL Password'); - Logger::getLogger()->info(' - SB_DATABASE_DATABASE=socialbox => The MySQL Database'); - Logger::getLogger()->info(' - SB_CACHE_ENGINE=redis => The Cache engine to use, supports redis, memcached or null'); - Logger::getLogger()->info(' - SB_CACHE_HOST=localhost => The Cache Host'); - Logger::getLogger()->info(' - SB_CACHE_PORT=6379 => The Cache Port'); - Logger::getLogger()->info(' - SB_CACHE_PASSWORD=pass => The Cache Password'); - Logger::getLogger()->info(' - SB_CACHE_DATABASE=0 => The Cache Database'); - Logger::getLogger()->info(' - SB_STORAGE_PATH=/etc/socialbox => The Storage Path'); - Logger::getLogger()->info('Anything omitted will be null or empty in the configuration'); + + if(getenv('SB_MODE') === 'automated') + { + // Wait & Reload the configuration + while(!Configuration::getInstanceConfiguration()->isEnabled()) + { + Logger::getLogger()->info('Waiting for configuration, retrying in 5 seconds...'); + sleep(5); + Configuration::reload(); + } + } + else + { + return 1; + } return 1; } @@ -71,144 +70,7 @@ if(getenv('SB_MODE') === 'automated') { Logger::getLogger()->info('Automated Setup Procedure is detected'); - - if(getenv('SB_INSTANCE_DOMAIN') !== false) - { - Configuration::getConfigurationLib()->set('instance.domain', getenv('SB_INSTANCE_DOMAIN')); - Logger::getLogger()->info('Set instance.domain to ' . getenv('SB_INSTANCE_DOMAIN')); - } - else - { - Logger::getLogger()->warning('instance.domain is required but was not set, expected SB_INSTANCE_DOMAIN environment variable'); - } - - if(getenv('SB_INSTANCE_RPC_ENDPOINT') !== false) - { - Configuration::getConfigurationLib()->set('instance.rpc_endpoint', getenv('SB_INSTANCE_RPC_ENDPOINT')); - Logger::getLogger()->info('Set instance.rpc_endpoint to ' . getenv('SB_INSTANCE_RPC_ENDPOINT')); - } - else - { - Logger::getLogger()->warning('instance.rpc_endpoint is required but was not set, expected SB_INSTANCE_RPC_ENDPOINT environment variable'); - Configuration::getConfigurationLib()->set('instance.rpc_endpoint', 'http://127.0.0.0/'); - Logger::getLogger()->info('Set instance.rpc_endpoint to http://127.0.0.0/'); - } - - if(getenv('SB_STORAGE_PATH') !== false) - { - Configuration::getConfigurationLib()->set('storage.path', getenv('SB_STORAGE_PATH')); - Logger::getLogger()->info('Set storage.path to ' . getenv('SB_STORAGE_PATH')); - } - else - { - Configuration::getConfigurationLib()->set('storage.path', '/etc/socialbox'); - Logger::getLogger()->info('storage.path was not set, defaulting to /etc/socialbox'); - } - - if(getenv('SB_DATABASE_HOST') !== false) - { - Configuration::getConfigurationLib()->set('database.host', getenv('SB_DATABASE_HOST')); - Logger::getLogger()->info('Set database.host to ' . getenv('SB_DATABASE_HOST')); - } - else - { - Logger::getLogger()->warning('database.host is required but was not set, expected SB_DATABASE_HOST environment variable'); - } - - if(getenv('SB_DATABASE_PORT') !== false) - { - Configuration::getConfigurationLib()->set('database.port', getenv('SB_DATABASE_PORT')); - Logger::getLogger()->info('Set database.port to ' . getenv('SB_DATABASE_PORT')); - } - - if(getenv('SB_DATABASE_USERNAME') !== false) - { - Configuration::getConfigurationLib()->set('database.username', getenv('SB_DATABASE_USERNAME')); - Logger::getLogger()->info('Set database.username to ' . getenv('SB_DATABASE_USERNAME')); - } - else - { - Logger::getLogger()->warning('database.username is required but was not set, expected SB_DATABASE_USERNAME environment variable'); - } - - if(getenv('SB_DATABASE_PASSWORD') !== false) - { - Configuration::getConfigurationLib()->set('database.password', getenv('SB_DATABASE_PASSWORD')); - Logger::getLogger()->info('Set database.password to ' . getenv('SB_DATABASE_PASSWORD')); - } - else - { - Logger::getLogger()->warning('database.password is required but was not set, expected SB_DATABASE_PASSWORD environment variable'); - } - - if(getenv('SB_DATABASE_NAME') !== false) - { - Configuration::getConfigurationLib()->set('database.name', getenv('SB_DATABASE_NAME')); - Logger::getLogger()->info('Set database.name to ' . getenv('SB_DATABASE_NAME')); - } - else - { - Logger::getLogger()->warning('database.name is required but was not set, expected SB_DATABASE_NAME environment variable'); - } - - if(getenv('SB_CACHE_ENABLED') !== false) - { - Configuration::getConfigurationLib()->set('cache.enabled', true); - Logger::getLogger()->info('Set cache.engine to true'); - } - else - { - Configuration::getConfigurationLib()->set('cache.enabled', false); - Logger::getLogger()->info('cache.engine is was not set, defaulting to false'); - } - - - if(getenv('SB_CACHE_ENGINE') !== false) - { - Configuration::getConfigurationLib()->set('cache.engine', getenv('SB_CACHE_ENGINE')); - Logger::getLogger()->info('Set cache.engine to ' . getenv('SB_CACHE_ENGINE')); - } - - if(getenv('SB_CACHE_HOST') !== false) - { - Configuration::getConfigurationLib()->set('cache.host', getenv('SB_CACHE_HOST')); - Logger::getLogger()->info('Set cache.host to ' . getenv('SB_CACHE_HOST')); - } - elseif(Configuration::getCacheConfiguration()->isEnabled()) - { - Logger::getLogger()->warning('cache.host is required but was not set, expected SB_CACHE_HOST environment variable'); - } - - if(getenv('SB_CACHE_PORT') !== false) - { - Configuration::getConfigurationLib()->set('cache.port', getenv('SB_CACHE_PORT')); - Logger::getLogger()->info('Set cache.port to ' . getenv('SB_CACHE_PORT')); - } - - if(getenv('SB_CACHE_PASSWORD') !== false) - { - Configuration::getConfigurationLib()->set('cache.password', getenv('SB_CACHE_PASSWORD')); - Logger::getLogger()->info('Set cache.password to ' . getenv('SB_CACHE_PASSWORD')); - } - elseif(Configuration::getCacheConfiguration()->isEnabled()) - { - Logger::getLogger()->warning('cache.password is required but was not set, expected SB_CACHE_PASSWORD environment variable'); - } - - if(getenv('SB_CACHE_DATABASE') !== false) - { - Configuration::getConfigurationLib()->set('cache.database', getenv('SB_CACHE_DATABASE')); - Logger::getLogger()->info('Set cache.database to ' . getenv('SB_CACHE_DATABASE')); - } - elseif(Configuration::getCacheConfiguration()->isEnabled()) - { - Configuration::getConfigurationLib()->set('cache.database', 0); - Logger::getLogger()->info('cache.database defaulting to 0'); - } - - Logger::getLogger()->info('Updating configuration...'); - Configuration::getConfigurationLib()->save(); // Save - Configuration::reload(); // Reload + self::applyEnvironmentVariables(); } if(Configuration::getInstanceConfiguration()->getDomain() === null) @@ -300,22 +162,206 @@ } Logger::getLogger()->info('Updating configuration...'); - Configuration::getConfigurationLib()->save();; + Configuration::getConfigurationLib()->save(); Configuration::reload(); Logger::getLogger()->info('Socialbox has been initialized successfully'); Logger::getLogger()->info(sprintf('Set the DNS TXT record for the domain %s to the following value:', Configuration::getInstanceConfiguration()->getDomain())); Logger::getLogger()->info(Socialbox::getDnsRecord()); - if(getenv('SB_MODE') === 'automated') - { - Configuration::getConfigurationLib()->set('instance.enabled', true); - Configuration::getConfigurationLib()->save(); // Save + return 0; + } - Logger::getLogger()->info('Automated Setup Procedure is complete, requests to the RPC server ' . Configuration::getInstanceConfiguration()->getRpcEndpoint() . ' are now accepted'); + /** + * Applies environment variables to the application's configuration system. + * This method maps predefined environment variables to their corresponding + * configuration keys, validates their values, and updates the configuration + * library accordingly. If expected environment variables are missing and + * critical for certain components, warning logs are generated. + * Additionally, the configuration changes are saved and reloaded after being applied. + * + * @return void + */ + private static function applyEnvironmentVariables(): void + { + // Always set the 'instance.enabled' to true if the automated setup procedure is detected + Configuration::getConfigurationLib()->set('instance.enabled', true); + $configurationMap = [ + // Instance Configuration + 'SB_INSTANCE_NAME' => 'instance.name', + 'SB_INSTANCE_DOMAIN' => 'instance.domain', + 'SB_INSTANCE_RPC_ENDPOINT' => 'instance.rpc_endpoint', + 'SB_STORAGE_PATH' => 'storage.path', + + // Logging Configuration + 'SB_LOGGING_CONSOLE_ENABLED' => 'logging.console_logging_enabled', + 'SB_LOGGING_CONSOLE_LEVEL' => 'logging.console_logging_level', + 'SB_LOGGING_FILE_ENABLED' => 'logging.file_logging_enabled', + 'SB_LOGGING_FILE_LEVEL' => 'logging.file_logging_level', + + // Security & Cryptography Configuration + 'SB_SECURITY_DISPLAY_INTERNAL_EXCEPTIONS' => 'security.display_internal_exceptions', + 'SB_CRYPTO_KEYPAIR_EXPIRES' => 'cryptography.host_keypair_expires', + 'SB_CRYPTO_ENCRYPTION_KEYS_COUNT' => 'cryptography.encryption_keys_count', + 'SB_CRYPTO_ENCRYPTION_KEYS_ALGORITHM' => 'cryptography.encryption_keys_algorithm', + 'SB_CRYPTO_TRANSPORT_ENCRYPTION_ALGORITHM' => 'cryptography.transport_encryption_algorithm', + + // Database Configuration + 'SB_DATABASE_HOST' => 'database.host', + 'SB_DATABASE_PORT' => 'database.port', + 'SB_DATABASE_USERNAME' => 'database.username', + 'SB_DATABASE_PASSWORD' => 'database.password', + 'SB_DATABASE_NAME' => 'database.name', + + 'SB_CACHE_ENABLED' => 'cache.enabled', + 'SB_CACHE_ENGINE' => 'cache.engine', + 'SB_CACHE_HOST' => 'cache.host', + 'SB_CACHE_PORT' => 'cache.port', + 'SB_CACHE_USERNAME' => 'cache.username', + 'SB_CACHE_PASSWORD' => 'cache.password', + 'SB_CACHE_DATABASE' => 'cache.database', + ]; + + foreach($configurationMap as $env => $config) + { + $variable = getenv($env); + Logger::getLogger()->info(sprintf('Checking environment variable %s...', $env)); + + switch($env) + { + case 'SB_STORAGE_PATH': + case 'SB_LOGGING_FILE_LEVEL': + case 'SB_LOGGING_CONSOLE_LEVEL': + case 'SB_INSTANCE_NAME': + case 'SB_CRYPTO_ENCRYPTION_KEYS_ALGORITHM': + case 'SB_CRYPTO_TRANSPORT_ENCRYPTION_ALGORITHM': + case 'SB_CACHE_ENGINE': + case 'SB_CACHE_HOST': + case 'SB_CACHE_USERNAME': + case 'SB_CACHE_PASSWORD': + case 'SB_CACHE_DATABASE': + if($variable !== false) + { + Configuration::getConfigurationLib()->set($config, $variable); + Logger::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); + } + break; + + case 'SB_INSTANCE_DOMAIN': + if($variable === false && Configuration::getInstanceConfiguration()->getDomain() === null) + { + Logger::getLogger()->warning(sprintf('%s is not set, expected %s environment variable', $config, $env)); + } + else + { + Configuration::getConfigurationLib()->set($config, $variable); + Logger::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); + } + break; + + case 'SB_DATABASE_HOST': + if($variable === false && Configuration::getDatabaseConfiguration()->getHost() === null) + { + Logger::getLogger()->warning(sprintf('%s is not set, expected %s environment variable', $config, $env)); + } + else + { + Configuration::getConfigurationLib()->set($config, $variable); + Logger::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); + } + break; + + case 'SB_DATABASE_PORT': + if($variable === false && Configuration::getDatabaseConfiguration()->getPort() === null) + { + Logger::getLogger()->warning(sprintf('%s is not set, expected %s environment variable', $config, $env)); + } + else + { + Configuration::getConfigurationLib()->set($config, (int) $variable); + Logger::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); + } + break; + + case 'SB_DATABASE_USERNAME': + if($variable === false && Configuration::getDatabaseConfiguration()->getUsername() === null) + { + Logger::getLogger()->warning(sprintf('%s is not set, expected %s environment variable', $config, $env)); + } + else + { + Configuration::getConfigurationLib()->set($config, $variable); + Logger::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); + } + break; + + case 'SB_DATABASE_PASSWORD': + if($variable === false && Configuration::getDatabaseConfiguration()->getPassword() === null) + { + Logger::getLogger()->warning(sprintf('%s is not set, expected %s environment variable', $config, $env)); + } + else + { + Configuration::getConfigurationLib()->set($config, $variable); + Logger::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); + } + break; + + case 'SB_DATABASE_NAME': + if($variable === false && Configuration::getDatabaseConfiguration()->getName() === null) + { + Logger::getLogger()->warning(sprintf('%s is not set, expected %s environment variable', $config, $env)); + } + else + { + Configuration::getConfigurationLib()->set($config, $variable); + Logger::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); + } + break; + + case 'SB_INSTANCE_RPC_ENDPOINT': + if($variable === false && Configuration::getInstanceConfiguration()->getRpcEndpoint() === null) + { + Logger::getLogger()->warning(sprintf('%s is not set, expected %s environment variable', $config, $env)); + } + else + { + Configuration::getConfigurationLib()->set($config, $variable); + Logger::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); + } + break; + + case 'SB_LOGGING_CONSOLE_ENABLED': + case 'SB_SECURITY_DISPLAY_INTERNAL_EXCEPTIONS': + case 'SB_LOGGING_FILE_ENABLED': + case 'SB_CACHE_ENABLED': + if($variable !== false) + { + Configuration::getConfigurationLib()->set($config, filter_var($variable, FILTER_VALIDATE_BOOLEAN)); + Logger::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); + } + break; + + case 'SB_CRYPTO_KEYPAIR_EXPIRES': + case 'SB_CRYPTO_ENCRYPTION_KEYS_COUNT': + case 'SB_CACHE_PORT': + if($variable !== false) + { + Configuration::getConfigurationLib()->set($config, (int) $variable); + Logger::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); + } + break; + + default: + Logger::getLogger()->warning("Environment variable $env is not supported"); + break; + } } - return 0; + // Apply changes & reload the configuration + Logger::getLogger()->info('Updating configuration...'); + Configuration::getConfigurationLib()->save(); // Save + Configuration::reload(); // Reload } /** @@ -323,10 +369,36 @@ */ public static function getHelpMessage(): string { - return "Initialize Command - Initializes Socialbox for first-runs\n" . - "Usage: socialbox init [arguments]\n\n" . - "Arguments:\n" . - " --force - Forces the initialization process to run even the instance is disabled\n"; + return "Initialize Command - Initializes Socialbox for first-runs\n" . + "Usage: socialbox init [arguments]\n\n" . + "Arguments:\n" . + " --force - Forces the initialization process to run even the instance is disabled\n\n" . + "Environment Variables:\n" . + " SB_MODE - Set to 'automated' to enable automated setup procedure (Must be set to enable environment variables)\n" . + " SB_INSTANCE_DOMAIN - The domain name of the instance (eg; Socialbox)\n" . + " SB_INSTANCE_RPC_ENDPOINT - The public RPC endpoint of the instance (eg; https://rpc.teapot.com/)\n" . + " SB_STORAGE_PATH - The path to store files (default: /etc/socialbox)\n" . + " SB_LOGGING_CONSOLE_ENABLED - Enable console logging (default: true)\n" . + " SB_LOGGING_CONSOLE_LEVEL - Console logging level (default: info)\n" . + " SB_LOGGING_FILE_ENABLED - Enable file logging (default: true)\n" . + " SB_LOGGING_FILE_LEVEL - File logging level (default: error)\n" . + " SB_SECURITY_DISPLAY_INTERNAL_EXCEPTIONS - Display internal exceptions (default: false)\n" . + " SB_CRYPTO_KEYPAIR_EXPIRES - The expiration date of the key pair in Unix timestamp (default: current time + 1 year)\n" . + " SB_CRYPTO_ENCRYPTION_KEYS_COUNT - The number of internal encryption keys to generate (default: 5)\n" . + " SB_CRYPTO_ENCRYPTION_KEYS_ALGORITHM - The algorithm to use for encryption keys (default: xchacha20)\n" . + " SB_CRYPTO_TRANSPORT_ENCRYPTION_ALGORITHM - The algorithm to use for transport encryption (default: chacha20)\n" . + " SB_DATABASE_HOST - The database host (default: localhost)\n" . + " SB_DATABASE_PORT - The database port (default: 3306)\n" . + " SB_DATABASE_USERNAME - The database username (default: root)\n" . + " SB_DATABASE_PASSWORD - The database password (default: null)\n" . + " SB_DATABASE_NAME - The database name (default: socialbox)\n" . + " SB_CACHE_ENABLED - Enable cache layer (default: false)\n" . + " SB_CACHE_ENGINE - The cache engine to use (default: redis)\n" . + " SB_CACHE_HOST - The cache host (default: localhost)\n" . + " SB_CACHE_PORT - The cache port (default: 6379)\n" . + " SB_CACHE_USERNAME - The cache username (default: null)\n" . + " SB_CACHE_PASSWORD - The cache password (default: null)\n" . + " SB_CACHE_DATABASE - The cache database (default: 0)\n"; } /** diff --git a/src/Socialbox/Classes/Cryptography.php b/src/Socialbox/Classes/Cryptography.php index e5a223f..8b6fdee 100644 --- a/src/Socialbox/Classes/Cryptography.php +++ b/src/Socialbox/Classes/Cryptography.php @@ -52,7 +52,7 @@ */ public static function validatePublicEncryptionKey(string $publicKey): bool { - if(!str_starts_with($publicKey, 'enc:')) + if(!str_starts_with($publicKey, self::KEY_TYPE_ENCRYPTION)) { return false; } @@ -112,12 +112,11 @@ * * @param string $publicKey The base64-encoded public signing key to be validated. * @return bool Returns true if the key is valid, or false if it is invalid. - * @throws CryptographyException If the public key is incorrectly formatted or its length is invalid. */ public static function validatePublicSigningKey(string $publicKey): bool { // Check if the key is prefixed with "sig:" - if (!str_starts_with($publicKey, 'sig:')) + if (!str_starts_with($publicKey, self::KEY_TYPE_SIGNING)) { // If it doesn't start with "sig:", consider it invalid return false; From 76abbdcb3e5f1391b1d9962145d0fae1edaf8cfb Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 3 Jan 2025 14:18:20 -0500 Subject: [PATCH 082/420] Switch encryption key columns from text to varchar(64) --- .../Classes/Resources/database/sessions.sql | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Socialbox/Classes/Resources/database/sessions.sql b/src/Socialbox/Classes/Resources/database/sessions.sql index ddd0b9e..dcf4a11 100644 --- a/src/Socialbox/Classes/Resources/database/sessions.sql +++ b/src/Socialbox/Classes/Resources/database/sessions.sql @@ -6,13 +6,13 @@ create table sessions client_name varchar(256) not null comment 'The name of the client that is using this session', client_version varchar(16) not null comment 'The version of the client', authenticated tinyint(1) default 0 not null comment 'Indicates if the session is currently authenticated as the peer', - client_public_signing_key text not null comment 'The client''s public signing key used for signing decrypted messages', - client_public_encryption_key text not null comment 'The Public Key of the client''s encryption key', - server_public_encryption_key text not null comment 'The server''s public encryption key for this session', - server_private_encryption_key text not null comment 'The server''s private encryption key for this session', - private_shared_secret text null comment 'The shared secret encryption key between the Client & Server', - client_transport_encryption_key text null comment 'The encryption key for sending messages to the client', - server_transport_encryption_key text null comment 'The encryption key for sending messages to the server', + client_public_signing_key varchar(64) not null comment 'The client''s public signing key used for signing decrypted messages', + client_public_encryption_key varchar(64) not null comment 'The Public Key of the client''s encryption key', + server_public_encryption_key varchar(64) not null comment 'The server''s public encryption key for this session', + server_private_encryption_key varchar(64) not null comment 'The server''s private encryption key for this session', + private_shared_secret varchar(64) null comment 'The shared secret encryption key between the Client & Server', + client_transport_encryption_key varchar(64) null comment 'The encryption key for sending messages to the client', + server_transport_encryption_key varchar(64) null comment 'The encryption key for sending messages to the server', state enum ('AWAITING_DHE', 'ACTIVE', 'CLOSED', 'EXPIRED') default 'AWAITING_DHE' not null comment 'The status of the session', flags text null comment 'The current flags that is set to the session', created timestamp default current_timestamp() not null comment 'The Timestamp for when the session was last created', From b60b12f9aed53f5fa8f1639cc574c3379b00923f Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 3 Jan 2025 18:30:16 -0500 Subject: [PATCH 083/420] Add PoliciesConfiguration to handle server policies --- src/Socialbox/Classes/Configuration.php | 25 +++++++++++++++++++ .../Configuration/PoliciesConfiguration.php | 21 ++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 src/Socialbox/Classes/Configuration/PoliciesConfiguration.php diff --git a/src/Socialbox/Classes/Configuration.php b/src/Socialbox/Classes/Configuration.php index 09fd208..fcae5b0 100644 --- a/src/Socialbox/Classes/Configuration.php +++ b/src/Socialbox/Classes/Configuration.php @@ -7,6 +7,7 @@ use Socialbox\Classes\Configuration\DatabaseConfiguration; use Socialbox\Classes\Configuration\InstanceConfiguration; use Socialbox\Classes\Configuration\LoggingConfiguration; + use Socialbox\Classes\Configuration\PoliciesConfiguration; use Socialbox\Classes\Configuration\RegistrationConfiguration; use Socialbox\Classes\Configuration\SecurityConfiguration; use Socialbox\Classes\Configuration\StorageConfiguration; @@ -21,6 +22,7 @@ private static ?LoggingConfiguration $loggingConfiguration = null; private static ?CacheConfiguration $cacheConfiguration = null; private static ?RegistrationConfiguration $registrationConfiguration = null; + private static ?PoliciesConfiguration $policiesConfiguration = null; private static ?StorageConfiguration $storageConfiguration = null; /** @@ -118,6 +120,10 @@ $config->setDefault('registration.display_picture_required', false); $config->setDefault('registration.image_captcha_verification_required', true); + // Server Policies + // The maximum number of signing keys a peer can register onto the server at once + $config->setDefault('policies.max_signing_keys', 20); + // Storage configuration $config->setDefault('storage.path', '/etc/socialbox'); // The main path for file storage $config->setDefault('storage.user_display_images_path', 'user_profiles'); // eg; `/etc/socialbox/user_profiles` @@ -133,6 +139,7 @@ self::$loggingConfiguration = new LoggingConfiguration(self::$configuration->getConfiguration()['logging']); self::$cacheConfiguration = new CacheConfiguration(self::$configuration->getConfiguration()['cache']); self::$registrationConfiguration = new RegistrationConfiguration(self::$configuration->getConfiguration()['registration']); + self::$policiesConfiguration = new PoliciesConfiguration(self::$configuration->getConfiguration()['policies']); self::$storageConfiguration = new StorageConfiguration(self::$configuration->getConfiguration()['storage']); } @@ -301,6 +308,24 @@ return self::$registrationConfiguration; } + /** + * Retrieves the policies configuration. + * + * This method returns the current PoliciesConfiguration instance. + * If the configuration has not been initialized yet, it initializes it first. + * + * @return PoliciesConfiguration The policies configuration instance. + */ + public static function getPoliciesConfiguration(): PoliciesConfiguration + { + if(self::$policiesConfiguration === null) + { + self::initializeConfiguration(); + } + + return self::$policiesConfiguration; + } + /** * Retrieves the storage configuration. * diff --git a/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php b/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php new file mode 100644 index 0000000..90b356d --- /dev/null +++ b/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php @@ -0,0 +1,21 @@ +maxSigningKeys = $data['max_signing_keys']; + } + + /** + * @return int + */ + public function getMaxSigningKeys(): int + { + return $this->maxSigningKeys; + } + } \ No newline at end of file From a3976742d69aebc874075d95a183e864c97cfb9c Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 3 Jan 2025 18:30:27 -0500 Subject: [PATCH 084/420] Add recursive array conversion in RpcResponse::convertToArray --- src/Socialbox/Objects/RpcResponse.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Socialbox/Objects/RpcResponse.php b/src/Socialbox/Objects/RpcResponse.php index 2e56751..2ec8238 100644 --- a/src/Socialbox/Objects/RpcResponse.php +++ b/src/Socialbox/Objects/RpcResponse.php @@ -55,6 +55,26 @@ return $data->toArray(); } + // If the data is an array, recursively call this function on each element + if(is_array($data)) + { + foreach($data as $key => $value) + { + if(is_array($value)) + { + $data[$key] = $this->convertToArray($value); + } + elseif($value instanceof SerializableInterface) + { + $data[$key] = $value->toArray(); + } + else + { + $data[$key] = $value; + } + } + } + return $data; } From d732c89632ad42cfe91702a8e36f3a6ef0c35a3f Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 3 Jan 2025 18:30:40 -0500 Subject: [PATCH 085/420] Add `Peer` class and conversion method in `RegisteredPeerRecord` --- .../Objects/Database/RegisteredPeerRecord.php | 11 ++ src/Socialbox/Objects/Standard/Peer.php | 108 ++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 src/Socialbox/Objects/Standard/Peer.php diff --git a/src/Socialbox/Objects/Database/RegisteredPeerRecord.php b/src/Socialbox/Objects/Database/RegisteredPeerRecord.php index 7d805bb..4f6171e 100644 --- a/src/Socialbox/Objects/Database/RegisteredPeerRecord.php +++ b/src/Socialbox/Objects/Database/RegisteredPeerRecord.php @@ -6,6 +6,7 @@ use Socialbox\Classes\Configuration; use Socialbox\Enums\Flags\PeerFlags; use Socialbox\Interfaces\SerializableInterface; + use Socialbox\Objects\Standard\Peer; use Socialbox\Objects\Standard\SelfUser; class RegisteredPeerRecord implements SerializableInterface @@ -191,6 +192,16 @@ return new SelfUser($this); } + /** + * Converts the current instance to a Peer object. + * + * @return Peer The Peer representation of the current instance. + */ + public function toPeer(): Peer + { + return Peer::fromArray($this->toArray()); + } + /** * @inheritDoc */ diff --git a/src/Socialbox/Objects/Standard/Peer.php b/src/Socialbox/Objects/Standard/Peer.php new file mode 100644 index 0000000..16b3c02 --- /dev/null +++ b/src/Socialbox/Objects/Standard/Peer.php @@ -0,0 +1,108 @@ +address = PeerAddress::fromAddress($data['address']); + } + elseif($data['address'] instanceof PeerAddress) + { + $this->address = $data['address']; + } + else + { + throw new InvalidArgumentException('Invalid address value'); + } + + $this->displayName = $data['display_name']; + $this->flags = $data['flags']; + $this->registered = $data['registered']; + } + + /** + * Retrieves the address property. + * + * @return PeerAddress The address associated with the instance. + */ + public function getAddress(): PeerAddress + { + return $this->address; + } + + /** + * Retrieves the display name of the entity. + * + * @return string The display name associated with the entity. + */ + public function getDisplayName(): string + { + return $this->displayName; + } + + /** + * Retrieves the flags associated with the entity. + * + * @return array An array containing the flags. + */ + public function getFlags(): array + { + return $this->flags; + } + + /** + * Retrieves the registered value. + * + * @return int The registered property value. + */ + public function getRegistered(): int + { + return $this->registered; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): Peer + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'address' => $this->address->getAddress(), + 'display_name' => $this->displayName, + 'flags' => $this->flags, + 'registered' => $this->registered + ]; + } + } \ No newline at end of file From e4b9a08972086f19593a130a1538896e085d1745 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 3 Jan 2025 18:30:50 -0500 Subject: [PATCH 086/420] Add signing key management functionality --- .../StandardMethods/SettingsAddSigningKey.php | 62 +++++ .../SettingsGetSigningKeys.php | 39 +++ src/Socialbox/Enums/SigningKeyState.php | 10 + src/Socialbox/Enums/StandardMethods.php | 14 +- src/Socialbox/Managers/SigningKeysManager.php | 224 ++++++++++++++++++ .../Objects/Database/SigningKeyRecord.php | 171 +++++++++++++ src/Socialbox/Objects/Standard/SigningKey.php | 174 ++++++++++++++ 7 files changed, 693 insertions(+), 1 deletion(-) create mode 100644 src/Socialbox/Classes/StandardMethods/SettingsAddSigningKey.php create mode 100644 src/Socialbox/Classes/StandardMethods/SettingsGetSigningKeys.php create mode 100644 src/Socialbox/Enums/SigningKeyState.php create mode 100644 src/Socialbox/Managers/SigningKeysManager.php create mode 100644 src/Socialbox/Objects/Database/SigningKeyRecord.php create mode 100644 src/Socialbox/Objects/Standard/SigningKey.php diff --git a/src/Socialbox/Classes/StandardMethods/SettingsAddSigningKey.php b/src/Socialbox/Classes/StandardMethods/SettingsAddSigningKey.php new file mode 100644 index 0000000..60b4f6a --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/SettingsAddSigningKey.php @@ -0,0 +1,62 @@ +containsParameter('public_key')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'public_key' parameter"); + } + + $expires = null; + if($rpcRequest->containsParameter('expires')) + { + $expires = (int)$rpcRequest->getParameter('expires'); + } + + $name = null; + if($rpcRequest->containsParameter('name')) + { + $name = $rpcRequest->getParameter('name'); + } + + $peerUuid = $request->getPeer()->getUuid(); + + try + { + if(SigningKeysManager::getSigningKeyCount($peerUuid) >= Configuration::getPoliciesConfiguration()->getMaxSigningKeys()) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The maximum number of signing keys has been reached'); + } + + $uuid = SigningKeysManager::addSigningKey($peerUuid, $rpcRequest->getParameter('public_key'), $expires, $name); + } + catch(InvalidArgumentException $e) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, $e->getMessage()); + } + catch(Exception $e) + { + throw new StandardException('Failed to add the signing key', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse($uuid); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/SettingsGetSigningKeys.php b/src/Socialbox/Classes/StandardMethods/SettingsGetSigningKeys.php new file mode 100644 index 0000000..615aac4 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/SettingsGetSigningKeys.php @@ -0,0 +1,39 @@ +getPeer()->getUuid()); + } + catch (DatabaseOperationException $e) + { + throw new StandardException('Failed to get the signing keys', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if(empty($keys)) + { + // Return an empty array if the results are empty + return $rpcRequest->produceResponse([]); + } + + // Return the signing keys as an array of standard objects + return $rpcRequest->produceResponse(array_map(fn($key) => $key->toStandard(), $keys)); + } + } \ No newline at end of file diff --git a/src/Socialbox/Enums/SigningKeyState.php b/src/Socialbox/Enums/SigningKeyState.php new file mode 100644 index 0000000..403d930 --- /dev/null +++ b/src/Socialbox/Enums/SigningKeyState.php @@ -0,0 +1,10 @@ + SettingsSetPassword::execute($request, $rpcRequest), self::SETTINGS_SET_DISPLAY_NAME => SettingsSetDisplayName::execute($request, $rpcRequest), + self::SETTINGS_ADD_SIGNING_KEY => SettingsAddSigningKey::execute($request, $rpcRequest), + self::SETTINGS_GET_SIGNING_KEYS => SettingsGetSigningKeys::execute($request, $rpcRequest), + default => $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, sprintf("The method %s is not supported by the server", $rpcRequest->getMethod())) }; } @@ -158,9 +166,13 @@ $methods[] = self::SETTINGS_SET_PASSWORD; } - // If the user is authenticated, then preform additional method calls + // If the user is authenticated, then allow additional method calls if($session->isAuthenticated()) { + // Authenticated users can always manage their signing keys + $methods[] = self::SETTINGS_ADD_SIGNING_KEY; + $methods[] = self::SETTINGS_GET_SIGNING_KEYS; + // Always allow the authenticated user to change their password if(!in_array(SessionFlags::SET_PASSWORD, $session->getFlags())) { diff --git a/src/Socialbox/Managers/SigningKeysManager.php b/src/Socialbox/Managers/SigningKeysManager.php new file mode 100644 index 0000000..c74be55 --- /dev/null +++ b/src/Socialbox/Managers/SigningKeysManager.php @@ -0,0 +1,224 @@ +prepare("SELECT state, expires FROM signing_keys WHERE uuid=:uuid"); + $statement->bindParam(':uuid', $uuid); + $statement->execute(); + + if($row = $statement->fetch()) + { + if(is_int($row['expires']) && $row['expires'] < time()) + { + return SigningKeyState::EXPIRED; + } + + if($row['expires'] instanceof DateTime && $row['expires'] < new DateTime()) + { + return SigningKeyState::EXPIRED; + } + + return SigningKeyState::tryFrom($row['state']) ?? SigningKeyState::NOT_FOUND; + } + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to get the signing key state from the database', $e); + } + + return SigningKeyState::NOT_FOUND; + } + + /** + * Retrieves the count of signing keys associated with a specific peer UUID. + * + * @param string $peerUuid The UUID of the peer for which to count the signing keys. + * @return int The number of signing keys associated with the given peer UUID. + * @throws DatabaseOperationException If there is an error during the database operation. + */ + public static function getSigningKeyCount(string $peerUuid): int + { + try + { + $statement = Database::getConnection()->prepare("SELECT COUNT(*) FROM signing_keys WHERE peer_uuid=:peer_uuid"); + $statement->bindParam(':peer_uuid', $peerUuid); + $statement->execute(); + + return $statement->fetchColumn(); + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to get the signing key count from the database', $e); + } + } + + /** + * Adds a signing key to the database for a specific peer. + * + * @param string $peerUuid The unique identifier of the peer associated with the signing key. + * @param string $publicKey The public signing key to be added. Must be valid according to the Cryptography::validatePublicSigningKey method. + * @param int|null $expires Optional expiration timestamp for the signing key. Can be null if the key does not expire. + * @param string|null $name Optional name associated with the signing key. Must not exceed 64 characters in length. + * @throws DatabaseOperationException If the operation to add the signing key to the database fails. + * @return string The UUID of the newly added signing key. + */ + public static function addSigningKey(string $peerUuid, string $publicKey, ?int $expires=null, ?string $name=null): string + { + if(!Cryptography::validatePublicSigningKey($publicKey)) + { + throw new InvalidArgumentException('The public key is invalid'); + } + + if(strlen($name) > 64) + { + throw new InvalidArgumentException('The name is too long'); + } + + $uuid = UuidV4::v4()->toRfc4122(); + + try + { + $statement = Database::getConnection()->prepare("INSERT INTO signing_keys (uuid, peer_uuid, public_key, expires, name) VALUES (:uuid, :peer_uuid, :public_key, :expires, :name)"); + $statement->bindParam(':uuid', $uuid); + $statement->bindParam(':peer_uuid', $peerUuid); + $statement->bindParam(':public_key', $publicKey); + $statement->bindParam(':expires', $expires); + $statement->bindParam(':name', $name); + $statement->execute(); + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to add a signing key to the database', $e); + } + + return $uuid; + } + + /** + * Updates the state of a signing key in the database identified by its UUID. + * + * @param string $uuid The unique identifier of the signing key to update. + * @param SigningKeyState $state The new state to set for the signing key. + * @return void + * @throws DatabaseOperationException + */ + public static function updateSigningKeyState(string $uuid, SigningKeyState $state): void + { + $state = $state->value; + + try + { + $statement = Database::getConnection()->prepare("UPDATE signing_keys SET state=:state WHERE uuid=:uuid"); + $statement->bindParam(':state', $state); + $statement->bindParam(':uuid', $uuid); + $statement->execute(); + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to update the signing key state in the database', $e); + } + } + + /** + * Retrieves a signing key from the database using the provided UUID. + * + * @param string $uuid The UUID of the signing key to retrieve. + * @return SigningKeyRecord|null The signing key record if found, or null if no record exists. + * @throws DatabaseOperationException If a database error occurs during the operation. + */ + public static function getSigningKey(string $uuid): ?SigningKeyRecord + { + try + { + $statement = Database::getConnection()->prepare("SELECT * FROM signing_keys WHERE uuid=:uuid"); + $statement->bindParam(':uuid', $uuid); + $statement->execute(); + + if($row = $statement->fetch()) + { + return SigningKeyRecord::fromArray($row); + } + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to get the signing key from the database', $e); + } + + return null; + } + + /** + * Retrieves the signing keys associated with a specific peer UUID. + * + * @param string $peerUuid The UUID of the peer whose signing keys are to be retrieved. + * @return SigningKeyRecord[] An array of SigningKeyRecord objects representing the signing keys. + * @throws DatabaseOperationException If an error occurs during the database operation. + */ + public static function getSigningKeys(string $peerUuid): array + { + try + { + $statement = Database::getConnection()->prepare("SELECT * FROM signing_keys WHERE peer_uuid=:peer_uuid"); + $statement->bindParam(':peer_uuid', $peerUuid); + $statement->execute(); + + $signingKeys = []; + while($row = $statement->fetch()) + { + $signingKeys[] = SigningKeyRecord::fromArray($row); + } + + return $signingKeys; + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to get the signing keys from the database', $e); + } + } + + /** + * Verifies the digital signature of a message using the signing key associated with a specific UUID. + * + * @param string $message The message whose signature needs to be verified. + * @param string $signature The digital signature to be verified. + * @param string $uuid The UUID used to retrieve the corresponding signing key. + * @return bool True if the signature is valid, false otherwise. + * @throws CryptographyException If an error occurs during the cryptographic operation. + * @throws DatabaseOperationException If an error occurs during the database operation. + */ + public static function verifySignature(string $message, string $signature, string $uuid): bool + { + $signingKey = self::getSigningKey($uuid); + if($signingKey === null) + { + return false; + } + + return Cryptography::verifyMessage($message, $signature, $signingKey->getPublicKey()); + } + } \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/SigningKeyRecord.php b/src/Socialbox/Objects/Database/SigningKeyRecord.php new file mode 100644 index 0000000..c00bc3e --- /dev/null +++ b/src/Socialbox/Objects/Database/SigningKeyRecord.php @@ -0,0 +1,171 @@ +peerUuid = $data['peer_uuid']; + $this->uuid = $data['uuid']; + $this->name = $data['name'] ?? null; + $this->publicKey = $data['public_key']; + $this->state = SigningKeyState::tryFrom($data['state']); + + if(is_int($data['expires'])) + { + $this->expires = $data['expires']; + } + elseif($data['expires'] instanceof DateTime) + { + $this->expires = $data['expires']->getTimestamp(); + } + else + { + throw new InvalidArgumentException('Invalid expires value'); + } + + if(is_int($data['created'])) + { + $this->created = $data['created']; + } + elseif($data['created'] instanceof DateTime) + { + $this->created = $data['created']->getTimestamp(); + } + else + { + throw new InvalidArgumentException('Invalid created value'); + } + } + + /** + * Retrieves the UUID of the peer. + * + * @return string The UUID of the peer. + */ + public function getPeerUuid(): string + { + return $this->peerUuid; + } + + /** + * Retrieves the UUID associated with this instance. + * + * @return string The UUID as a string. + */ + public function getUuid(): string + { + return $this->uuid; + } + + /** + * Retrieves the name. + * + * @return string|null The name, or null if not set. + */ + public function getName(): ?string + { + return $this->name; + } + + /** + * Retrieves the public key. + * + * @return string The public key. + */ + public function getPublicKey(): string + { + return $this->publicKey; + } + + /** + * Retrieves the current state of the signing key. + * + * @return SigningKeyState The state of the signing key. + */ + public function getState(): SigningKeyState + { + return $this->state; + } + + /** + * Retrieves the expiration timestamp. + * + * @return int The expiration timestamp as an integer. + */ + public function getExpires(): int + { + return $this->expires; + } + + /** + * + * @return int Returns the created timestamp as an integer. + */ + public function getCreated(): int + { + return $this->created; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): SigningKeyRecord + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'peer_uuid' => $this->peerUuid, + 'uuid' => $this->uuid, + 'name' => $this->name, + 'public_key' => $this->publicKey, + 'state' => $this->state->value, + 'expires' => (new DateTime())->setTimestamp($this->expires), + 'created' => (new DateTime())->setTimestamp($this->created) + ]; + } + + /** + * Converts the current signing key record to its standard format. + * + * @return SigningKey The signing key in its standard format. + */ + public function toStandard(): SigningKey + { + return SigningKey::fromSigningKeyRecord($this); + } + } \ No newline at end of file diff --git a/src/Socialbox/Objects/Standard/SigningKey.php b/src/Socialbox/Objects/Standard/SigningKey.php new file mode 100644 index 0000000..0dd7757 --- /dev/null +++ b/src/Socialbox/Objects/Standard/SigningKey.php @@ -0,0 +1,174 @@ +uuid = $data['uuid']; + $this->name = $data['name'] ?? null; + $this->publicKey = $data['public_key']; + $this->state = SigningKeyState::from($data['state']); + + if(is_int($data['expires'])) + { + $this->expires = $data['expires']; + } + elseif($data['expires'] instanceof DateTime) + { + $this->expires = $data['expires']->getTimestamp(); + } + else + { + throw new InvalidArgumentException('Invalid expires value'); + } + + if(is_int($data['created'])) + { + $this->created = $data['created']; + } + elseif($data['created'] instanceof DateTime) + { + $this->created = $data['created']->getTimestamp(); + } + else + { + throw new InvalidArgumentException('Invalid created value'); + } + } + + /** + * Retrieves the UUID associated with this instance. + * + * @return string The UUID as a string. + */ + public function getUuid(): string + { + return $this->uuid; + } + + /** + * Retrieves the name associated with this instance. + * + * @return string|null The name as a string, or null if not set. + */ + public function getName(): ?string + { + return $this->name; + } + + /** + * + * Retrieves the public key. + * + * @return string The public key. + */ + public function getPublicKey(): string + { + return $this->publicKey; + } + + /** + * Retrieves the current state of the signing key. + * + * @return SigningKeyState The current state of the signing key. + */ + public function getState(): SigningKeyState + { + if(time() > $this->expires) + { + return SigningKeyState::EXPIRED; + } + + return $this->state; + } + + /** + * Retrieves the expiration time. + * + * @return int The expiration time as an integer. + */ + public function getExpires(): int + { + return $this->expires; + } + + /** + * + * @return int The timestamp representing the creation time. + */ + public function getCreated(): int + { + return $this->created; + } + + /** + * Creates a new SigningKey instance from a SigningKeyRecord. + * + * @param SigningKeyRecord $record The record containing the signing key data. + * @return SigningKey An instance of SigningKey populated with data from the provided record. + */ + public static function fromSigningKeyRecord(SigningKeyRecord $record): SigningKey + { + return new self([ + 'uuid' => $record->getUuid(), + 'name' => $record->getName(), + 'public_key' => $record->getPublicKey(), + 'state' => $record->getState(), + 'expires' => $record->getExpires(), + 'created' => $record->getCreated() + ]); + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): SigningKey + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'uuid' => $this->uuid, + 'name' => $this->name, + 'public_key' => $this->publicKey, + 'state' => $this->state->value, + 'expires' => $this->expires, + 'created' => $this->created + ]; + } + } \ No newline at end of file From b9b7b23e9e898958946f660aa9d0729c7a1cc97f Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 3 Jan 2025 18:31:02 -0500 Subject: [PATCH 087/420] Set SQL dialect to MariaDB for SigningKeysManager.php --- .idea/sqldialects.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml index db6a2e3..a2d3934 100644 --- a/.idea/sqldialects.xml +++ b/.idea/sqldialects.xml @@ -11,6 +11,7 @@ + \ No newline at end of file From e9269a24fc9f9ef781fd8a715a3f95ac5b04f4c9 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 3 Jan 2025 21:22:02 -0500 Subject: [PATCH 088/420] Implement session inactivity expiration handling. --- src/Socialbox/Classes/Configuration.php | 3 +++ .../Classes/Configuration/PoliciesConfiguration.php | 10 ++++++++++ src/Socialbox/Objects/Database/SessionRecord.php | 7 +++++++ src/Socialbox/Socialbox.php | 13 ++++++++++++- 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/Socialbox/Classes/Configuration.php b/src/Socialbox/Classes/Configuration.php index fcae5b0..2b0338a 100644 --- a/src/Socialbox/Classes/Configuration.php +++ b/src/Socialbox/Classes/Configuration.php @@ -123,6 +123,9 @@ // Server Policies // The maximum number of signing keys a peer can register onto the server at once $config->setDefault('policies.max_signing_keys', 20); + // The amount of time in seconds it takes before a session is considered expired due to inactivity + // Default: 12hours + $config->setDefault('policies.session_inactivity_expires', 43200); // Storage configuration $config->setDefault('storage.path', '/etc/socialbox'); // The main path for file storage diff --git a/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php b/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php index 90b356d..3d9bef0 100644 --- a/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php +++ b/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php @@ -5,10 +5,12 @@ class PoliciesConfiguration { private int $maxSigningKeys; + private int $sessionInactivityExpires; public function __construct(array $data) { $this->maxSigningKeys = $data['max_signing_keys']; + $this->sessionInactivityExpires = $data['session_inactivity_expires']; } /** @@ -18,4 +20,12 @@ { return $this->maxSigningKeys; } + + /** + * @return int + */ + public function getSessionInactivityExpires(): int + { + return $this->sessionInactivityExpires; + } } \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/SessionRecord.php b/src/Socialbox/Objects/Database/SessionRecord.php index ec21c7c..49091d4 100644 --- a/src/Socialbox/Objects/Database/SessionRecord.php +++ b/src/Socialbox/Objects/Database/SessionRecord.php @@ -3,6 +3,7 @@ namespace Socialbox\Objects\Database; use DateTime; + use Socialbox\Classes\Configuration; use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\SessionState; use Socialbox\Interfaces\SerializableInterface; @@ -165,6 +166,12 @@ */ public function getState(): SessionState { + $expires = time() + Configuration::getPoliciesConfiguration()->getSessionInactivityExpires(); + if($this->lastRequest !== null && $this->lastRequest->getTimestamp() > $expires) + { + return SessionState::EXPIRED; + } + return $this->state; } diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index 3895268..519b3ba 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -409,7 +409,18 @@ // Verify if the session is active if($session->getState() !== SessionState::ACTIVE) { - self::returnError(403, StandardError::FORBIDDEN, 'Session is not active'); + self::returnError(403, StandardError::FORBIDDEN, 'Session is not active (' . $session->getState()->value . ')'); + return; + } + + try + { + SessionManager::updateLastRequest($session->getUuid()); + } + catch (DatabaseOperationException $e) + { + Logger::getLogger()->error('Failed to update the last request time for the session', $e); + self::returnError(500, StandardError::INTERNAL_SERVER_ERROR, 'Failed to update the session', $e); return; } From f2ae3100deb3b44817c3dd24c1fd9e54282306e7 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 3 Jan 2025 21:22:40 -0500 Subject: [PATCH 089/420] Minor correction --- src/Socialbox/Objects/Database/SessionRecord.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Socialbox/Objects/Database/SessionRecord.php b/src/Socialbox/Objects/Database/SessionRecord.php index 49091d4..642fbe5 100644 --- a/src/Socialbox/Objects/Database/SessionRecord.php +++ b/src/Socialbox/Objects/Database/SessionRecord.php @@ -167,7 +167,7 @@ public function getState(): SessionState { $expires = time() + Configuration::getPoliciesConfiguration()->getSessionInactivityExpires(); - if($this->lastRequest !== null && $this->lastRequest->getTimestamp() > $expires) + if($this->lastRequest !== null && $this->lastRequest->getTimestamp() < $expires) { return SessionState::EXPIRED; } From 9ebf3f641f3e4dc6164705e46aadaf575ef7bcda Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 3 Jan 2025 21:38:33 -0500 Subject: [PATCH 090/420] Add expiration handling to sessions and refactor state conversion --- .../Managers/RegisteredPeerManager.php | 8 ++-- .../Objects/Database/SessionRecord.php | 27 +++++++---- .../Objects/Standard/SessionState.php | 46 +++++++++++++++++-- 3 files changed, 65 insertions(+), 16 deletions(-) diff --git a/src/Socialbox/Managers/RegisteredPeerManager.php b/src/Socialbox/Managers/RegisteredPeerManager.php index 0595ae5..960ba4d 100644 --- a/src/Socialbox/Managers/RegisteredPeerManager.php +++ b/src/Socialbox/Managers/RegisteredPeerManager.php @@ -2,6 +2,8 @@ namespace Socialbox\Managers; + use DateMalformedStringException; + use Exception; use InvalidArgumentException; use PDO; use PDOException; @@ -140,7 +142,7 @@ return new RegisteredPeerRecord($result); } - catch(PDOException | \DateMalformedStringException $e) + catch(Exception $e) { throw new DatabaseOperationException('Failed to get the peer from the database', $e); } @@ -175,7 +177,7 @@ return new RegisteredPeerRecord($result); } - catch(PDOException | \DateMalformedStringException $e) + catch(PDOException | DateMalformedStringException $e) { throw new DatabaseOperationException('Failed to get the peer from the database', $e); } @@ -446,7 +448,7 @@ return new SecurePasswordRecord($result); } - catch(PDOException | \DateMalformedStringException $e) + catch(PDOException | DateMalformedStringException $e) { throw new DatabaseOperationException('Failed to get the secure password record from the database', $e); } diff --git a/src/Socialbox/Objects/Database/SessionRecord.php b/src/Socialbox/Objects/Database/SessionRecord.php index 642fbe5..c4ca58d 100644 --- a/src/Socialbox/Objects/Database/SessionRecord.php +++ b/src/Socialbox/Objects/Database/SessionRecord.php @@ -6,6 +6,7 @@ use Socialbox\Classes\Configuration; use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\SessionState; + use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\RegisteredPeerManager; @@ -185,13 +186,26 @@ return $this->created; } + /** + * @return DateTime + */ + public function getExpires(): DateTime + { + return new DateTime('@' . time() + Configuration::getPoliciesConfiguration()->getSessionInactivityExpires()); + } + /** * Retrieves the list of flags associated with the current instance. * * @return array Returns an array of flags. */ - public function getFlags(): array + public function getFlags(bool $asString): array { + if($asString) + { + return array_map(fn(SessionFlags $flag) => $flag->value, $this->flags); + } + return $this->flags; } @@ -252,16 +266,9 @@ */ public function toStandardSessionState(): \Socialbox\Objects\Standard\SessionState { - return new \Socialbox\Objects\Standard\SessionState([ - 'uuid' => $this->uuid, - 'identified_as' => RegisteredPeerManager::getPeer($this->peerUuid)->getAddress(), - 'authenticated' => $this->authenticated, - 'flags' => $this->flags, - 'created' => $this->created - ]); + return \Socialbox\Objects\Standard\SessionState::fromSessionRecord($this); } - - + /** * @inheritDoc */ diff --git a/src/Socialbox/Objects/Standard/SessionState.php b/src/Socialbox/Objects/Standard/SessionState.php index d2352ae..735aac5 100644 --- a/src/Socialbox/Objects/Standard/SessionState.php +++ b/src/Socialbox/Objects/Standard/SessionState.php @@ -5,6 +5,8 @@ use DateTime; use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Interfaces\SerializableInterface; + use Socialbox\Managers\RegisteredPeerManager; + use Socialbox\Objects\Database\SessionRecord; class SessionState implements SerializableInterface { @@ -16,6 +18,7 @@ */ private ?array $flags; private int $created; + private int $expires; /** * Constructor for initializing the object with the provided data. @@ -57,6 +60,19 @@ { $this->created = time(); } + + if(is_int($data['expires'])) + { + $this->expires = $data['expires']; + } + elseif($data['expires'] instanceof DateTime) + { + $this->expires = $data['expires']->getTimestamp(); + } + else + { + $this->expires = time(); + } } /** @@ -130,6 +146,16 @@ return $this->created; } + /** + * Retrieves the expiration timestamp of the current instance. + * + * @return int The expiration timestamp as an integer. + */ + public function getExpires(): int + { + return $this->expires; + } + /** * Creates a new instance of SessionState from the provided array. * @@ -142,9 +168,22 @@ } /** - * Converts the current instance into an associative array. - * - * @return array An associative array representation of the instance, including UUID, identification, authentication status, flags, and creation date. + * @inheritDoc + */ + public static function fromSessionRecord(SessionRecord $sessionRecord): SessionState + { + return new self([ + 'uuid' => $sessionRecord->getUuid(), + 'identified_as' => RegisteredPeerManager::getPeer($sessionRecord->getPeerUuid())->getAddress(), + 'authenticated' => $sessionRecord->isAuthenticated(), + 'flags' => $sessionRecord->getFlags(true), + 'created' => $sessionRecord->getCreated()->getTimestamp(), + 'expires' => $sessionRecord->getExpires()->getTimestamp() + ]); + } + + /** + * @inheritDoc */ public function toArray(): array { @@ -154,6 +193,7 @@ 'authenticated' => $this->authenticated, 'flags' => $this->flags, 'created' => $this->created, + 'expires' => $this->expires ]; } } \ No newline at end of file From 484cd36ddf4098bc4a37d31e23ee15b0d14f4c70 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 3 Jan 2025 21:38:41 -0500 Subject: [PATCH 091/420] Add src directory as a source folder in project config --- .idea/socialbox-php.iml | 1 + 1 file changed, 1 insertion(+) diff --git a/.idea/socialbox-php.iml b/.idea/socialbox-php.iml index 11e3fb9..47ac8d8 100644 --- a/.idea/socialbox-php.iml +++ b/.idea/socialbox-php.iml @@ -2,6 +2,7 @@ + From cb1cc5ee1549d07b15c076baefc2bdcb2755acb6 Mon Sep 17 00:00:00 2001 From: netkas Date: Sat, 4 Jan 2025 15:32:25 -0500 Subject: [PATCH 092/420] Add docblock for createSession and refactor password check --- src/Socialbox/Managers/SessionManager.php | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Socialbox/Managers/SessionManager.php b/src/Socialbox/Managers/SessionManager.php index 88e4b57..d58d745 100644 --- a/src/Socialbox/Managers/SessionManager.php +++ b/src/Socialbox/Managers/SessionManager.php @@ -24,6 +24,21 @@ class SessionManager { + /** + * Creates a new session for a given peer and client details, and stores it in the database. + * + * @param RegisteredPeerRecord $peer The peer record for which the session is being created. + * @param string $clientName The name of the client application. + * @param string $clientVersion The version of the client application. + * @param string $clientPublicSigningKey The client's public signing key, which must be a valid Ed25519 key. + * @param string $clientPublicEncryptionKey The client's public encryption key, which must be a valid X25519 key. + * @param KeyPair $serverEncryptionKeyPair The server's key pair for encryption, including both public and private keys. + * + * @return string The UUID of the newly created session. + * + * @throws InvalidArgumentException If the provided public signing key or encryption key is invalid. + * @throws DatabaseOperationException If there is an error during the session creation in the database. + */ public static function createSession(RegisteredPeerRecord $peer, string $clientName, string $clientVersion, string $clientPublicSigningKey, string $clientPublicEncryptionKey, KeyPair $serverEncryptionKeyPair): string { if($clientPublicSigningKey === '' || Cryptography::validatePublicSigningKey($clientPublicSigningKey) === false) @@ -44,7 +59,7 @@ { $flags[] = SessionFlags::AUTHENTICATION_REQUIRED; - if(RegisteredPeerManager::getPasswordAuthentication($peer)) + if(PasswordManager::usesPassword($peer->getUuid())) { $flags[] = SessionFlags::VER_PASSWORD; } From b04de2f2a7bd033f94194294d3758b1d3e7abf61 Mon Sep 17 00:00:00 2001 From: netkas Date: Sat, 4 Jan 2025 15:32:42 -0500 Subject: [PATCH 093/420] Add methods for deleting and updating peer information --- .../SettingsDeleteDisplayName.php | 39 +++ src/Socialbox/Classes/Validator.php | 10 + src/Socialbox/Enums/StandardMethods.php | 3 + .../Managers/RegisteredPeerManager.php | 252 ++++++++++++++++-- .../Objects/Database/RegisteredPeerRecord.php | 64 ++++- 5 files changed, 343 insertions(+), 25 deletions(-) create mode 100644 src/Socialbox/Classes/StandardMethods/SettingsDeleteDisplayName.php diff --git a/src/Socialbox/Classes/StandardMethods/SettingsDeleteDisplayName.php b/src/Socialbox/Classes/StandardMethods/SettingsDeleteDisplayName.php new file mode 100644 index 0000000..51b4181 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/SettingsDeleteDisplayName.php @@ -0,0 +1,39 @@ +isDisplayNameRequired()) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'A display name is required for this server'); + } + + try + { + // Set the password + RegisteredPeerManager::deleteDisplayName($request->getPeer()); + } + catch(Exception $e) + { + throw new StandardException('Failed to set password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/Validator.php b/src/Socialbox/Classes/Validator.php index 9fe24a0..4d0bda5 100644 --- a/src/Socialbox/Classes/Validator.php +++ b/src/Socialbox/Classes/Validator.php @@ -34,4 +34,14 @@ class Validator return preg_match(self::USERNAME_PATTERN, $username) === 1; } + /** + * Validates whether a given phone number conforms to the required format. + * + * @param string $phoneNumber The phone number to validate. Must start with a "+" followed by 1 to 15 digits. + * @return bool Returns true if the phone number is valid according to the format, otherwise false. + */ + public static function validatePhoneNumber(string $phoneNumber): bool + { + return preg_match("/^\+[0-9]{1,15}$/", $phoneNumber) === 1; + } } \ No newline at end of file diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index 752c3e8..3ffb879 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -11,6 +11,7 @@ use Socialbox\Classes\StandardMethods\GetTermsOfService; use Socialbox\Classes\StandardMethods\Ping; use Socialbox\Classes\StandardMethods\SettingsAddSigningKey; + use Socialbox\Classes\StandardMethods\SettingsDeleteDisplayName; use Socialbox\Classes\StandardMethods\SettingsGetSigningKeys; use Socialbox\Classes\StandardMethods\SettingsSetDisplayName; use Socialbox\Classes\StandardMethods\SettingsSetPassword; @@ -55,6 +56,7 @@ case SETTINGS_SET_PASSWORD = 'settingsSetPassword'; case SETTINGS_SET_OTP = 'settingsSetOtp'; case SETTINGS_SET_DISPLAY_NAME = 'settingsSetDisplayName'; + case SETTINGS_DELETE_DISPLAY_NAME = 'settingsDeleteDisplayName'; case SETTINGS_SET_DISPLAY_PICTURE = 'settingsSetDisplayPicture'; case SETTINGS_SET_EMAIL = 'settingsSetEmail'; case SETTINGS_SET_PHONE = 'settingsSetPhone'; @@ -90,6 +92,7 @@ self::SETTINGS_SET_PASSWORD => SettingsSetPassword::execute($request, $rpcRequest), self::SETTINGS_SET_DISPLAY_NAME => SettingsSetDisplayName::execute($request, $rpcRequest), + self::SETTINGS_DELETE_DISPLAY_NAME => SettingsDeleteDisplayName::execute($request, $rpcRequest), self::SETTINGS_ADD_SIGNING_KEY => SettingsAddSigningKey::execute($request, $rpcRequest), self::SETTINGS_GET_SIGNING_KEYS => SettingsGetSigningKeys::execute($request, $rpcRequest), diff --git a/src/Socialbox/Managers/RegisteredPeerManager.php b/src/Socialbox/Managers/RegisteredPeerManager.php index 960ba4d..0924d45 100644 --- a/src/Socialbox/Managers/RegisteredPeerManager.php +++ b/src/Socialbox/Managers/RegisteredPeerManager.php @@ -2,7 +2,6 @@ namespace Socialbox\Managers; - use DateMalformedStringException; use Exception; use InvalidArgumentException; use PDO; @@ -10,10 +9,10 @@ use Socialbox\Classes\Configuration; use Socialbox\Classes\Database; use Socialbox\Classes\Logger; + use Socialbox\Classes\Validator; use Socialbox\Enums\Flags\PeerFlags; use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Objects\Database\RegisteredPeerRecord; - use Socialbox\Objects\Database\SecurePasswordRecord; use Socialbox\Objects\PeerAddress; use Symfony\Component\Uid\Uuid; @@ -177,7 +176,7 @@ return new RegisteredPeerRecord($result); } - catch(PDOException | DateMalformedStringException $e) + catch(Exception $e) { throw new DatabaseOperationException('Failed to get the peer from the database', $e); } @@ -365,6 +364,41 @@ } } + /** + * Deletes the display name of a registered peer identified by a unique identifier or RegisteredPeerRecord object. + * + * @param string|RegisteredPeerRecord $peer The unique identifier of the registered peer, or an instance of RegisteredPeerRecord. + * @return void + * @throws InvalidArgumentException If the peer is external and its display name cannot be deleted. + * @throws DatabaseOperationException If there is an error during the database operation. + */ + public static function deleteDisplayName(string|RegisteredPeerRecord $peer): void + { + if(is_string($peer)) + { + $peer = self::getPeer($peer); + } + + if($peer->isExternal()) + { + throw new InvalidArgumentException('Cannot delete the display name of an external peer'); + } + + Logger::getLogger()->verbose(sprintf("Deleting display name of peer %s", $peer->getUuid())); + + try + { + $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET display_name=NULL WHERE uuid=?'); + $uuid = $peer->getUuid(); + $statement->bindParam(1, $uuid); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to delete the display name of the peer in the database', $e); + } + } + /** * Updates the display picture of a registered peer in the database. * @@ -420,37 +454,211 @@ } /** - * Retrieves the password authentication record associated with the given unique peer identifier or a RegisteredPeerRecord object. + * Deletes the display picture of a registered peer based on the given unique identifier or RegisteredPeerRecord object. * - * @param string|RegisteredPeerRecord $peerUuid The unique identifier of the peer, or an instance of RegisteredPeerRecord. - * @return SecurePasswordRecord|null Returns a SecurePasswordRecord object if a password authentication record exists, otherwise null. + * @param string|RegisteredPeerRecord $peer The unique identifier of the registered peer, or an instance of RegisteredPeerRecord. + * @return void + * @throws InvalidArgumentException If the peer is external and its display picture cannot be deleted. * @throws DatabaseOperationException If there is an error during the database operation. */ - public static function getPasswordAuthentication(string|RegisteredPeerRecord $peerUuid): ?SecurePasswordRecord + public static function deleteDisplayPicture(string|RegisteredPeerRecord $peer): void { - if($peerUuid instanceof RegisteredPeerRecord) + if(is_string($peer)) { - $peerUuid = $peerUuid->getUuid(); + $peer = self::getPeer($peer); } + if($peer->isExternal()) + { + throw new InvalidArgumentException('Cannot delete the display picture of an external peer'); + } + + Logger::getLogger()->verbose(sprintf("Deleting display picture of peer %s", $peer->getUuid())); + try { - $statement = Database::getConnection()->prepare('SELECT * FROM `authentication_passwords` WHERE peer_uuid=?'); - $statement->bindParam(1, $peerUuid); + $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET display_picture=NULL WHERE uuid=?'); + $uuid = $peer->getUuid(); + $statement->bindParam(1, $uuid); $statement->execute(); - - $result = $statement->fetch(PDO::FETCH_ASSOC); - - if($result === false) - { - return null; - } - - return new SecurePasswordRecord($result); } - catch(PDOException | DateMalformedStringException $e) + catch(PDOException $e) { - throw new DatabaseOperationException('Failed to get the secure password record from the database', $e); + throw new DatabaseOperationException('Failed to delete the display picture of the peer in the database', $e); + } + } + + /** + * Updates the email address of a registered peer. + * + * @param string|RegisteredPeerRecord $peer The unique identifier of the peer, or an instance of RegisteredPeerRecord. + * @param string $emailAddress The new email address to be assigned to the peer. + * @return void + * @throws InvalidArgumentException If the email address is empty, exceeds 256 characters, is not a valid email format, or if the peer is external. + * @throws DatabaseOperationException If there is an error during the database operation. + */ + public static function updateEmailAddress(string|RegisteredPeerRecord $peer, string $emailAddress): void + { + if(empty($emailAddress)) + { + throw new InvalidArgumentException('The email address cannot be empty'); + } + + if(strlen($emailAddress) > 256) + { + throw new InvalidArgumentException('The email address cannot exceed 256 characters'); + } + + if(filter_var($emailAddress, FILTER_VALIDATE_EMAIL) === false) + { + throw new InvalidArgumentException('The email address is not valid'); + } + + if(is_string($peer)) + { + $peer = self::getPeer($peer); + } + + if($peer->isExternal()) + { + throw new InvalidArgumentException('Cannot update the email address of an external peer'); + } + + Logger::getLogger()->verbose(sprintf("Updating email address of peer %s to %s", $peer->getUuid(), $emailAddress)); + + try + { + $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET email_address=? WHERE uuid=?'); + $statement->bindParam(1, $emailAddress); + $uuid = $peer->getUuid(); + $statement->bindParam(2, $uuid); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to update the email address of the peer in the database', $e); + } + } + + /** + * Deletes the email address of a registered peer identified by either a unique identifier or a RegisteredPeerRecord object. + * + * @param string|RegisteredPeerRecord $peer The unique identifier of the registered peer, or an instance of RegisteredPeerRecord. + * @return void + * @throws InvalidArgumentException If the peer is external and its email address cannot be deleted. + * @throws DatabaseOperationException If there is an error during the database operation. + */ + public static function deleteEmailAddress(string|RegisteredPeerRecord $peer): void + { + if(is_string($peer)) + { + $peer = self::getPeer($peer); + } + + if($peer->isExternal()) + { + throw new InvalidArgumentException('Cannot delete the email address of an external peer'); + } + + Logger::getLogger()->verbose(sprintf("Deleting email address of peer %s", $peer->getUuid())); + + try + { + $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET email_address=NULL WHERE uuid=?'); + $uuid = $peer->getUuid(); + $statement->bindParam(1, $uuid); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to delete the email address of the peer in the database', $e); + } + } + + /** + * Updates the phone number of the specified registered peer. + * + * @param string|RegisteredPeerRecord $peer The unique identifier of the registered peer, or an instance of RegisteredPeerRecord. + * @param string $phoneNumber The new phone number to be set for the peer. + * @return void + * @throws InvalidArgumentException If the phone number is empty, exceeds 16 characters, is invalid, or if the peer is external. + * @throws DatabaseOperationException If there is an error during the database operation. + */ + public static function updatePhoneNumber(string|RegisteredPeerRecord $peer, string $phoneNumber): void + { + if(empty($phoneNumber)) + { + throw new InvalidArgumentException('The phone number cannot be empty'); + } + + if(strlen($phoneNumber) > 16) + { + throw new InvalidArgumentException('The phone number cannot exceed 16 characters'); + } + + if(!Validator::validatePhoneNumber($phoneNumber)) + { + throw new InvalidArgumentException('The phone number is not valid'); + } + + if(is_string($peer)) + { + $peer = self::getPeer($peer); + } + + if($peer->isExternal()) + { + throw new InvalidArgumentException('Cannot update the phone number of an external peer'); + } + + Logger::getLogger()->verbose(sprintf("Updating phone number of peer %s to %s", $peer->getUuid(), $phoneNumber)); + + try + { + $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET phone_number=? WHERE uuid=?'); + $statement->bindParam(1, $phoneNumber); + $uuid = $peer->getUuid(); + $statement->bindParam(2, $uuid); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to update the phone number of the peer in the database', $e); + } + } + + /** + * Deletes the phone number of a registered peer based on the given unique identifier or RegisteredPeerRecord object. + * + * @param string|RegisteredPeerRecord $peer The unique identifier of the registered peer, or an instance of RegisteredPeerRecord. + * @return void This method does not return a value. + * @throws InvalidArgumentException If the peer is external and its phone number cannot be deleted. + * @throws DatabaseOperationException If there is an error during the database operation. + */ + public static function deletePhoneNumber(string|RegisteredPeerRecord $peer): void + { + if(is_string($peer)) + { + $peer = self::getPeer($peer); + } + + if($peer->isExternal()) + { + throw new InvalidArgumentException('Cannot delete the phone number of an external peer'); + } + + Logger::getLogger()->verbose(sprintf("Deleting phone number of peer %s", $peer->getUuid())); + + try + { + $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET phone_number=NULL WHERE uuid=?'); + $uuid = $peer->getUuid(); + $statement->bindParam(1, $uuid); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to delete the phone number of the peer in the database', $e); } } } \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/RegisteredPeerRecord.php b/src/Socialbox/Objects/Database/RegisteredPeerRecord.php index 4f6171e..56ad984 100644 --- a/src/Socialbox/Objects/Database/RegisteredPeerRecord.php +++ b/src/Socialbox/Objects/Database/RegisteredPeerRecord.php @@ -16,6 +16,9 @@ private string $server; private ?string $displayName; private ?string $displayPicture; + private ?string $emailAddress; + private ?string $phoneNumber; + private ?DateTime $birthday; /** * @var PeerFlags[] */ @@ -27,7 +30,6 @@ * Constructor for initializing class properties from provided data. * * @param array $data Array containing initialization data. - * @throws \DateMalformedStringException */ public function __construct(array $data) { @@ -36,6 +38,25 @@ $this->server = $data['server']; $this->displayName = $data['display_name'] ?? null; $this->displayPicture = $data['display_picture'] ?? null; + $this->emailAddress = $data['email_address'] ?? null; + $this->phoneNumber = $data['phone_number'] ?? null; + + if(is_int($data['birthday'])) + { + $this->birthday = (new DateTime())->setTimestamp($data['birthday']); + } + elseif(is_string($data['birthday'])) + { + $this->birthday = new DateTime($data['birthday']); + } + elseif($data['birthday'] instanceof DateTime) + { + $this->birthday = $data['birthday']; + } + else + { + $this->birthday = null; + } if($data['flags']) { @@ -48,13 +69,17 @@ $this->enabled = $data['enabled']; - if (is_string($data['created'])) + if(is_int($data['created'])) + { + $this->created = (new DateTime())->setTimestamp($data['created']); + } + elseif(is_string($data['created'])) { $this->created = new DateTime($data['created']); } else { - $this->created = $data['created']; + throw new \InvalidArgumentException("The created field must be a valid timestamp or date string."); } } @@ -118,6 +143,36 @@ return $this->displayPicture; } + /** + * Retrieves the email address. + * + * @return string|null The email address if set, or null otherwise. + */ + public function getEmailAddress(): ?string + { + return $this->emailAddress; + } + + /** + * Retrieves the phone number. + * + * @return string|null The phone number if set, or null otherwise. + */ + public function getPhoneNumber(): ?string + { + return $this->phoneNumber; + } + + /** + * Retrieves the birthday. + * + * @return DateTime|null The birthday if set, or null otherwise. + */ + public function getBirthday(): ?DateTime + { + return $this->birthday; + } + /** * Retrieves the flags. * @@ -221,6 +276,9 @@ 'server' => $this->server, 'display_name' => $this->displayName, 'display_picture' => $this->displayPicture, + 'email_address' => $this->emailAddress, + 'phone_number' => $this->phoneNumber, + 'birthday' => $this->birthday?->getTimestamp(), 'flags' => PeerFlags::toString($this->flags), 'enabled' => $this->enabled, 'created' => $this->created From 8b9896f196a482a30eff032a2d5b2b61b8a92406 Mon Sep 17 00:00:00 2001 From: netkas Date: Sat, 4 Jan 2025 15:34:40 -0500 Subject: [PATCH 094/420] Add default value for getFlags() $asString parameter --- src/Socialbox/Objects/Database/SessionRecord.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Socialbox/Objects/Database/SessionRecord.php b/src/Socialbox/Objects/Database/SessionRecord.php index c4ca58d..37145d1 100644 --- a/src/Socialbox/Objects/Database/SessionRecord.php +++ b/src/Socialbox/Objects/Database/SessionRecord.php @@ -195,11 +195,12 @@ } /** - * Retrieves the list of flags associated with the current instance. + * Retrieves the flags associated with the session. * - * @return array Returns an array of flags. + * @param bool $asString Determines whether the flags should be returned as strings. + * @return array An array of session flags, either as objects or strings depending on the $asString parameter. */ - public function getFlags(bool $asString): array + public function getFlags(bool $asString=false): array { if($asString) { From 85814913e47eee9e3cb02f593ca5740d7dcad624 Mon Sep 17 00:00:00 2001 From: netkas Date: Sun, 5 Jan 2025 01:23:43 -0500 Subject: [PATCH 095/420] Update password handling and session methods --- src/Socialbox/Classes/Cryptography.php | 14 ++- .../StandardMethods/GetSessionState.php | 6 -- .../SettingsDeletePassword.php | 53 ++++++++++ .../StandardMethods/SettingsSetPassword.php | 17 ++-- .../SettingsUpdatePassword.php | 57 +++++++++++ src/Socialbox/Enums/StandardError.php | 12 +-- src/Socialbox/Enums/StandardMethods.php | 98 ++++++++++++------- src/Socialbox/Managers/PasswordManager.php | 26 +++++ src/Socialbox/Managers/SessionManager.php | 8 +- .../Objects/Database/RegisteredPeerRecord.php | 7 +- .../Objects/Database/SessionRecord.php | 11 +++ 11 files changed, 239 insertions(+), 70 deletions(-) create mode 100644 src/Socialbox/Classes/StandardMethods/SettingsDeletePassword.php create mode 100644 src/Socialbox/Classes/StandardMethods/SettingsUpdatePassword.php diff --git a/src/Socialbox/Classes/Cryptography.php b/src/Socialbox/Classes/Cryptography.php index 8b6fdee..dc1bb00 100644 --- a/src/Socialbox/Classes/Cryptography.php +++ b/src/Socialbox/Classes/Cryptography.php @@ -642,7 +642,6 @@ * * @param string $hash The hash string to be validated. * @return bool Returns true if the hash is valid and meets current security standards. - * @throws CryptographyException If the hash format is invalid or does not meet security requirements. */ public static function validatePasswordHash(string $hash): bool { @@ -652,22 +651,21 @@ $argon2id_pattern = '/^\$argon2id\$v=\d+\$m=\d+,t=\d+,p=\d+\$[A-Za-z0-9+\/=]+\$[A-Za-z0-9+\/=]+$/D'; if (!preg_match($argon2id_pattern, $hash)) { - throw new CryptographyException("Invalid hash format"); + return false; } // Step 2: Check if it needs rehashing (validates the hash structure) if (sodium_crypto_pwhash_str_needs_rehash($hash, SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE)) { - throw new CryptographyException("Hash does not meet current security requirements"); + return false; } - - // If all checks pass, the hash is valid. - return true; } - catch (Exception $e) + catch (Exception) { - throw new CryptographyException("Invalid hash: " . $e->getMessage()); + return false; } + + return true; } /** diff --git a/src/Socialbox/Classes/StandardMethods/GetSessionState.php b/src/Socialbox/Classes/StandardMethods/GetSessionState.php index 11f4d2d..dc45964 100644 --- a/src/Socialbox/Classes/StandardMethods/GetSessionState.php +++ b/src/Socialbox/Classes/StandardMethods/GetSessionState.php @@ -3,7 +3,6 @@ namespace Socialbox\Classes\StandardMethods; use Socialbox\Abstracts\Method; - use Socialbox\Enums\StandardError; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\ClientRequest; use Socialbox\Objects\RpcRequest; @@ -16,11 +15,6 @@ */ public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { - if($request->getSessionUuid() === null) - { - return $rpcRequest->produceError(StandardError::SESSION_REQUIRED); - } - return $rpcRequest->produceResponse($request->getSession()->toStandardSessionState()); } } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/SettingsDeletePassword.php b/src/Socialbox/Classes/StandardMethods/SettingsDeletePassword.php new file mode 100644 index 0000000..92259a4 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/SettingsDeletePassword.php @@ -0,0 +1,53 @@ +isPasswordRequired()) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'A password is required for this server'); + } + + try + { + if (!PasswordManager::usesPassword($request->getPeer()->getUuid())) + { + return $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, "Cannot update password when one isn't already set, use 'settingsSetPassword' instead"); + } + } + catch (DatabaseOperationException $e) + { + throw new StandardException('Failed to check password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + try + { + // Set the password + PasswordManager::updatePassword($request->getPeer(), $rpcRequest->getParameter('password')); + } + catch(Exception $e) + { + throw new StandardException('Failed to set password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/SettingsSetPassword.php b/src/Socialbox/Classes/StandardMethods/SettingsSetPassword.php index 8241343..1c180dd 100644 --- a/src/Socialbox/Classes/StandardMethods/SettingsSetPassword.php +++ b/src/Socialbox/Classes/StandardMethods/SettingsSetPassword.php @@ -4,6 +4,7 @@ use Exception; use Socialbox\Abstracts\Method; + use Socialbox\Classes\Cryptography; use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; @@ -26,16 +27,16 @@ return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'password' parameter"); } - if(!preg_match('/^[a-f0-9]{128}$/', $rpcRequest->getParameter('password'))) + if(!Cryptography::validatePasswordHash($rpcRequest->getParameter('password'))) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Invalid 'password' parameter, must be sha512 hexadecimal hash"); + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Invalid 'password' parameter, must be a valid argon2id hash"); } try { if (PasswordManager::usesPassword($request->getPeer()->getUuid())) { - return $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, "Cannot set password when one is already set, use 'settingsChangePassword' instead"); + return $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, "Cannot set password when one is already set, use 'settingsUpdatePassword' instead"); } } catch (DatabaseOperationException $e) @@ -48,11 +49,11 @@ // Set the password PasswordManager::setPassword($request->getPeer(), $rpcRequest->getParameter('password')); - // Remove the SET_PASSWORD flag - SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::SET_PASSWORD]); - - // Check & update the session flow - SessionManager::updateFlow($request->getSession()); + // Remove the SET_PASSWORD flag & update the session flow if necessary + if($request->getSession()->flagExists(SessionFlags::SET_PASSWORD)) + { + SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_PASSWORD]); + } } catch(Exception $e) { diff --git a/src/Socialbox/Classes/StandardMethods/SettingsUpdatePassword.php b/src/Socialbox/Classes/StandardMethods/SettingsUpdatePassword.php new file mode 100644 index 0000000..eb6b62a --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/SettingsUpdatePassword.php @@ -0,0 +1,57 @@ +containsParameter('password')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'password' parameter"); + } + + if(!Cryptography::validatePasswordHash($rpcRequest->getParameter('password'))) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Invalid 'password' parameter, must be a valid argon2id hash"); + } + + try + { + if (!PasswordManager::usesPassword($request->getPeer()->getUuid())) + { + return $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, "Cannot update password when one isn't already set, use 'settingsSetPassword' instead"); + } + } + catch (DatabaseOperationException $e) + { + throw new StandardException('Failed to check password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + try + { + // Set the password + PasswordManager::updatePassword($request->getPeer(), $rpcRequest->getParameter('password')); + } + catch(Exception $e) + { + throw new StandardException('Failed to set password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file diff --git a/src/Socialbox/Enums/StandardError.php b/src/Socialbox/Enums/StandardError.php index ef54bfd..3c0fbc6 100644 --- a/src/Socialbox/Enums/StandardError.php +++ b/src/Socialbox/Enums/StandardError.php @@ -7,7 +7,7 @@ enum StandardError : int // Fallback Codes case UNKNOWN = -1; - // Server/Request Errors + // Generic Errors case INTERNAL_SERVER_ERROR = -100; case SERVER_UNAVAILABLE = -101; case BAD_REQUEST = -102; @@ -26,12 +26,7 @@ enum StandardError : int // Authentication/Cryptography Errors case INVALID_PUBLIC_KEY = -4000; // * - - case SESSION_REQUIRED = -5001; // * case SESSION_NOT_FOUND = -5002; // * - case SESSION_EXPIRED = -5003; // * - case SESSION_DHE_REQUIRED = -5004; // * - case ALREADY_AUTHENTICATED = -6005; case UNSUPPORTED_AUTHENTICATION_TYPE = -6006; case AUTHENTICATION_REQUIRED = -6007; @@ -55,7 +50,7 @@ enum StandardError : int { return match ($this) { - self::UNKNOWN => 'Unknown Error', + default => 'Unknown Error', self::RPC_METHOD_NOT_FOUND => 'The request method was not found', self::RPC_INVALID_ARGUMENTS => 'The request method contains one or more invalid arguments', @@ -68,7 +63,6 @@ enum StandardError : int self::ALREADY_AUTHENTICATED => 'The action cannot be preformed while authenticated', self::AUTHENTICATION_REQUIRED => 'Authentication is required to preform this action', self::SESSION_NOT_FOUND => 'The requested session UUID was not found', - self::SESSION_REQUIRED => 'A session is required to preform this action', self::REGISTRATION_DISABLED => 'Registration is disabled on the server', self::CAPTCHA_NOT_AVAILABLE => 'Captcha is not available', self::INCORRECT_CAPTCHA_ANSWER => 'The Captcha answer is incorrect', @@ -79,8 +73,6 @@ enum StandardError : int self::USERNAME_ALREADY_EXISTS => 'The given username already exists on the network', self::NOT_REGISTERED => 'The given username is not registered on the server', self::METHOD_NOT_ALLOWED => 'The requested method is not allowed', - self::SESSION_EXPIRED => 'The session has expired', - self::SESSION_DHE_REQUIRED => 'The session requires DHE to be established', }; } diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index 3ffb879..1f33ef5 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -2,6 +2,7 @@ namespace Socialbox\Enums; + use Socialbox\Classes\Configuration; use Socialbox\Classes\StandardMethods\AcceptCommunityGuidelines; use Socialbox\Classes\StandardMethods\AcceptPrivacyPolicy; use Socialbox\Classes\StandardMethods\AcceptTermsOfService; @@ -12,9 +13,11 @@ use Socialbox\Classes\StandardMethods\Ping; use Socialbox\Classes\StandardMethods\SettingsAddSigningKey; use Socialbox\Classes\StandardMethods\SettingsDeleteDisplayName; + use Socialbox\Classes\StandardMethods\SettingsDeletePassword; use Socialbox\Classes\StandardMethods\SettingsGetSigningKeys; use Socialbox\Classes\StandardMethods\SettingsSetDisplayName; use Socialbox\Classes\StandardMethods\SettingsSetPassword; + use Socialbox\Classes\StandardMethods\SettingsUpdatePassword; use Socialbox\Classes\StandardMethods\VerificationAnswerImageCaptcha; use Socialbox\Classes\StandardMethods\VerificationGetImageCaptcha; use Socialbox\Enums\Flags\SessionFlags; @@ -54,6 +57,8 @@ case VERIFICATION_ANSWER_EXTERNAL_URL = 'verificationAnswerExternalUrl'; case SETTINGS_SET_PASSWORD = 'settingsSetPassword'; + case SETTINGS_UPDATE_PASSWORD = 'settingsUpdatePassword'; + case SETTINGS_DELETE_PASSWORD = 'settingsDeletePassword'; case SETTINGS_SET_OTP = 'settingsSetOtp'; case SETTINGS_SET_DISPLAY_NAME = 'settingsSetDisplayName'; case SETTINGS_DELETE_DISPLAY_NAME = 'settingsDeleteDisplayName'; @@ -91,6 +96,8 @@ self::VERIFICATION_ANSWER_IMAGE_CAPTCHA => VerificationAnswerImageCaptcha::execute($request, $rpcRequest), self::SETTINGS_SET_PASSWORD => SettingsSetPassword::execute($request, $rpcRequest), + self::SETTINGS_UPDATE_PASSWORD => SettingsUpdatePassword::execute($request, $rpcRequest), + self::SETTINGS_DELETE_PASSWORD => SettingsDeletePassword::execute($request, $rpcRequest), self::SETTINGS_SET_DISPLAY_NAME => SettingsSetDisplayName::execute($request, $rpcRequest), self::SETTINGS_DELETE_DISPLAY_NAME => SettingsDeleteDisplayName::execute($request, $rpcRequest), @@ -138,43 +145,28 @@ $session = $clientRequest->getSession(); - // If the flag `VER_PRIVACY_POLICY` is set, then the user can accept the privacy policy - if($session->flagExists(SessionFlags::VER_PRIVACY_POLICY)) + // If the session is external (eg; coming from a different server) + // Servers will have their own access control mechanisms + if($session->isExternal()) { - $methods[] = self::ACCEPT_PRIVACY_POLICY; + // TODO: Implement server access control } - - // If the flag `VER_TERMS_OF_SERVICE` is set, then the user can accept the terms of service - if($session->flagExists(SessionFlags::VER_TERMS_OF_SERVICE)) + // If the session is authenticated, then allow additional method calls + elseif($session->isAuthenticated()) { - $methods[] = self::ACCEPT_TERMS_OF_SERVICE; - } + // These methods are always allowed for authenticated users + $methods = array_merge($methods, [ + self::SETTINGS_ADD_SIGNING_KEY, + self::SETTINGS_GET_SIGNING_KEYS, + self::SETTINGS_SET_DISPLAY_NAME, + self::SETTINGS_SET_PASSWORD, + ]); - // If the flag `VER_COMMUNITY_GUIDELINES` is set, then the user can accept the community guidelines - if($session->flagExists(SessionFlags::VER_COMMUNITY_GUIDELINES)) - { - $methods[] = self::ACCEPT_COMMUNITY_GUIDELINES; - } - - // If the flag `VER_IMAGE_CAPTCHA` is set, then the user has to get and answer an image captcha - if($session->flagExists(SessionFlags::VER_IMAGE_CAPTCHA)) - { - $methods[] = self::VERIFICATION_GET_IMAGE_CAPTCHA; - $methods[] = self::VERIFICATION_ANSWER_IMAGE_CAPTCHA; - } - - // If the flag `SET_PASSWORD` is set, then the user has to set a password - if(in_array(SessionFlags::SET_PASSWORD, $session->getFlags())) - { - $methods[] = self::SETTINGS_SET_PASSWORD; - } - - // If the user is authenticated, then allow additional method calls - if($session->isAuthenticated()) - { - // Authenticated users can always manage their signing keys - $methods[] = self::SETTINGS_ADD_SIGNING_KEY; - $methods[] = self::SETTINGS_GET_SIGNING_KEYS; + // Prevent the user from deleting their display name if it is required + if(!Configuration::getRegistrationConfiguration()->isDisplayNameRequired()) + { + $methods[] = self::SETTINGS_DELETE_DISPLAY_NAME; + } // Always allow the authenticated user to change their password if(!in_array(SessionFlags::SET_PASSWORD, $session->getFlags())) @@ -182,6 +174,46 @@ $methods[] = self::SETTINGS_SET_PASSWORD; } } + // If the session isn't authenticated nor a host, a limited set of methods is available + else + { + // If the flag `VER_PRIVACY_POLICY` is set, then the user can accept the privacy policy + if($session->flagExists(SessionFlags::VER_PRIVACY_POLICY)) + { + $methods[] = self::ACCEPT_PRIVACY_POLICY; + } + + // If the flag `VER_TERMS_OF_SERVICE` is set, then the user can accept the terms of service + if($session->flagExists(SessionFlags::VER_TERMS_OF_SERVICE)) + { + $methods[] = self::ACCEPT_TERMS_OF_SERVICE; + } + + // If the flag `VER_COMMUNITY_GUIDELINES` is set, then the user can accept the community guidelines + if($session->flagExists(SessionFlags::VER_COMMUNITY_GUIDELINES)) + { + $methods[] = self::ACCEPT_COMMUNITY_GUIDELINES; + } + + // If the flag `VER_IMAGE_CAPTCHA` is set, then the user has to get and answer an image captcha + if($session->flagExists(SessionFlags::VER_IMAGE_CAPTCHA)) + { + $methods[] = self::VERIFICATION_GET_IMAGE_CAPTCHA; + $methods[] = self::VERIFICATION_ANSWER_IMAGE_CAPTCHA; + } + + // If the flag `SET_PASSWORD` is set, then the user has to set a password + if($session->flagExists(SessionFlags::SET_PASSWORD)) + { + $methods[] = self::SETTINGS_SET_PASSWORD; + } + + // If the flag `SET_DISPLAY_NAME` is set, then the user has to set a display name + if($session->flagExists(SessionFlags::SET_DISPLAY_NAME)) + { + $methods[] = self::SETTINGS_SET_DISPLAY_NAME; + } + } return $methods; } diff --git a/src/Socialbox/Managers/PasswordManager.php b/src/Socialbox/Managers/PasswordManager.php index 313cc4f..0a6947c 100644 --- a/src/Socialbox/Managers/PasswordManager.php +++ b/src/Socialbox/Managers/PasswordManager.php @@ -115,6 +115,32 @@ } } + /** + * Deletes the stored password for a specific peer. + * + * @param string|RegisteredPeerRecord $peerUuid The unique identifier of the peer, or an instance of RegisteredPeerRecord. + * @return void + * @throws DatabaseOperationException If an error occurs during the database operation. + */ + public static function deletePassword(string|RegisteredPeerRecord $peerUuid): void + { + if($peerUuid instanceof RegisteredPeerRecord) + { + $peerUuid = $peerUuid->getUuid(); + } + + try + { + $stmt = Database::getConnection()->prepare('DELETE FROM authentication_passwords WHERE peer_uuid=:uuid'); + $stmt->bindParam(':uuid', $peerUuid); + $stmt->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('An error occurred while deleting the password', $e); + } + } + /** * Verifies a given password against a stored password hash for a specific peer. * diff --git a/src/Socialbox/Managers/SessionManager.php b/src/Socialbox/Managers/SessionManager.php index d58d745..ef36946 100644 --- a/src/Socialbox/Managers/SessionManager.php +++ b/src/Socialbox/Managers/SessionManager.php @@ -423,11 +423,12 @@ * Marks the session as complete if all necessary conditions are met. * * @param SessionRecord $session The session record to evaluate and potentially mark as complete. + * @param array $flagsToRemove An array of flags to remove from the session if it is marked as complete. + * @return void * @throws DatabaseOperationException If there is an error while updating the session in the database. * @throws StandardException If the session record cannot be found or if there is an error during retrieval. - * @return void */ - public static function updateFlow(SessionRecord $session): void + public static function updateFlow(SessionRecord $session, array $flagsToRemove=[]): void { // Don't do anything if the session is already authenticated if(!in_array(SessionFlags::REGISTRATION_REQUIRED, $session->getFlags()) || !in_array(SessionFlags::AUTHENTICATION_REQUIRED, $session->getFlags())) @@ -435,6 +436,9 @@ return; } + self::removeFlags($session->getUuid(), $flagsToRemove); + $session = self::getSession($session->getUuid()); + // Check if all registration/authentication requirements are met if(SessionFlags::isComplete($session->getFlags())) { diff --git a/src/Socialbox/Objects/Database/RegisteredPeerRecord.php b/src/Socialbox/Objects/Database/RegisteredPeerRecord.php index 56ad984..a88f48e 100644 --- a/src/Socialbox/Objects/Database/RegisteredPeerRecord.php +++ b/src/Socialbox/Objects/Database/RegisteredPeerRecord.php @@ -228,13 +228,14 @@ } /** - * Determines if the server is external. + * Determines if the user is considered external by checking if the username is 'host' and the server + * is not the same as the domain from the configuration. * - * @return bool True if the server is external, false otherwise. + * @return bool True if the user is external, false otherwise. */ public function isExternal(): bool { - return $this->server === 'host'; + return $this->username === 'host' && $this->server !== Configuration::getInstanceConfiguration()->getDomain(); } /** diff --git a/src/Socialbox/Objects/Database/SessionRecord.php b/src/Socialbox/Objects/Database/SessionRecord.php index 37145d1..6492528 100644 --- a/src/Socialbox/Objects/Database/SessionRecord.php +++ b/src/Socialbox/Objects/Database/SessionRecord.php @@ -260,6 +260,17 @@ return $this->clientVersion; } + /** + * Returns whether the session is external. + * + * @return bool True if the session is external, false otherwise. + * @throws DatabaseOperationException Thrown if the peer record cannot be retrieved. + */ + public function isExternal(): bool + { + return RegisteredPeerManager::getPeer($this->peerUuid)->isExternal(); + } + /** * Converts the current session state into a standard session state object. * From 2082225fb7c7103d72a66758551d605fd6ea5260 Mon Sep 17 00:00:00 2001 From: netkas Date: Sun, 5 Jan 2025 01:36:57 -0500 Subject: [PATCH 096/420] Add support for deleting display pictures --- .../SettingsDeleteDisplayPicture.php | 39 +++++++++++++++++++ .../SettingsSetDisplayPicture.php | 11 +++--- src/Socialbox/Enums/StandardMethods.php | 23 +++++++++-- .../Managers/RegisteredPeerManager.php | 1 + 4 files changed, 65 insertions(+), 9 deletions(-) create mode 100644 src/Socialbox/Classes/StandardMethods/SettingsDeleteDisplayPicture.php diff --git a/src/Socialbox/Classes/StandardMethods/SettingsDeleteDisplayPicture.php b/src/Socialbox/Classes/StandardMethods/SettingsDeleteDisplayPicture.php new file mode 100644 index 0000000..7e86818 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/SettingsDeleteDisplayPicture.php @@ -0,0 +1,39 @@ +isDisplayPictureRequired()) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'A display picture is required for this server'); + } + + try + { + // Set the password + RegisteredPeerManager::deleteDisplayPicture($request->getPeer()); + } + catch(Exception $e) + { + throw new StandardException('Failed to update display picture: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/SettingsSetDisplayPicture.php b/src/Socialbox/Classes/StandardMethods/SettingsSetDisplayPicture.php index e79b2ce..e709a23 100644 --- a/src/Socialbox/Classes/StandardMethods/SettingsSetDisplayPicture.php +++ b/src/Socialbox/Classes/StandardMethods/SettingsSetDisplayPicture.php @@ -34,8 +34,7 @@ try { - $decodedImage = base64_decode($rpcRequest->getParameter('image')); - + $decodedImage = @base64_decode($rpcRequest->getParameter('image')); if($decodedImage === false) { return $rpcRequest->produceError(StandardError::RPC_BAD_REQUEST, "Failed to decode JPEG image base64 data"); @@ -53,11 +52,11 @@ // Set the password RegisteredPeerManager::updateDisplayPicture($request->getPeer(), $sanitizedImage); - // Remove the SET_DISPLAY_PICTURE flag - SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::SET_DISPLAY_PICTURE]); - // Check & update the session flow - SessionManager::updateFlow($request->getSession()); + if($request->getSession()->flagExists(SessionFlags::SET_DISPLAY_PICTURE)) + { + SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_DISPLAY_PICTURE]); + } } catch(Exception $e) { diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index 1f33ef5..a6b8b12 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -13,9 +13,11 @@ use Socialbox\Classes\StandardMethods\Ping; use Socialbox\Classes\StandardMethods\SettingsAddSigningKey; use Socialbox\Classes\StandardMethods\SettingsDeleteDisplayName; + use Socialbox\Classes\StandardMethods\SettingsDeleteDisplayPicture; use Socialbox\Classes\StandardMethods\SettingsDeletePassword; use Socialbox\Classes\StandardMethods\SettingsGetSigningKeys; use Socialbox\Classes\StandardMethods\SettingsSetDisplayName; + use Socialbox\Classes\StandardMethods\SettingsSetDisplayPicture; use Socialbox\Classes\StandardMethods\SettingsSetPassword; use Socialbox\Classes\StandardMethods\SettingsUpdatePassword; use Socialbox\Classes\StandardMethods\VerificationAnswerImageCaptcha; @@ -63,6 +65,7 @@ case SETTINGS_SET_DISPLAY_NAME = 'settingsSetDisplayName'; case SETTINGS_DELETE_DISPLAY_NAME = 'settingsDeleteDisplayName'; case SETTINGS_SET_DISPLAY_PICTURE = 'settingsSetDisplayPicture'; + case SETTINGS_DELETE_DISPLAY_PICTURE = 'settingsDeleteDisplayPicture'; case SETTINGS_SET_EMAIL = 'settingsSetEmail'; case SETTINGS_SET_PHONE = 'settingsSetPhone'; case SETTINGS_SET_BIRTHDAY = 'settingsSetBirthday'; @@ -100,6 +103,8 @@ self::SETTINGS_DELETE_PASSWORD => SettingsDeletePassword::execute($request, $rpcRequest), self::SETTINGS_SET_DISPLAY_NAME => SettingsSetDisplayName::execute($request, $rpcRequest), self::SETTINGS_DELETE_DISPLAY_NAME => SettingsDeleteDisplayName::execute($request, $rpcRequest), + self::SETTINGS_SET_DISPLAY_PICTURE => SettingsSetDisplayPicture::execute($request, $rpcRequest), + self::SETTINGS_DELETE_DISPLAY_PICTURE => SettingsDeleteDisplayPicture::execute($request, $rpcRequest), self::SETTINGS_ADD_SIGNING_KEY => SettingsAddSigningKey::execute($request, $rpcRequest), self::SETTINGS_GET_SIGNING_KEYS => SettingsGetSigningKeys::execute($request, $rpcRequest), @@ -159,7 +164,9 @@ self::SETTINGS_ADD_SIGNING_KEY, self::SETTINGS_GET_SIGNING_KEYS, self::SETTINGS_SET_DISPLAY_NAME, + self::SETTINGS_SET_DISPLAY_PICTURE, self::SETTINGS_SET_PASSWORD, + self::SETTINGS_UPDATE_PASSWORD, ]); // Prevent the user from deleting their display name if it is required @@ -168,10 +175,14 @@ $methods[] = self::SETTINGS_DELETE_DISPLAY_NAME; } - // Always allow the authenticated user to change their password - if(!in_array(SessionFlags::SET_PASSWORD, $session->getFlags())) + if(!Configuration::getRegistrationConfiguration()->isPasswordRequired()) { - $methods[] = self::SETTINGS_SET_PASSWORD; + $methods[] = self::SETTINGS_DELETE_PASSWORD; + } + + if(!Configuration::getRegistrationConfiguration()->isDisplayPictureRequired()) + { + $methods[] = self::SETTINGS_DELETE_DISPLAY_PICTURE; } } // If the session isn't authenticated nor a host, a limited set of methods is available @@ -213,6 +224,12 @@ { $methods[] = self::SETTINGS_SET_DISPLAY_NAME; } + + // If the flag `SET_DISPLAY_PICTURE` is set, then the user has to set a display picture + if($session->flagExists(SessionFlags::SET_DISPLAY_PICTURE)) + { + $methods[] = self::SETTINGS_DELETE_DISPLAY_PICTURE; + } } return $methods; diff --git a/src/Socialbox/Managers/RegisteredPeerManager.php b/src/Socialbox/Managers/RegisteredPeerManager.php index 0924d45..34fc37f 100644 --- a/src/Socialbox/Managers/RegisteredPeerManager.php +++ b/src/Socialbox/Managers/RegisteredPeerManager.php @@ -432,6 +432,7 @@ $peer = self::getPeer($peer); } + // TODO: Handle for external peers, needs a way to resolve peers to their external counterparts if($peer->isExternal()) { throw new InvalidArgumentException('Cannot update the display picture of an external peer'); From 02d42e4e23dfb2a4d24da8b847aa6664919ce7bf Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 01:24:13 -0500 Subject: [PATCH 097/420] Refactor Cryptography methods for improved safety and validation --- src/Socialbox/Classes/Cryptography.php | 66 ++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/src/Socialbox/Classes/Cryptography.php b/src/Socialbox/Classes/Cryptography.php index dc1bb00..38e85f0 100644 --- a/src/Socialbox/Classes/Cryptography.php +++ b/src/Socialbox/Classes/Cryptography.php @@ -5,6 +5,7 @@ use Exception; use Socialbox\Exceptions\CryptographyException; use Socialbox\Objects\KeyPair; + use SodiumException; class Cryptography { @@ -471,7 +472,14 @@ { if (isset($key)) { - sodium_memzero($key); + try + { + sodium_memzero($key); + } + catch (SodiumException) + { + // Ignore + } } } } @@ -535,7 +543,14 @@ { if (isset($key)) { - sodium_memzero($key); + try + { + sodium_memzero($key); + } + catch (SodiumException) + { + // Ignore + } } } } @@ -613,23 +628,46 @@ { if (isset($key)) { - sodium_memzero($key); + try + { + sodium_memzero($key); + } + catch (SodiumException) + { + // Ignore + } } } } + /** + * Validates whether the provided hash is a valid SHA-512 hash. + * + * @param string $hash The hash string to be validated, expected to be a 128-character hexadecimal string. + * @return bool Returns true if the hash is a valid SHA-512 hash, otherwise false. + */ + public static function validateSha512(string $hash): bool + { + return preg_match('/^[A-Fa-f0-9]{128}$/', $hash) === 1; + } + /** * Hashes a password securely using a memory-hard, CPU-intensive hashing algorithm. * - * @param string $password The plaintext password to be hashed. + * @param string $sha512 The SHA-512 hash of the password to be hashed. * @return string The hashed password in a secure format. * @throws CryptographyException If password hashing fails. */ - public static function hashPassword(string $password): string + public static function hashPassword(string $sha512): string { + if(!self::validateSha512($sha512)) + { + throw new CryptographyException("Invalid SHA-512 hash provided"); + } + try { - return sodium_crypto_pwhash_str($password, SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE); + return sodium_crypto_pwhash_str($sha512, SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE); } catch (Exception $e) { @@ -671,18 +709,26 @@ /** * Verifies a password against a stored hash. * - * @param string $password The password to be verified. + * @param string $sha512 The password to be verified. * @param string $hash The stored password hash to be compared against. * @return bool True if the password matches the hash; false otherwise. * @throws CryptographyException If the password verification process fails. */ - public static function verifyPassword(string $password, string $hash): bool + public static function verifyPassword(string $sha512, string $hash): bool { - self::validatePasswordHash($hash); + if(!self::validateSha512($sha512)) + { + throw new CryptographyException("Invalid password hash provided"); + } + + if(!self::validatePasswordHash($hash)) + { + throw new CryptographyException("Invalid password hash provided"); + } try { - return sodium_crypto_pwhash_str_verify($hash, $password); + return sodium_crypto_pwhash_str_verify($hash, $sha512); } catch (Exception $e) { From 5196ac248666d510689d355b43fd1d72ec3c1b19 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 01:29:15 -0500 Subject: [PATCH 098/420] Refactor `hashPassword` method and enhance validations --- src/Socialbox/Classes/Cryptography.php | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/Socialbox/Classes/Cryptography.php b/src/Socialbox/Classes/Cryptography.php index 38e85f0..4c60541 100644 --- a/src/Socialbox/Classes/Cryptography.php +++ b/src/Socialbox/Classes/Cryptography.php @@ -9,9 +9,9 @@ class Cryptography { - private const KEY_TYPE_ENCRYPTION = 'enc:'; - private const KEY_TYPE_SIGNING = 'sig:'; - private const BASE64_VARIANT = SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING; + private const string KEY_TYPE_ENCRYPTION = 'enc:'; + private const string KEY_TYPE_SIGNING = 'sig:'; + private const int BASE64_VARIANT = SODIUM_BASE64_VARIANT_URLSAFE_NO_PADDING; /** * Generates a new encryption key pair consisting of a public key and a secret key. @@ -654,16 +654,27 @@ /** * Hashes a password securely using a memory-hard, CPU-intensive hashing algorithm. * - * @param string $sha512 The SHA-512 hash of the password to be hashed. + * @param string $password The password in plaintext to be hashed, if $hash is false this should be a SHA-512 hash. + * @param bool $hash True to hash the password, false to use the provided SHA-512 hash directly which is validated. * @return string The hashed password in a secure format. * @throws CryptographyException If password hashing fails. */ - public static function hashPassword(string $sha512): string + public static function hashPassword(string $password, bool $hash=true): string { - if(!self::validateSha512($sha512)) + if(empty($password)) + { + throw new CryptographyException("Empty password provided"); + } + + if($hash === false && !self::validateSha512($password)) { throw new CryptographyException("Invalid SHA-512 hash provided"); } + + if($hash) + { + $sha512 = hash('sha512', $password); + } try { @@ -709,7 +720,7 @@ /** * Verifies a password against a stored hash. * - * @param string $sha512 The password to be verified. + * @param string $sha512 The sha512 password to be verified. * @param string $hash The stored password hash to be compared against. * @return bool True if the password matches the hash; false otherwise. * @throws CryptographyException If the password verification process fails. From 3e3bcfd143a9ed82fe22cd1aee99b47e3b81fcbc Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 01:37:51 -0500 Subject: [PATCH 099/420] Add password verification and update enhancements --- .../SettingsUpdatePassword.php | 22 +++++++++ .../VerificationPasswordAuthentication.php | 49 +++++++++++++++++++ src/Socialbox/Managers/PasswordManager.php | 18 ++++--- 3 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 src/Socialbox/Classes/StandardMethods/VerificationPasswordAuthentication.php diff --git a/src/Socialbox/Classes/StandardMethods/SettingsUpdatePassword.php b/src/Socialbox/Classes/StandardMethods/SettingsUpdatePassword.php index eb6b62a..d62339d 100644 --- a/src/Socialbox/Classes/StandardMethods/SettingsUpdatePassword.php +++ b/src/Socialbox/Classes/StandardMethods/SettingsUpdatePassword.php @@ -30,6 +30,16 @@ return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Invalid 'password' parameter, must be a valid argon2id hash"); } + if(!$rpcRequest->containsParameter('existing_password')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'existing_password' parameter"); + } + + if(!Cryptography::validateSha512($rpcRequest->getParameter('existing_password'))) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Invalid 'existing_password' parameter, must be a valid SHA-512 hash"); + } + try { if (!PasswordManager::usesPassword($request->getPeer()->getUuid())) @@ -42,6 +52,18 @@ throw new StandardException('Failed to check password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } + try + { + if (!PasswordManager::verifyPassword($request->getPeer()->getUuid(), $rpcRequest->getParameter('existing_password'))) + { + return $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, "Failed to update password due to incorrect existing password"); + } + } + catch (Exception $e) + { + throw new StandardException('Failed to verify existing password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + } + try { // Set the password diff --git a/src/Socialbox/Classes/StandardMethods/VerificationPasswordAuthentication.php b/src/Socialbox/Classes/StandardMethods/VerificationPasswordAuthentication.php new file mode 100644 index 0000000..9954186 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/VerificationPasswordAuthentication.php @@ -0,0 +1,49 @@ +containsParameter('password')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'password' parameter"); + } + + if(!Cryptography::validateSha512($rpcRequest->getParameter('password'))) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Invalid 'password' parameter, must be a valid SHA-512 hash"); + } + + try + { + return $rpcRequest->produceResponse(PasswordManager::verifyPassword($request->getPeer()->getUuid(), $rpcRequest->getParameter('password'))); + } + catch (CryptographyException $e) + { + return $rpcRequest->produceResponse(false); + } + catch (Exception $e) + { + Logger::getLogger()->error('Failed to verify password due to an internal exception', $e); + throw new StandardException('Failed to verify password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + } + } + } \ No newline at end of file diff --git a/src/Socialbox/Managers/PasswordManager.php b/src/Socialbox/Managers/PasswordManager.php index 0a6947c..e9c3b26 100644 --- a/src/Socialbox/Managers/PasswordManager.php +++ b/src/Socialbox/Managers/PasswordManager.php @@ -59,7 +59,10 @@ } // Throws an exception if the hash is invalid - Cryptography::validatePasswordHash($hash); + if(!Cryptography::validatePasswordHash($hash)) + { + throw new CryptographyException('Invalid password hash'); + } $encryptionKey = Configuration::getCryptographyConfiguration()->getRandomInternalEncryptionKey(); $securedPassword = Cryptography::encryptMessage($hash, $encryptionKey, Configuration::getCryptographyConfiguration()->getEncryptionKeysAlgorithm()); @@ -94,7 +97,10 @@ $peerUuid = $peerUuid->getUuid(); } - Cryptography::validatePasswordHash($hash); + if(!Cryptography::validatePasswordHash($hash)) + { + throw new CryptographyException('Invalid password hash'); + } $encryptionKey = Configuration::getCryptographyConfiguration()->getRandomInternalEncryptionKey(); $securedPassword = Cryptography::encryptMessage($hash, $encryptionKey, Configuration::getCryptographyConfiguration()->getEncryptionKeysAlgorithm()); @@ -145,20 +151,18 @@ * Verifies a given password against a stored password hash for a specific peer. * * @param string|RegisteredPeerRecord $peerUuid The unique identifier of the peer, or an instance of RegisteredPeerRecord. - * @param string $hash The password hash to verify. + * @param string $sha512 The SHA-512 hash of the password to be verified. * @return bool Returns true if the password matches the stored hash; false otherwise. * @throws CryptographyException If the password hash is invalid or an error occurs during the cryptographic operation. * @throws DatabaseOperationException If an error occurs during the database operation. */ - public static function verifyPassword(string|RegisteredPeerRecord $peerUuid, string $hash): bool + public static function verifyPassword(string|RegisteredPeerRecord $peerUuid, string $sha512): bool { if($peerUuid instanceof RegisteredPeerRecord) { $peerUuid = $peerUuid->getUuid(); } - Cryptography::validatePasswordHash($hash); - try { $stmt = Database::getConnection()->prepare('SELECT hash FROM authentication_passwords WHERE peer_uuid=:uuid'); @@ -190,7 +194,7 @@ throw new CryptographyException('Cannot decrypt hashed password'); } - return Cryptography::verifyPassword($hash, $decryptedHash); + return Cryptography::verifyPassword($sha512, $decryptedHash); } catch(PDOException $e) { From 2656becd2587c03f9e64c75ffb30b8da6700ed03 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 01:40:07 -0500 Subject: [PATCH 100/420] Add email address validation and RPC method to set email --- .../SettingsSetEmailAddress.php | 50 +++++++++++++++++++ src/Socialbox/Classes/Validator.php | 11 ++++ 2 files changed, 61 insertions(+) create mode 100644 src/Socialbox/Classes/StandardMethods/SettingsSetEmailAddress.php diff --git a/src/Socialbox/Classes/StandardMethods/SettingsSetEmailAddress.php b/src/Socialbox/Classes/StandardMethods/SettingsSetEmailAddress.php new file mode 100644 index 0000000..c8fda50 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/SettingsSetEmailAddress.php @@ -0,0 +1,50 @@ +containsParameter('email_address')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'email_address' parameter"); + } + + if(!Validator::validateEmailAddress($rpcRequest->getParameter('email_address'))) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Invalid 'email_address' parameter, must be a valid email address"); + } + + try + { + // Set the password + RegisteredPeerManager::updateEmailAddress($request->getPeer(), $rpcRequest->getParameter('email_address')); + + // Check & update the session flow + SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_EMAIL]); + } + catch(Exception $e) + { + throw new StandardException('Failed to set email address due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/Validator.php b/src/Socialbox/Classes/Validator.php index 4d0bda5..628dd30 100644 --- a/src/Socialbox/Classes/Validator.php +++ b/src/Socialbox/Classes/Validator.php @@ -18,6 +18,17 @@ class Validator return preg_match(self::PEER_ADDRESS_PATTERN, $address) === 1; } + /** + * Checks if the provided email address is in a valid email format. + * + * @param string $emailAddress The email address to be validated. + * @return bool Returns true if the email address is valid, otherwise false. + */ + public static function validateEmailAddress(string $emailAddress): bool + { + return filter_var($emailAddress, FILTER_VALIDATE_EMAIL) !== false; + } + /** * Validates a username * From 4058a36cfe5d86a726e35cf05075a15471410b82 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 01:41:16 -0500 Subject: [PATCH 101/420] Minor Update --- src/Socialbox/Classes/Validator.php | 98 ++++++++++++++--------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/src/Socialbox/Classes/Validator.php b/src/Socialbox/Classes/Validator.php index 628dd30..f859717 100644 --- a/src/Socialbox/Classes/Validator.php +++ b/src/Socialbox/Classes/Validator.php @@ -1,58 +1,58 @@ 255) + /** + * Validates a peer address + * + * @param string $address The address to validate. + * @return bool True if the address is valid, false otherwise. + */ + public static function validatePeerAddress(string $address): bool { - return false; + return preg_match(self::PEER_ADDRESS_PATTERN, $address) === 1; } - return preg_match(self::USERNAME_PATTERN, $username) === 1; - } + /** + * Checks if the provided email address is in a valid email format. + * + * @param string $emailAddress The email address to be validated. + * @return bool Returns true if the email address is valid, otherwise false. + */ + public static function validateEmailAddress(string $emailAddress): bool + { + return filter_var($emailAddress, FILTER_VALIDATE_EMAIL) !== false; + } - /** - * Validates whether a given phone number conforms to the required format. - * - * @param string $phoneNumber The phone number to validate. Must start with a "+" followed by 1 to 15 digits. - * @return bool Returns true if the phone number is valid according to the format, otherwise false. - */ - public static function validatePhoneNumber(string $phoneNumber): bool - { - return preg_match("/^\+[0-9]{1,15}$/", $phoneNumber) === 1; - } -} \ No newline at end of file + /** + * Validates a username + * + * @param string $username The username to validate. + * @return bool True if the username is valid, false otherwise. + */ + public static function validateUsername(string $username): bool + { + if(strlen($username) < 3 || strlen($username) > 255) + { + return false; + } + + return preg_match(self::USERNAME_PATTERN, $username) === 1; + } + + /** + * Validates whether a given phone number conforms to the required format. + * + * @param string $phoneNumber The phone number to validate. Must start with a "+" followed by 1 to 15 digits. + * @return bool Returns true if the phone number is valid according to the format, otherwise false. + */ + public static function validatePhoneNumber(string $phoneNumber): bool + { + return preg_match("/^\+[0-9]{1,15}$/", $phoneNumber) === 1; + } + } \ No newline at end of file From a85534dfe6a38144298c3b62c6185d4ed1e2af44 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 01:44:16 -0500 Subject: [PATCH 102/420] Removed unused $e --- .../StandardMethods/VerificationPasswordAuthentication.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Socialbox/Classes/StandardMethods/VerificationPasswordAuthentication.php b/src/Socialbox/Classes/StandardMethods/VerificationPasswordAuthentication.php index 9954186..e5b0521 100644 --- a/src/Socialbox/Classes/StandardMethods/VerificationPasswordAuthentication.php +++ b/src/Socialbox/Classes/StandardMethods/VerificationPasswordAuthentication.php @@ -36,7 +36,7 @@ { return $rpcRequest->produceResponse(PasswordManager::verifyPassword($request->getPeer()->getUuid(), $rpcRequest->getParameter('password'))); } - catch (CryptographyException $e) + catch (CryptographyException) { return $rpcRequest->produceResponse(false); } From dfc198cca3027edf1946215b84b0b4eca3b87766 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 01:49:34 -0500 Subject: [PATCH 103/420] Improve password validation and error handling in delete flow --- .../SettingsDeletePassword.php | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/SettingsDeletePassword.php b/src/Socialbox/Classes/StandardMethods/SettingsDeletePassword.php index 92259a4..820db7e 100644 --- a/src/Socialbox/Classes/StandardMethods/SettingsDeletePassword.php +++ b/src/Socialbox/Classes/StandardMethods/SettingsDeletePassword.php @@ -7,7 +7,7 @@ use Socialbox\Classes\Configuration; use Socialbox\Classes\Cryptography; use Socialbox\Enums\StandardError; - use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\CryptographyException; use Socialbox\Exceptions\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PasswordManager; @@ -26,26 +26,40 @@ return $rpcRequest->produceError(StandardError::FORBIDDEN, 'A password is required for this server'); } - try + if(!$rpcRequest->containsParameter('password')) { - if (!PasswordManager::usesPassword($request->getPeer()->getUuid())) - { - return $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, "Cannot update password when one isn't already set, use 'settingsSetPassword' instead"); - } + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'password' parameter"); } - catch (DatabaseOperationException $e) + + if(!Cryptography::validateSha512($rpcRequest->getParameter('password'))) { - throw new StandardException('Failed to check password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Invalid 'password' parameter, must be a valid SHA-512 hash"); } + $peer = $request->getPeer(); + try { + if (!PasswordManager::usesPassword($peer->getUuid())) + { + return $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, "Cannot update password when one isn't already set, use 'settingsSetPassword' instead"); + } + + if (!PasswordManager::verifyPassword($peer->getUuid(), $rpcRequest->getParameter('password'))) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, "Failed to update password due to incorrect existing password"); + } + // Set the password - PasswordManager::updatePassword($request->getPeer(), $rpcRequest->getParameter('password')); + PasswordManager::updatePassword($peer->getUuid(), $rpcRequest->getParameter('password')); } - catch(Exception $e) + catch(CryptographyException) { - throw new StandardException('Failed to set password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + return $rpcRequest->produceError(StandardError::CRYPTOGRAPHIC_ERROR, 'Failed to update password due to a cryptographic error'); + } + catch (Exception $e) + { + throw new StandardException('Failed to check password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } return $rpcRequest->produceResponse(true); From 480eaa6bc51f1e79a64c00ddb9cbdc3af4896d79 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 01:59:13 -0500 Subject: [PATCH 104/420] Refactor password deletion logic in SettingsDeletePassword. --- .../SettingsDeletePassword.php | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/SettingsDeletePassword.php b/src/Socialbox/Classes/StandardMethods/SettingsDeletePassword.php index 820db7e..82ff41d 100644 --- a/src/Socialbox/Classes/StandardMethods/SettingsDeletePassword.php +++ b/src/Socialbox/Classes/StandardMethods/SettingsDeletePassword.php @@ -17,51 +17,60 @@ class SettingsDeletePassword extends Method { /** + * Executes the password deletion process, validating the required parameters + * and deleting the password if the existing password is correct. * @inheritDoc */ public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { + // Prevent deletion of password if it is required if(Configuration::getRegistrationConfiguration()->isPasswordRequired()) { return $rpcRequest->produceError(StandardError::FORBIDDEN, 'A password is required for this server'); } + // Check if the password parameter is present if(!$rpcRequest->containsParameter('password')) { return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'password' parameter"); } + // Validate the password parameter if(!Cryptography::validateSha512($rpcRequest->getParameter('password'))) { return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Invalid 'password' parameter, must be a valid SHA-512 hash"); } + // Get the peer $peer = $request->getPeer(); try { + // Check if the password is set if (!PasswordManager::usesPassword($peer->getUuid())) { - return $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, "Cannot update password when one isn't already set, use 'settingsSetPassword' instead"); + return $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, "Cannot delete password when one isn't already set"); } + // Verify the existing password before deleting it if (!PasswordManager::verifyPassword($peer->getUuid(), $rpcRequest->getParameter('password'))) { - return $rpcRequest->produceError(StandardError::FORBIDDEN, "Failed to update password due to incorrect existing password"); + return $rpcRequest->produceError(StandardError::FORBIDDEN, "Failed to delete password due to incorrect existing password"); } - // Set the password - PasswordManager::updatePassword($peer->getUuid(), $rpcRequest->getParameter('password')); + // Delete the password + PasswordManager::deletePassword($peer->getUuid()); } catch(CryptographyException) { - return $rpcRequest->produceError(StandardError::CRYPTOGRAPHIC_ERROR, 'Failed to update password due to a cryptographic error'); + return $rpcRequest->produceError(StandardError::CRYPTOGRAPHIC_ERROR, 'Failed to delete password due to a cryptographic error'); } catch (Exception $e) { throw new StandardException('Failed to check password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } + // Success return $rpcRequest->produceResponse(true); } } \ No newline at end of file From da250d618911f280a72fa2c5e68f210fe9754d54 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 02:14:29 -0500 Subject: [PATCH 105/420] Refine community guidelines acceptance logic --- .../StandardMethods/AcceptCommunityGuidelines.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/AcceptCommunityGuidelines.php b/src/Socialbox/Classes/StandardMethods/AcceptCommunityGuidelines.php index 16ff746..6694bfd 100644 --- a/src/Socialbox/Classes/StandardMethods/AcceptCommunityGuidelines.php +++ b/src/Socialbox/Classes/StandardMethods/AcceptCommunityGuidelines.php @@ -15,17 +15,21 @@ { /** + * Executes the process of accepting the community guidelines. + * * @inheritDoc */ public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { + if(!$request->getSession()->flagExists(SessionFlags::VER_COMMUNITY_GUIDELINES)) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'Community guidelines has already been accepted'); + } + try { - // Remove the verification flag - SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::VER_COMMUNITY_GUIDELINES]); - // Check & update the session flow - SessionManager::updateFlow($request->getSession()); + SessionManager::updateFlow($request->getSession(), [SessionFlags::VER_COMMUNITY_GUIDELINES]); } catch (DatabaseOperationException $e) { From 3a723639377f70abb0adb544440213070cb03033 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 02:17:49 -0500 Subject: [PATCH 106/420] Refactor session handling in policy acceptance methods. --- .../StandardMethods/AcceptCommunityGuidelines.php | 5 +++-- .../StandardMethods/AcceptPrivacyPolicy.php | 14 +++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/AcceptCommunityGuidelines.php b/src/Socialbox/Classes/StandardMethods/AcceptCommunityGuidelines.php index 6694bfd..0bb9370 100644 --- a/src/Socialbox/Classes/StandardMethods/AcceptCommunityGuidelines.php +++ b/src/Socialbox/Classes/StandardMethods/AcceptCommunityGuidelines.php @@ -21,7 +21,8 @@ */ public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { - if(!$request->getSession()->flagExists(SessionFlags::VER_COMMUNITY_GUIDELINES)) + $session = $request->getSession(); + if(!$session->flagExists(SessionFlags::VER_COMMUNITY_GUIDELINES)) { return $rpcRequest->produceError(StandardError::FORBIDDEN, 'Community guidelines has already been accepted'); } @@ -29,7 +30,7 @@ try { // Check & update the session flow - SessionManager::updateFlow($request->getSession(), [SessionFlags::VER_COMMUNITY_GUIDELINES]); + SessionManager::updateFlow($session, [SessionFlags::VER_COMMUNITY_GUIDELINES]); } catch (DatabaseOperationException $e) { diff --git a/src/Socialbox/Classes/StandardMethods/AcceptPrivacyPolicy.php b/src/Socialbox/Classes/StandardMethods/AcceptPrivacyPolicy.php index a09c5cd..feb5811 100644 --- a/src/Socialbox/Classes/StandardMethods/AcceptPrivacyPolicy.php +++ b/src/Socialbox/Classes/StandardMethods/AcceptPrivacyPolicy.php @@ -13,19 +13,23 @@ class AcceptPrivacyPolicy extends Method { - /** + * Executes the process of accepting the privacy policy. + * * @inheritDoc */ public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { + $session = $request->getSession(); + if(!$session->flagExists(SessionFlags::VER_PRIVACY_POLICY)) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'Privacy policy has already been accepted'); + } + try { - // Remove the verification flag - SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::VER_PRIVACY_POLICY]); - // Check & update the session flow - SessionManager::updateFlow($request->getSession()); + SessionManager::updateFlow($session, [SessionFlags::VER_PRIVACY_POLICY]); } catch (DatabaseOperationException $e) { From 17a151fd8a641be179d0e979cd75eaf714dce765 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 02:20:09 -0500 Subject: [PATCH 107/420] Fix terms acceptance logic and improve session handling --- .../StandardMethods/AcceptTermsOfService.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/AcceptTermsOfService.php b/src/Socialbox/Classes/StandardMethods/AcceptTermsOfService.php index 2732c2e..c79f643 100644 --- a/src/Socialbox/Classes/StandardMethods/AcceptTermsOfService.php +++ b/src/Socialbox/Classes/StandardMethods/AcceptTermsOfService.php @@ -14,17 +14,22 @@ class AcceptTermsOfService extends Method { /** + * Executes the process of accepting the terms of service. + * * @inheritDoc */ public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { + $session = $request->getSession(); + if(!$session->flagExists(SessionFlags::VER_TERMS_OF_SERVICE)) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'Terms of service has already been accepted'); + } + try { - // Remove the verification flag - SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::VER_TERMS_OF_SERVICE]); - // Check & update the session flow - SessionManager::updateFlow($request->getSession()); + SessionManager::updateFlow($session, [SessionFlags::VER_TERMS_OF_SERVICE]); } catch (DatabaseOperationException $e) { From 3d915f55a5662512632cb76daed443cba05e9c18 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 02:27:22 -0500 Subject: [PATCH 108/420] Fix error message in display name deletion exception. --- .../Classes/StandardMethods/SettingsDeleteDisplayName.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/SettingsDeleteDisplayName.php b/src/Socialbox/Classes/StandardMethods/SettingsDeleteDisplayName.php index 51b4181..0e1b443 100644 --- a/src/Socialbox/Classes/StandardMethods/SettingsDeleteDisplayName.php +++ b/src/Socialbox/Classes/StandardMethods/SettingsDeleteDisplayName.php @@ -26,12 +26,11 @@ try { - // Set the password RegisteredPeerManager::deleteDisplayName($request->getPeer()); } catch(Exception $e) { - throw new StandardException('Failed to set password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardException('Failed to delete display name due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } return $rpcRequest->produceResponse(true); From fa188fb27fef87833bb0a560e5759782850aba25 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 02:29:33 -0500 Subject: [PATCH 109/420] Removed unused Base32 class --- src/Socialbox/Classes/Base32.php | 114 ------------------------------- 1 file changed, 114 deletions(-) delete mode 100644 src/Socialbox/Classes/Base32.php diff --git a/src/Socialbox/Classes/Base32.php b/src/Socialbox/Classes/Base32.php deleted file mode 100644 index 38b59a9..0000000 --- a/src/Socialbox/Classes/Base32.php +++ /dev/null @@ -1,114 +0,0 @@ -= 5) - { - $base32String .= self::LOOKUP_TABLE[($buffer >> ($bitsLeft - 5)) & 0x1F]; - $bitsLeft -= 5; - } - } - - if ($bitsLeft > 0) - { - $base32String .= self::LOOKUP_TABLE[($buffer << (5 - $bitsLeft)) & 0x1F]; - } - - return $base32String; - } -} From 6a922dcac2983fd173bf8a1635c8ea3f73688653 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 04:36:39 -0500 Subject: [PATCH 110/420] Add validation for name and expiration in key creation --- src/Socialbox/Managers/SigningKeysManager.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Socialbox/Managers/SigningKeysManager.php b/src/Socialbox/Managers/SigningKeysManager.php index c74be55..c544f15 100644 --- a/src/Socialbox/Managers/SigningKeysManager.php +++ b/src/Socialbox/Managers/SigningKeysManager.php @@ -99,6 +99,23 @@ throw new InvalidArgumentException('The name is too long'); } + if($name !== null && empty($name)) + { + throw new InvalidArgumentException('The name cannot be empty'); + } + + if($expires !== null && $expires < time()) + { + throw new InvalidArgumentException('The expiration time is in the past'); + } + + // At least more than 1 hour + if($expires !== null && $expires < time() + 3600) + { + throw new InvalidArgumentException('The expiration time is too soon, must be at least 1 hour in the future'); + } + + $uuid = UuidV4::v4()->toRfc4122(); try From fffad01516b0e591b16ac88fcabd2a0cfa6ceb9e Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 04:37:32 -0500 Subject: [PATCH 111/420] Fix exception message in delete display picture method --- .../Classes/StandardMethods/SettingsDeleteDisplayPicture.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/SettingsDeleteDisplayPicture.php b/src/Socialbox/Classes/StandardMethods/SettingsDeleteDisplayPicture.php index 7e86818..9d4a95c 100644 --- a/src/Socialbox/Classes/StandardMethods/SettingsDeleteDisplayPicture.php +++ b/src/Socialbox/Classes/StandardMethods/SettingsDeleteDisplayPicture.php @@ -26,12 +26,11 @@ try { - // Set the password RegisteredPeerManager::deleteDisplayPicture($request->getPeer()); } catch(Exception $e) { - throw new StandardException('Failed to update display picture: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardException('Failed to delete display picture: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); } return $rpcRequest->produceResponse(true); From 1c4621c55ba209a22bae29d0c26f621e771fcbff Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 04:44:12 -0500 Subject: [PATCH 112/420] Enhance flag handling for session operations. --- src/Socialbox/Managers/SessionManager.php | 11 +++++++++-- .../Objects/Database/SessionRecord.php | 17 +++++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/Socialbox/Managers/SessionManager.php b/src/Socialbox/Managers/SessionManager.php index ef36946..0a86bef 100644 --- a/src/Socialbox/Managers/SessionManager.php +++ b/src/Socialbox/Managers/SessionManager.php @@ -436,14 +436,21 @@ return; } + // Don't do anything if the flags to remove are not present + if(!$session->flagExists($flagsToRemove)) + { + return; + } + + // Remove & update the session flags self::removeFlags($session->getUuid(), $flagsToRemove); $session = self::getSession($session->getUuid()); // Check if all registration/authentication requirements are met if(SessionFlags::isComplete($session->getFlags())) { - SessionManager::setAuthenticated($session->getUuid(), true); - SessionManager::removeFlags($session->getUuid(), [SessionFlags::REGISTRATION_REQUIRED, SessionFlags::AUTHENTICATION_REQUIRED]); + SessionManager::removeFlags($session->getUuid(), [SessionFlags::REGISTRATION_REQUIRED, SessionFlags::AUTHENTICATION_REQUIRED]); // Remove the registration/authentication flags + SessionManager::setAuthenticated($session->getUuid(), true); // Mark the session as authenticated } } } \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/SessionRecord.php b/src/Socialbox/Objects/Database/SessionRecord.php index 6492528..515dd88 100644 --- a/src/Socialbox/Objects/Database/SessionRecord.php +++ b/src/Socialbox/Objects/Database/SessionRecord.php @@ -213,11 +213,24 @@ /** * Checks if a given flag exists in the list of session flags. * - * @param string|SessionFlags $flag The flag to check, either as a string or a SessionFlags object. + * @param string|SessionFlags|array $flag The flag to check, either as a string or a SessionFlags object. If an array is provided, all flags must exist. * @return bool True if the flag exists, false otherwise. */ - public function flagExists(string|SessionFlags $flag): bool + public function flagExists(string|SessionFlags|array $flag): bool { + if(is_array($flag)) + { + foreach($flag as $f) + { + if(!$this->flagExists($f)) + { + return false; + } + } + + return true; + } + if(is_string($flag)) { $flag = SessionFlags::tryFrom($flag); From 426df314214efd1fbe5b73703e736fa9e9e3a228 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 04:44:35 -0500 Subject: [PATCH 113/420] Update display name logic and handle invalid arguments. --- .../StandardMethods/SettingsSetDisplayName.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/SettingsSetDisplayName.php b/src/Socialbox/Classes/StandardMethods/SettingsSetDisplayName.php index 93b8d90..148e0f8 100644 --- a/src/Socialbox/Classes/StandardMethods/SettingsSetDisplayName.php +++ b/src/Socialbox/Classes/StandardMethods/SettingsSetDisplayName.php @@ -3,6 +3,7 @@ namespace Socialbox\Classes\StandardMethods; use Exception; + use ncc\ThirdParty\Symfony\Process\Exception\InvalidArgumentException; use Socialbox\Abstracts\Method; use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; @@ -27,14 +28,15 @@ try { - // Set the password + // Update the display name RegisteredPeerManager::updateDisplayName($request->getPeer(), $rpcRequest->getParameter('name')); - // Remove the SET_PASSWORD flag - SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::SET_DISPLAY_NAME]); - // Check & update the session flow - SessionManager::updateFlow($request->getSession()); + SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_DISPLAY_NAME]); + } + catch(InvalidArgumentException) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Invalid display name'); } catch(Exception $e) { From 746744eb647d6e392b15bdc216793c45cd902bcf Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 04:47:56 -0500 Subject: [PATCH 114/420] Update image handling for display picture operations --- .../Classes/StandardMethods/SettingsSetDisplayPicture.php | 7 ++----- src/Socialbox/Managers/RegisteredPeerManager.php | 8 ++++++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/SettingsSetDisplayPicture.php b/src/Socialbox/Classes/StandardMethods/SettingsSetDisplayPicture.php index e709a23..6567b26 100644 --- a/src/Socialbox/Classes/StandardMethods/SettingsSetDisplayPicture.php +++ b/src/Socialbox/Classes/StandardMethods/SettingsSetDisplayPicture.php @@ -40,7 +40,7 @@ return $rpcRequest->produceError(StandardError::RPC_BAD_REQUEST, "Failed to decode JPEG image base64 data"); } - $sanitizedImage = Utilities::resizeImage(Utilities::sanitizeJpeg($decodedImage), 126, 126); + $sanitizedImage = Utilities::resizeImage(Utilities::sanitizeJpeg($decodedImage), 256, 256); } catch(Exception $e) { @@ -53,10 +53,7 @@ RegisteredPeerManager::updateDisplayPicture($request->getPeer(), $sanitizedImage); // Check & update the session flow - if($request->getSession()->flagExists(SessionFlags::SET_DISPLAY_PICTURE)) - { - SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_DISPLAY_PICTURE]); - } + SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_DISPLAY_PICTURE]); } catch(Exception $e) { diff --git a/src/Socialbox/Managers/RegisteredPeerManager.php b/src/Socialbox/Managers/RegisteredPeerManager.php index 34fc37f..867913d 100644 --- a/src/Socialbox/Managers/RegisteredPeerManager.php +++ b/src/Socialbox/Managers/RegisteredPeerManager.php @@ -471,11 +471,19 @@ if($peer->isExternal()) { + // TODO: Implement this throw new InvalidArgumentException('Cannot delete the display picture of an external peer'); } Logger::getLogger()->verbose(sprintf("Deleting display picture of peer %s", $peer->getUuid())); + // Delete the file if it exists + $displayPicturePath = Configuration::getStorageConfiguration()->getUserDisplayImagesPath() . DIRECTORY_SEPARATOR . $peer->getDisplayPicture() . '.jpeg'; + if(file_exists($displayPicturePath)) + { + unlink($displayPicturePath); + } + try { $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET display_picture=NULL WHERE uuid=?'); From ecdd9e1228025937c0fa25f5d1735a971fe3e72d Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 04:48:26 -0500 Subject: [PATCH 115/420] Simplify session flag handling in password update flow --- .../Classes/StandardMethods/SettingsSetPassword.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/SettingsSetPassword.php b/src/Socialbox/Classes/StandardMethods/SettingsSetPassword.php index 1c180dd..73eca67 100644 --- a/src/Socialbox/Classes/StandardMethods/SettingsSetPassword.php +++ b/src/Socialbox/Classes/StandardMethods/SettingsSetPassword.php @@ -50,10 +50,7 @@ PasswordManager::setPassword($request->getPeer(), $rpcRequest->getParameter('password')); // Remove the SET_PASSWORD flag & update the session flow if necessary - if($request->getSession()->flagExists(SessionFlags::SET_PASSWORD)) - { - SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_PASSWORD]); - } + SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_PASSWORD]); } catch(Exception $e) { From 066e26fbe18805a5532853f3d1064a9b76ccb4e5 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 04:49:38 -0500 Subject: [PATCH 116/420] Refactor session flag handling in image captcha verification --- .../Classes/StandardMethods/VerificationAnswerImageCaptcha.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php b/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php index 67b5027..37052f9 100644 --- a/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php +++ b/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php @@ -46,8 +46,7 @@ if($result) { - SessionManager::removeFlags($request->getSessionUuid(), [SessionFlags::VER_IMAGE_CAPTCHA]); - SessionManager::updateFlow($session); + SessionManager::updateFlow($session, [SessionFlags::VER_IMAGE_CAPTCHA]); } } catch (DatabaseOperationException $e) From 65c2254b0c34a388860de65b2ad38f1b058626f8 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 14:54:08 -0500 Subject: [PATCH 117/420] Removed logging events --- src/Socialbox/Managers/CaptchaManager.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Socialbox/Managers/CaptchaManager.php b/src/Socialbox/Managers/CaptchaManager.php index 8151456..3d599c9 100644 --- a/src/Socialbox/Managers/CaptchaManager.php +++ b/src/Socialbox/Managers/CaptchaManager.php @@ -88,7 +88,6 @@ class CaptchaManager // Return false if the captcha does not exist if(!self::captchaExists($peer_uuid)) { - Logger::getLogger()->warning(sprintf("The requested captcha does not exist for the peer %s", $peer_uuid)); return false; } @@ -97,25 +96,21 @@ class CaptchaManager // Return false if the captcha has already been solved if($captcha->getStatus() === CaptchaStatus::SOLVED) { - Logger::getLogger()->warning(sprintf("The requested captcha has already been solved for the peer %s", $peer_uuid)); return false; } // Return false if the captcha is older than 5 minutes if ($captcha->isExpired()) { - Logger::getLogger()->warning(sprintf("The requested captcha is older than 5 minutes for the peer %s", $peer_uuid)); return false; } // Verify the answer if($captcha->getAnswer() !== $answer) { - Logger::getLogger()->warning(sprintf("The answer to the requested captcha is incorrect for the peer %s", $peer_uuid)); return false; } - Logger::getLogger()->debug(sprintf("The answer to the requested captcha is correct for the peer %s", $peer_uuid)); $statement = Database::getConnection()->prepare("UPDATE captcha_images SET status='SOLVED', answered=NOW() WHERE peer_uuid=?"); $statement->bindParam(1, $peer_uuid); From e4e07e120aa1a3f18584ebc743db793860db5d20 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 14:54:35 -0500 Subject: [PATCH 118/420] Minor correction for null-types --- .../Classes/StandardMethods/VerificationAnswerImageCaptcha.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php b/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php index 37052f9..e0d627e 100644 --- a/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php +++ b/src/Socialbox/Classes/StandardMethods/VerificationAnswerImageCaptcha.php @@ -30,7 +30,7 @@ try { - if(CaptchaManager::getCaptcha($session->getPeerUuid())->isExpired()) + if(CaptchaManager::getCaptcha($session->getPeerUuid())?->isExpired()) { return $rpcRequest->produceError(StandardError::CAPTCHA_EXPIRED, 'The captcha has expired'); } From bda8fdc623e7e9aa1caa959f3a3807cef5c72de5 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 14:58:53 -0500 Subject: [PATCH 119/420] Added parameter check and condition check --- .../VerificationGetImageCaptcha.php | 8 +++----- .../VerificationPasswordAuthentication.php | 14 ++++++++++++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php b/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php index 515938b..34672d7 100644 --- a/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php +++ b/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php @@ -4,7 +4,7 @@ use Gregwar\Captcha\CaptchaBuilder; use Socialbox\Abstracts\Method; - use Socialbox\Classes\Logger; + use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\StandardException; @@ -22,18 +22,16 @@ public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { $session = $request->getSession(); - // Check for session conditions - if($session->getPeerUuid() === null) + if(!$session->flagExists(SessionFlags::VER_IMAGE_CAPTCHA)) { - return $rpcRequest->produceError(StandardError::AUTHENTICATION_REQUIRED); + return $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, 'Solving an image captcha is not required at this time'); } $peer = $request->getPeer(); try { - Logger::getLogger()->debug('Creating a new captcha for peer ' . $peer->getUuid()); if(CaptchaManager::captchaExists($peer)) { $captchaRecord = CaptchaManager::getCaptcha($peer); diff --git a/src/Socialbox/Classes/StandardMethods/VerificationPasswordAuthentication.php b/src/Socialbox/Classes/StandardMethods/VerificationPasswordAuthentication.php index e5b0521..d8f9458 100644 --- a/src/Socialbox/Classes/StandardMethods/VerificationPasswordAuthentication.php +++ b/src/Socialbox/Classes/StandardMethods/VerificationPasswordAuthentication.php @@ -6,11 +6,13 @@ use Socialbox\Abstracts\Method; use Socialbox\Classes\Cryptography; use Socialbox\Classes\Logger; + use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\CryptographyException; use Socialbox\Exceptions\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PasswordManager; + use Socialbox\Managers\SessionManager; use Socialbox\Objects\ClientRequest; use Socialbox\Objects\RpcRequest; @@ -32,9 +34,16 @@ return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Invalid 'password' parameter, must be a valid SHA-512 hash"); } + $session = $request->getSession(); + if(!$session->flagExists(SessionFlags::VER_PASSWORD)) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'Password verification is not required at this time'); + } + try { - return $rpcRequest->produceResponse(PasswordManager::verifyPassword($request->getPeer()->getUuid(), $rpcRequest->getParameter('password'))); + $result = PasswordManager::verifyPassword($request->getPeer()->getUuid(), $rpcRequest->getParameter('password')); + SessionManager::updateFlow($request->getSession(), [SessionFlags::VER_PASSWORD]); } catch (CryptographyException) { @@ -42,8 +51,9 @@ } catch (Exception $e) { - Logger::getLogger()->error('Failed to verify password due to an internal exception', $e); throw new StandardException('Failed to verify password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } + + return $rpcRequest->produceResponse($result); } } \ No newline at end of file From 20fa093463d3a15ee3c6019f7949f2326e12253c Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 15:00:42 -0500 Subject: [PATCH 120/420] Added method SettingsSetPhoneNumber --- .../SettingsSetPhoneNumber.php | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/Socialbox/Classes/StandardMethods/SettingsSetPhoneNumber.php diff --git a/src/Socialbox/Classes/StandardMethods/SettingsSetPhoneNumber.php b/src/Socialbox/Classes/StandardMethods/SettingsSetPhoneNumber.php new file mode 100644 index 0000000..eaa9a4d --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/SettingsSetPhoneNumber.php @@ -0,0 +1,49 @@ +containsParameter('phone_number')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'phone_number' parameter"); + } + + if(!Validator::validatePhoneNumber($rpcRequest->getParameter('phone_number'))) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Invalid 'phone_number' parameter, must be a valid phone number"); + } + + try + { + // Set the phone number + RegisteredPeerManager::updatePhoneNumber($request->getPeer(), $rpcRequest->getParameter('phone_number')); + + // Check & update the session flow + SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_PHONE]); + } + catch(Exception $e) + { + throw new StandardException('Failed to set phone number due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file From c0de6ce0069e00d2727e8ad2571cb6f4324a65e8 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 15:11:14 -0500 Subject: [PATCH 121/420] Added methods setBirthday(string|RegisteredPeerRecord $peer, int $year, int $month, int $day): void and deleteBirthday(string|RegisteredPeerRecord $peer): void to \Socialbox\Managers > RegisteredPeerManager --- .../StandardMethods/SettingsSetBirthday.php | 49 ++++++++++++ src/Socialbox/Classes/Validator.php | 13 +++ .../Managers/RegisteredPeerManager.php | 80 +++++++++++++++++++ 3 files changed, 142 insertions(+) create mode 100644 src/Socialbox/Classes/StandardMethods/SettingsSetBirthday.php diff --git a/src/Socialbox/Classes/StandardMethods/SettingsSetBirthday.php b/src/Socialbox/Classes/StandardMethods/SettingsSetBirthday.php new file mode 100644 index 0000000..1d48313 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/SettingsSetBirthday.php @@ -0,0 +1,49 @@ +containsParameter('email_address')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'email_address' parameter"); + } + + if(!Validator::validateEmailAddress($rpcRequest->getParameter('email_address'))) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Invalid 'email_address' parameter, must be a valid email address"); + } + + try + { + // Set the password + RegisteredPeerManager::updateEmailAddress($request->getPeer(), $rpcRequest->getParameter('email_address')); + + // Check & update the session flow + SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_EMAIL]); + } + catch(Exception $e) + { + throw new StandardException('Failed to set email address due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/Validator.php b/src/Socialbox/Classes/Validator.php index f859717..2e3165f 100644 --- a/src/Socialbox/Classes/Validator.php +++ b/src/Socialbox/Classes/Validator.php @@ -55,4 +55,17 @@ { return preg_match("/^\+[0-9]{1,15}$/", $phoneNumber) === 1; } + + /** + * Validates whether the given date is a valid gregorian calendar date. + * + * @param int $month The month component of the date (1 through 12). + * @param int $day The day component of the date. + * @param int $year The year component of the date. + * @return bool Returns true if the provided date is valid, otherwise false. + */ + public static function validateDate(int $month, int $day, int $year): bool + { + return checkdate($month, $day, $year); + } } \ No newline at end of file diff --git a/src/Socialbox/Managers/RegisteredPeerManager.php b/src/Socialbox/Managers/RegisteredPeerManager.php index 867913d..7054c0c 100644 --- a/src/Socialbox/Managers/RegisteredPeerManager.php +++ b/src/Socialbox/Managers/RegisteredPeerManager.php @@ -2,6 +2,7 @@ namespace Socialbox\Managers; + use DateTime; use Exception; use InvalidArgumentException; use PDO; @@ -670,4 +671,83 @@ throw new DatabaseOperationException('Failed to delete the phone number of the peer in the database', $e); } } + + /** + * Sets the birthday of a registered peer record based on the provided date components. + * + * @param string|RegisteredPeerRecord $peer The unique identifier of the peer or an instance of RegisteredPeerRecord. + * @param int $year The year component of the birthday. + * @param int $month The month component of the birthday. + * @param int $day The day component of the birthday. + * @return void + * @throws InvalidArgumentException If the peer is external or the provided date is invalid. + * @throws DatabaseOperationException If there is an error during the database operation. + */ + public static function setBirthday(string|RegisteredPeerRecord $peer, int $year, int $month, int $day): void + { + if(is_string($peer)) + { + $peer = self::getPeer($peer); + } + + if($peer->isExternal()) + { + throw new InvalidArgumentException('Cannot set the birthday of an external peer'); + } + + if(!Validator::validateDate($month, $day, $year)) + { + throw new InvalidArgumentException('The provided date is not valid'); + } + + Logger::getLogger()->verbose(sprintf("Setting birthday of peer %s to %d-%d-%d", $peer->getUuid(), $year, $month, $day)); + + try + { + $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET birthday=? WHERE uuid=?'); + $birthday = (new DateTime())->setDate($year, $month, $day)->format('Y-m-d'); + $statement->bindParam(1, $birthday); + $uuid = $peer->getUuid(); + $statement->bindParam(2, $uuid); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to set the birthday of the peer in the database', $e); + } + } + + /** + * Deletes the birthday of a registered peer based on the given unique identifier or RegisteredPeerRecord object. + * + * @param string|RegisteredPeerRecord $peer The unique identifier of the registered peer, or an instance of RegisteredPeerRecord. + * @throws InvalidArgumentException If the peer is marked as external and cannot have its birthday deleted. + * @throws DatabaseOperationException If there is an error during the database operation. + */ + public static function deleteBirthday(string|RegisteredPeerRecord $peer): void + { + if(is_string($peer)) + { + $peer = self::getPeer($peer); + } + + if($peer->isExternal()) + { + throw new InvalidArgumentException('Cannot delete the birthday of an external peer'); + } + + Logger::getLogger()->verbose(sprintf("Deleting birthday of peer %s", $peer->getUuid())); + + try + { + $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET birthday=NULL WHERE uuid=?'); + $uuid = $peer->getUuid(); + $statement->bindParam(1, $uuid); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to delete the birthday of the peer in the database', $e); + } + } } \ No newline at end of file From 585ff4681090ebdd7db9809c8fad0732c028fc96 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 15:14:04 -0500 Subject: [PATCH 122/420] Added the method SettingsSetBirthday --- .../StandardMethods/SettingsSetBirthday.php | 28 ++++++++++++++----- .../Managers/RegisteredPeerManager.php | 4 +-- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/SettingsSetBirthday.php b/src/Socialbox/Classes/StandardMethods/SettingsSetBirthday.php index 1d48313..9af21b4 100644 --- a/src/Socialbox/Classes/StandardMethods/SettingsSetBirthday.php +++ b/src/Socialbox/Classes/StandardMethods/SettingsSetBirthday.php @@ -21,27 +21,41 @@ */ public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { - if(!$rpcRequest->containsParameter('email_address')) + if(!$rpcRequest->containsParameter('month')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'email_address' parameter"); + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'month' parameter"); } - if(!Validator::validateEmailAddress($rpcRequest->getParameter('email_address'))) + if(!$rpcRequest->containsParameter('day')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Invalid 'email_address' parameter, must be a valid email address"); + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'day' parameter"); + } + + if(!$rpcRequest->containsParameter('year')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'year' parameter"); + } + + $month = $rpcRequest->getParameter('month'); + $day = $rpcRequest->getParameter('day'); + $year = $rpcRequest->getParameter('year'); + + if(!Validator::validateDate($month, $day, $year)) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Invalid date provided, must be a valid gregorian calender date."); } try { // Set the password - RegisteredPeerManager::updateEmailAddress($request->getPeer(), $rpcRequest->getParameter('email_address')); + RegisteredPeerManager::updateBirthday($request->getPeer(), $month, $day, $year); // Check & update the session flow - SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_EMAIL]); + SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_BIRTHDAY]); } catch(Exception $e) { - throw new StandardException('Failed to set email address due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardException('Failed to set birthday due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } return $rpcRequest->produceResponse(true); diff --git a/src/Socialbox/Managers/RegisteredPeerManager.php b/src/Socialbox/Managers/RegisteredPeerManager.php index 7054c0c..1838226 100644 --- a/src/Socialbox/Managers/RegisteredPeerManager.php +++ b/src/Socialbox/Managers/RegisteredPeerManager.php @@ -676,14 +676,14 @@ * Sets the birthday of a registered peer record based on the provided date components. * * @param string|RegisteredPeerRecord $peer The unique identifier of the peer or an instance of RegisteredPeerRecord. - * @param int $year The year component of the birthday. * @param int $month The month component of the birthday. * @param int $day The day component of the birthday. + * @param int $year The year component of the birthday. * @return void * @throws InvalidArgumentException If the peer is external or the provided date is invalid. * @throws DatabaseOperationException If there is an error during the database operation. */ - public static function setBirthday(string|RegisteredPeerRecord $peer, int $year, int $month, int $day): void + public static function updateBirthday(string|RegisteredPeerRecord $peer, int $month, int $day, int $year): void { if(is_string($peer)) { From ab957226d5931dfb69dd28ba7e38ab25f3d9f1e4 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 15:22:52 -0500 Subject: [PATCH 123/420] Add methods to delete email, phone number, and birthday --- src/Socialbox/Classes/Configuration.php | 3 ++ .../RegistrationConfiguration.php | 36 ++++++++++++++++++ .../SettingsDeleteBirthday.php | 38 +++++++++++++++++++ .../SettingsDeleteEmailAddress.php | 38 +++++++++++++++++++ .../SettingsDeletePhoneNumber.php | 38 +++++++++++++++++++ 5 files changed, 153 insertions(+) create mode 100644 src/Socialbox/Classes/StandardMethods/SettingsDeleteBirthday.php create mode 100644 src/Socialbox/Classes/StandardMethods/SettingsDeleteEmailAddress.php create mode 100644 src/Socialbox/Classes/StandardMethods/SettingsDeletePhoneNumber.php diff --git a/src/Socialbox/Classes/Configuration.php b/src/Socialbox/Classes/Configuration.php index 2b0338a..3ef9567 100644 --- a/src/Socialbox/Classes/Configuration.php +++ b/src/Socialbox/Classes/Configuration.php @@ -118,6 +118,9 @@ $config->setDefault('registration.otp_required', false); $config->setDefault('registration.display_name_required', true); $config->setDefault('registration.display_picture_required', false); + $config->setDefault('registration.email_address_required', false); + $config->setDefault('registration.phone_number_required', false); + $config->setDefault('registration.birthday_required', false); $config->setDefault('registration.image_captcha_verification_required', true); // Server Policies diff --git a/src/Socialbox/Classes/Configuration/RegistrationConfiguration.php b/src/Socialbox/Classes/Configuration/RegistrationConfiguration.php index b909c9f..35fcaf0 100644 --- a/src/Socialbox/Classes/Configuration/RegistrationConfiguration.php +++ b/src/Socialbox/Classes/Configuration/RegistrationConfiguration.php @@ -18,6 +18,9 @@ private bool $otpRequired; private bool $displayNameRequired; private bool $displayPictureRequired; + private bool $emailAddressRequired; + private bool $phoneNumberRequired; + private bool $birthdayRequired; private bool $imageCaptchaVerificationRequired; /** @@ -47,6 +50,9 @@ $this->otpRequired = (bool)$data['otp_required']; $this->displayNameRequired = (bool)$data['display_name_required']; $this->displayPictureRequired = (bool)$data['display_picture_required']; + $this->emailAddressRequired = (bool)$data['email_address_required']; + $this->phoneNumberRequired = (bool)$data['phone_number_required']; + $this->birthdayRequired = (bool)$data['birthday_required']; $this->imageCaptchaVerificationRequired = (bool)$data['image_captcha_verification_required']; } @@ -190,6 +196,36 @@ return $this->displayPictureRequired; } + /** + * Determines whether an email address is required. + * + * @return bool Returns true if an email address is required, false otherwise. + */ + public function isEmailAddressRequired(): bool + { + return $this->emailAddressRequired; + } + + /** + * Determines if a phone number is required. + * + * @return bool Returns true if a phone number is required, false otherwise. + */ + public function isPhoneNumberRequired(): bool + { + return $this->phoneNumberRequired; + } + + /** + * Determines if a birthday is required. + * + * @return bool Returns true if a birthday is required, otherwise false. + */ + public function isBirthdayRequired(): bool + { + return $this->birthdayRequired; + } + /** * Determines if image CAPTCHA verification is required. * diff --git a/src/Socialbox/Classes/StandardMethods/SettingsDeleteBirthday.php b/src/Socialbox/Classes/StandardMethods/SettingsDeleteBirthday.php new file mode 100644 index 0000000..8da8fe7 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/SettingsDeleteBirthday.php @@ -0,0 +1,38 @@ +isBirthdayRequired()) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'A birthday is required for this server'); + } + + try + { + RegisteredPeerManager::deleteBirthday($request->getPeer()); + } + catch(Exception $e) + { + throw new StandardException('Failed to delete birthday ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/SettingsDeleteEmailAddress.php b/src/Socialbox/Classes/StandardMethods/SettingsDeleteEmailAddress.php new file mode 100644 index 0000000..cb82661 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/SettingsDeleteEmailAddress.php @@ -0,0 +1,38 @@ +isEmailAddressRequired()) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'A email address is required for this server'); + } + + try + { + RegisteredPeerManager::deleteEmailAddress($request->getPeer()); + } + catch(Exception $e) + { + throw new StandardException('Failed to delete email address: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/SettingsDeletePhoneNumber.php b/src/Socialbox/Classes/StandardMethods/SettingsDeletePhoneNumber.php new file mode 100644 index 0000000..aeb755e --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/SettingsDeletePhoneNumber.php @@ -0,0 +1,38 @@ +isPhoneNumberRequired()) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'A phone number is required for this server'); + } + + try + { + RegisteredPeerManager::deletePhoneNumber($request->getPeer()); + } + catch(Exception $e) + { + throw new StandardException('Failed to delete phone number: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file From b2aa5ed6a2957d94edb8f57c15a5092a7bfdf9ef Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 15:52:06 -0500 Subject: [PATCH 124/420] Add image captcha expiration policy configuration --- src/Socialbox/Classes/Configuration.php | 4 ++++ .../Configuration/PoliciesConfiguration.php | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/Socialbox/Classes/Configuration.php b/src/Socialbox/Classes/Configuration.php index 3ef9567..c3abd6f 100644 --- a/src/Socialbox/Classes/Configuration.php +++ b/src/Socialbox/Classes/Configuration.php @@ -129,6 +129,10 @@ // The amount of time in seconds it takes before a session is considered expired due to inactivity // Default: 12hours $config->setDefault('policies.session_inactivity_expires', 43200); + // The amount of time in seconds it takes before an image captcha is considered expired due to lack of + // answer within the time-frame that the captcha was generated + // If expired; client is expected to request for a new captcha which will generate a new random answer. + $config->setDefault('policies.image_captcha_expires', 300); // Storage configuration $config->setDefault('storage.path', '/etc/socialbox'); // The main path for file storage diff --git a/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php b/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php index 3d9bef0..bf7de98 100644 --- a/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php +++ b/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php @@ -6,14 +6,18 @@ { private int $maxSigningKeys; private int $sessionInactivityExpires; + private int $imageCaptchaExpires; public function __construct(array $data) { $this->maxSigningKeys = $data['max_signing_keys']; $this->sessionInactivityExpires = $data['session_inactivity_expires']; + $this->imageCaptchaExpires = $data['image_captcha_expires']; } /** + * Returns the maximum amount of signing keys a peer can register with the server at once + * * @return int */ public function getMaxSigningKeys(): int @@ -22,10 +26,24 @@ } /** + * Returns the maximum amount of seconds before the session is considered expired due to inactivity + * * @return int */ public function getSessionInactivityExpires(): int { return $this->sessionInactivityExpires; } + + /** + * Returns the maximum amount of seconds before a captcha is considered expired due to the amount of time + * that has passed since the answer was generated, if a user fails to answer the captcha during the time + * period then the user must request for a new captcha with a new answer. + * + * @return int + */ + public function getImageCaptchaExpires(): int + { + return $this->imageCaptchaExpires; + } } \ No newline at end of file From 3d5734702348c5c7731794f562ee5b6e9b43d85b Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 17:32:12 -0500 Subject: [PATCH 125/420] Minor correction --- .../StandardMethods/VerificationPasswordAuthentication.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Socialbox/Classes/StandardMethods/VerificationPasswordAuthentication.php b/src/Socialbox/Classes/StandardMethods/VerificationPasswordAuthentication.php index d8f9458..4bd4604 100644 --- a/src/Socialbox/Classes/StandardMethods/VerificationPasswordAuthentication.php +++ b/src/Socialbox/Classes/StandardMethods/VerificationPasswordAuthentication.php @@ -5,7 +5,6 @@ use Exception; use Socialbox\Abstracts\Method; use Socialbox\Classes\Cryptography; - use Socialbox\Classes\Logger; use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\CryptographyException; From d9c8208310c1aa8a21147fd621eb231c8d6c29ed Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 6 Jan 2025 18:00:45 -0500 Subject: [PATCH 126/420] Added OTP Cryptography (In-Development) --- src/Socialbox/Classes/OtpCryptography.php | 138 +++++++++++++ .../Socialbox/Classes/OtpCryptographyTest.php | 183 ++++++++++++++++++ 2 files changed, 321 insertions(+) create mode 100644 src/Socialbox/Classes/OtpCryptography.php create mode 100644 tests/Socialbox/Classes/OtpCryptographyTest.php diff --git a/src/Socialbox/Classes/OtpCryptography.php b/src/Socialbox/Classes/OtpCryptography.php new file mode 100644 index 0000000..a53373f --- /dev/null +++ b/src/Socialbox/Classes/OtpCryptography.php @@ -0,0 +1,138 @@ +getDomain(); + + if (!$domain) + { + throw new CryptographyException("Domain configuration is missing."); + } + + return sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s", rawurlencode($domain), rawurlencode($account), rawurlencode($secretKey), rawurlencode($issuer)); + } + + /** + * Computes a hash-based message authentication code (HMAC) using the specified algorithm. + * + * @param string $algorithm The hashing algorithm to be used (e.g., 'sha1', 'sha256', 'sha384', 'sha512'). + * @param string $data The data to be hashed. + * @param string $key The secret key used for the HMAC generation. + * + * @return string The generated HMAC as a raw binary string. + * + * @*/ + private static function hashHmac(string $algorithm, string $data, string $key): string + { + return match($algorithm) + { + 'sha1', 'sha256', 'sha384', 'sha512' => hash_hmac($algorithm, $data, $key, true), + default => throw new CryptographyException('Algorithm not supported') + }; + } + } \ No newline at end of file diff --git a/tests/Socialbox/Classes/OtpCryptographyTest.php b/tests/Socialbox/Classes/OtpCryptographyTest.php new file mode 100644 index 0000000..dcbbce9 --- /dev/null +++ b/tests/Socialbox/Classes/OtpCryptographyTest.php @@ -0,0 +1,183 @@ +assertEquals(64, strlen($result), 'Default secret key length (32 bytes) should produce 64 hex characters.'); + } + + /** + * Test generateOTP with valid parameters to ensure a correct OTP. + */ + public function testGenerateOTPValidParameters() + { + $secretKey = '12345678901234567890'; + $timeStep = 30; + $digits = 6; + $counter = 1; + $otp = OtpCryptography::generateOTP($secretKey, $timeStep, $digits, $counter, 'sha1'); + $this->assertEquals(6, strlen($otp), 'OTP should have the correct number of digits.'); + } + + /** + * Test generateOTP produces consistent results for the same inputs. + */ + public function testGenerateOTPConsistency() + { + $secretKey = '12345678901234567890'; + $timeStep = 30; + $digits = 6; + $counter = 1; + $otp1 = OtpCryptography::generateOTP($secretKey, $timeStep, $digits, $counter, 'sha1'); + $otp2 = OtpCryptography::generateOTP($secretKey, $timeStep, $digits, $counter, 'sha1'); + $this->assertEquals($otp1, $otp2, 'OTP should be consistent for the same secret key, counter, and configuration.'); + } + + /** + * Test generateOTP produces different OTPs for the same secret key but different counters. + */ + public function testGenerateOTPDifferentCounters() + { + $secretKey = '12345678901234567890'; + $timeStep = 30; + $digits = 6; + $otp1 = OtpCryptography::generateOTP($secretKey, $timeStep, $digits, 1, 'sha1'); + $otp2 = OtpCryptography::generateOTP($secretKey, $timeStep, $digits, 2, 'sha1'); + $this->assertNotEquals($otp1, $otp2, 'OTP should differ for the same secret key but different counters.'); + } + + /** + * Test generateOTP throws an exception if hash length is invalid. + */ + public function testGenerateOTPInvalidHashLength() + { + $secretKey = 'shortkey'; + $timeStep = 30; + $digits = 6; + $this->expectException(CryptographyException::class); + OtpCryptography::generateOTP($secretKey, $timeStep, $digits, 1, 'sha1'); + } + + /** + * Test generateOTP correctly handles different digit lengths. + */ + public function testGenerateOTPDigitLength() + { + $secretKey = '12345678901234567890'; + $timeStep = 30; + $otp6Digits = OtpCryptography::generateOTP($secretKey, $timeStep, 6, 1, 'sha1'); + $otp8Digits = OtpCryptography::generateOTP($secretKey, $timeStep, 8, 1, 'sha1'); + $this->assertEquals(6, strlen($otp6Digits), 'OTP with 6 digits should be generated.'); + $this->assertEquals(8, strlen($otp8Digits), 'OTP with 8 digits should be generated.'); + } + + /** + * Test that generateSecretKey generates a key of custom length. + */ + public function testGenerateSecretKeyCustomLength() + { + $customLength = 16; // 16 bytes + $result = OtpCryptography::generateSecretKey($customLength); + $this->assertEquals(32, strlen($result), "A secret key of $customLength bytes should produce 32 hex characters."); + } + + /** + * Test that generateSecretKey with a length of 0 results in a valid empty key. + */ + public function testGenerateSecretKeyZeroLength() + { + $this->expectException(CryptographyException::class); + OtpCryptography::generateSecretKey(0); + } + + /** + * Test that generateSecretKey with negative length throws an exception. + */ + public function testGenerateSecretKeyNegativeLength() + { + $this->expectException(CryptographyException::class); + OtpCryptography::generateSecretKey(-1); + } + + /** + * Test that generateSecretKey produces unique keys for multiple calls. + */ + public function testGenerateSecretKeyUniqueKeys() + { + $key1 = OtpCryptography::generateSecretKey(); + $key2 = OtpCryptography::generateSecretKey(); + $this->assertNotEquals($key1, $key2, 'Generated secret keys should be unique.'); + } + + /** + * Test verifyOTP with a valid OTP to ensure it returns true. + */ + public function testVerifyOTPValid() + { + $secretKey = '12345678901234567890'; + $timeStep = 30; + $digits = 6; + // Generate the OTP to test validity + $otp = OtpCryptography::generateOTP($secretKey, $timeStep, $digits, null, 'sha512'); + $this->assertTrue(OtpCryptography::verifyOTP($secretKey, $otp, $timeStep, 1, $digits, 'sha512'), 'verifyOTP should return true for a valid OTP.'); + } + + /** + * Test verifyOTP with an invalid OTP to ensure it returns false. + */ + public function testVerifyOTPInvalid() + { + $secretKey = '12345678901234567890'; + $timeStep = 30; + $digits = 6; + $invalidOtp = '999999'; // Arbitrary invalid OTP + $this->assertFalse(OtpCryptography::verifyOTP($secretKey, $invalidOtp, $timeStep, 1, $digits, 'sha512'), 'verifyOTP should return false for an invalid OTP.'); + } + + /** + * Test verifyOTP with an expired OTP window to ensure it returns false. + */ + public function testVerifyOTPEExpiredWindow() + { + $secretKey = '12345678901234567890'; + $timeStep = 30; + $digits = 6; + $pastCounter = floor(time() / $timeStep) - 10; // Simulate OTP generated far in the past + $expiredOtp = OtpCryptography::generateOTP($secretKey, $timeStep, $digits, $pastCounter, 'sha512'); + $this->assertFalse(OtpCryptography::verifyOTP($secretKey, $expiredOtp, $timeStep, 1, $digits, 'sha512'), 'verifyOTP should return false for an OTP outside the valid window.'); + } + + /** + * Test verifyOTP with an invalid secret key to ensure it throws an exception. + */ + public function testVerifyOTPInvalidSecretKey() + { + $this->expectException(CryptographyException::class); + $invalidSecretKey = 'invalidkey'; + $otp = '123456'; + OtpCryptography::verifyOTP($invalidSecretKey, $otp, 30, 1, 6, 'sha512'); + } + + /** + * Test verifyOTP with an incorrect digit length to ensure it returns false. + */ + public function testVerifyOTPInvalidDigitLength() + { + $secretKey = '12345678901234567890'; + $timeStep = 30; + $digits = 8; // Generate an 8-digit OTP + $validOtp = OtpCryptography::generateOTP($secretKey, $timeStep, $digits, null, 'sha512'); + // Verifying with a 6-digit configuration instead + $this->assertFalse(OtpCryptography::verifyOTP($secretKey, $validOtp, $timeStep, 1, 6, 'sha512'), 'verifyOTP should return false if the digit length does not match.'); + } + } \ No newline at end of file From 866bb90f2a1e0a0fde94b32edfef7232de092f4e Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 7 Jan 2025 14:15:07 -0500 Subject: [PATCH 127/420] Add OTP support with implementation for creation, deletion, and verification. --- .idea/sqldialects.xml | 1 + src/Socialbox/Classes/Configuration.php | 11 + .../Configuration/SecurityConfiguration.php | 59 +++++ src/Socialbox/Classes/OtpCryptography.php | 50 ++-- .../StandardMethods/SettingsDeleteOtp.php | 93 +++++++ .../StandardMethods/SettingsSetOtp.php | 90 +++++++ .../VerificationOtpAuthentication.php | 63 +++++ .../Managers/OneTimePasswordManager.php | 233 ++++++++++++++++++ 8 files changed, 572 insertions(+), 28 deletions(-) create mode 100644 src/Socialbox/Classes/StandardMethods/SettingsDeleteOtp.php create mode 100644 src/Socialbox/Classes/StandardMethods/SettingsSetOtp.php create mode 100644 src/Socialbox/Classes/StandardMethods/VerificationOtpAuthentication.php create mode 100644 src/Socialbox/Managers/OneTimePasswordManager.php diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml index a2d3934..4bbb771 100644 --- a/.idea/sqldialects.xml +++ b/.idea/sqldialects.xml @@ -7,6 +7,7 @@ + diff --git a/src/Socialbox/Classes/Configuration.php b/src/Socialbox/Classes/Configuration.php index c3abd6f..cf3c621 100644 --- a/src/Socialbox/Classes/Configuration.php +++ b/src/Socialbox/Classes/Configuration.php @@ -45,6 +45,17 @@ $config->setDefault('security.display_internal_exceptions', false); $config->setDefault('security.resolved_servers_ttl', 600); $config->setDefault('security.captcha_ttl', 200); + // Server-side OTP Security options + // The time step in seconds for the OTP generation + // Default: 30 seconds + $config->setDefault('security.otp_secret_key_length', 32); + $config->setDefault('security.otp_time_step', 30); + // The number of digits in the OTP + $config->setDefault('security.otp_digits', 6); + // The hash algorithm to use for the OTP generation (sha1, sha256, sha512) + $config->setDefault('security.otp_hash_algorithm', 'sha512'); + // The window of time steps to allow for OTP verification + $config->setDefault('security.otp_window', 1); // Cryptography Configuration // The Unix Timestamp for when the host's keypair should expire diff --git a/src/Socialbox/Classes/Configuration/SecurityConfiguration.php b/src/Socialbox/Classes/Configuration/SecurityConfiguration.php index 72854c2..09ccf2a 100644 --- a/src/Socialbox/Classes/Configuration/SecurityConfiguration.php +++ b/src/Socialbox/Classes/Configuration/SecurityConfiguration.php @@ -7,6 +7,11 @@ private bool $displayInternalExceptions; private int $resolvedServersTtl; private int $captchaTtl; + private int $otpSecretKeyLength; + private int $otpTimeStep; + private int $otpDigits; + private string $otpHashAlgorithm; + private int $otpWindow; /** * Constructor method for initializing class properties. @@ -20,6 +25,11 @@ $this->displayInternalExceptions = $data['display_internal_exceptions']; $this->resolvedServersTtl = $data['resolved_servers_ttl']; $this->captchaTtl = $data['captcha_ttl']; + $this->otpSecretKeyLength = $data['otp_secret_key_length']; + $this->otpTimeStep = $data['otp_time_step']; + $this->otpDigits = $data['otp_digits']; + $this->otpHashAlgorithm = $data['otp_hash_algorithm']; + $this->otpWindow = $data['otp_window']; } /** @@ -52,4 +62,53 @@ return $this->captchaTtl; } + /** + * Retrieves the length of the secret key used for OTP generation. + * + * @return int The length of the secret key used for OTP generation. + */ + public function getOtpSecretKeyLength(): int + { + return $this->otpSecretKeyLength; + } + + /** + * Retrieves the time step value for OTP generation. + * + * @return int The time step value for OTP generation. + */ + public function getOtpTimeStep(): int + { + return $this->otpTimeStep; + } + + /** + * Retrieves the number of digits in the OTP. + * + * @return int The number of digits in the OTP. + */ + public function getOtpDigits(): int + { + return $this->otpDigits; + } + + /** + * Retrieves the hash algorithm used for OTP generation. + * + * @return string The hash algorithm used for OTP generation. + */ + public function getOtpHashAlgorithm(): string + { + return $this->otpHashAlgorithm; + } + + /** + * Retrieves the window value for OTP generation. + * + * @return int The window value for OTP generation. + */ + public function getOtpWindow(): int + { + return $this->otpWindow; + } } \ No newline at end of file diff --git a/src/Socialbox/Classes/OtpCryptography.php b/src/Socialbox/Classes/OtpCryptography.php index a53373f..21a2b77 100644 --- a/src/Socialbox/Classes/OtpCryptography.php +++ b/src/Socialbox/Classes/OtpCryptography.php @@ -7,15 +7,17 @@ class OtpCryptography { + private const string URI_FORMAT = 'otpauth://totp/%s?secret=%s%s&algorithm=%s&digits=%d&period=%d'; + /** * Generates a random secret key of the specified length. * * @param int $length The length of the secret key in bytes. Default is 32. * @return string Returns the generated secret key as a hexadecimal string. - * @throws CryptographyException - * @throws RandomException + * @throws CryptographyException If the length is less than or equal to 0. + * @throws RandomException If an error occurs while generating random bytes. */ - public static function generateSecretKey(int $length = 32): string + public static function generateSecretKey(int $length=32): string { if($length <= 0) { @@ -32,11 +34,11 @@ * @param int $timeStep The time step in seconds used for OTP generation. Default is 30 seconds. * @param int $digits The number of digits in the OTP. Default is 6. * @param int|null $counter Optional counter value. If not provided, it is calculated based on the current time and time step. - * @param string $hashAlgorithm The hash algorithm used for OTP generation. Default is 'sha1'. + * @param string $hashAlgorithm The hash algorithm used for OTP generation. Default is 'sha512'. * @return string Returns the generated OTP as a string with the specified number of digits. * @throws CryptographyException If the generated hash length is less than 20 bytes. */ - public static function generateOTP(string $secretKey, int $timeStep=30, int $digits=6, int $counter=null, string $hashAlgorithm='sha1'): string + public static function generateOTP(string $secretKey, int $timeStep=30, int $digits=6, int $counter=null, string $hashAlgorithm='sha512'): string { if ($counter === null) { @@ -73,6 +75,7 @@ * @param int $digits The number of digits in the OTP. Default is 6. * @param string $hashAlgorithm The hash algorithm used for OTP generation. Default is 'sha512'. * @return bool Returns true if the OTP is valid within the provided parameters, otherwise false. + * @throws CryptographyException If the generated hash length is less than 20 bytes. */ public static function verifyOTP(string $secretKey, string $otp, int $timeStep=30, int $window=1, int $digits=6, string $hashAlgorithm='sha512'): bool { @@ -93,28 +96,20 @@ } /** - * Generates a QR code payload for a TOTP-based authentication system. + * Generates a key URI for use in configuring an authenticator application. * - * The method constructs a URI in the format compatible with TOTP applications. - * - * @param string $account The account name or identifier associated with the QR code. - * @param string $secretKey The secret key to be included in the payload. - * @param string $issuer The issuer name to identify the organization or service. - * - * @return string A formatted string representing the QR code payload. - * - * @throws CryptographyException If the domain configuration is missing. + * @param string $label A unique label to identify the account (e.g., user or service name). + * @param string $secretKey The secret key used for generating the OTP. + * @param string|null $issuer The name of the organization or service issuing the key. Default is null. + * @param int $timeStep The time step in seconds used for OTP generation. Default is 30 seconds. + * @param int $digits The number of digits in the generated OTP. Default is 6. + * @param string $hashAlgorithm The hash algorithm used for OTP generation. Default is 'sha512'. + * @return string Returns the URI string formatted */ - public static function generateQrPayload(string $account, string $secretKey, string $issuer): string + public static function generateKeyUri(string $label, string $secretKey, ?string $issuer = null, int $timeStep=30, int $digits=6, string $hashAlgorithm='sha512'): string { - $domain = Configuration::getInstanceConfiguration()->getDomain(); - - if (!$domain) - { - throw new CryptographyException("Domain configuration is missing."); - } - - return sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s", rawurlencode($domain), rawurlencode($account), rawurlencode($secretKey), rawurlencode($issuer)); + $issuerPart = $issuer ? "&issuer=" . rawurlencode($issuer) : ''; + return sprintf(self::URI_FORMAT, rawurlencode($label), $secretKey, $issuerPart, strtoupper($hashAlgorithm), $digits, $timeStep); } /** @@ -123,15 +118,14 @@ * @param string $algorithm The hashing algorithm to be used (e.g., 'sha1', 'sha256', 'sha384', 'sha512'). * @param string $data The data to be hashed. * @param string $key The secret key used for the HMAC generation. - * * @return string The generated HMAC as a raw binary string. - * - * @*/ + * @throws CryptographyException If the algorithm is not supported. + */ private static function hashHmac(string $algorithm, string $data, string $key): string { return match($algorithm) { - 'sha1', 'sha256', 'sha384', 'sha512' => hash_hmac($algorithm, $data, $key, true), + 'sha1', 'sha256', 'sha512' => hash_hmac($algorithm, $data, $key, true), default => throw new CryptographyException('Algorithm not supported') }; } diff --git a/src/Socialbox/Classes/StandardMethods/SettingsDeleteOtp.php b/src/Socialbox/Classes/StandardMethods/SettingsDeleteOtp.php new file mode 100644 index 0000000..d36b5ce --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/SettingsDeleteOtp.php @@ -0,0 +1,93 @@ +isOtpRequired()) + { + return $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, 'One Time Password is required for this server'); + } + + $peer = $request->getPeer(); + + try + { + if (!OneTimePasswordManager::usesOtp($peer->getUuid())) + { + return $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, "Cannot delete One Time Password when none is set"); + } + } + catch (DatabaseOperationException $e) + { + throw new StandardException('Failed to check One Time Password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + try + { + $usesPassword = PasswordManager::usesPassword($peer); + } + catch (DatabaseOperationException $e) + { + throw new StandardException('Failed to check password usage due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + // Password verification is required to set an OTP if a password is set + if($usesPassword) + { + if(!$rpcRequest->containsParameter('password')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'When a password is set, the current password must be provided to delete an OTP'); + } + + if(!Cryptography::validateSha512($rpcRequest->getParameter('password'))) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The provided password is not a valid SHA-512 hash'); + } + + try + { + if(!PasswordManager::verifyPassword($peer, $rpcRequest->getParameter('password'))) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The provided password is incorrect'); + } + } + catch(Exception $e) + { + throw new StandardException('Failed to verify password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + } + } + + try + { + // Delete the OTP + OneTimePasswordManager::deleteOtp($peer); + } + catch(Exception $e) + { + throw new StandardException('Failed to set password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/SettingsSetOtp.php b/src/Socialbox/Classes/StandardMethods/SettingsSetOtp.php new file mode 100644 index 0000000..1c959cf --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/SettingsSetOtp.php @@ -0,0 +1,90 @@ +getPeer(); + + try + { + if (OneTimePasswordManager::usesOtp($peer->getUuid())) + { + return $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, "Cannot set One Time Password when one is already set, use 'settingsUpdateOtp' instead"); + } + } + catch (DatabaseOperationException $e) + { + throw new StandardException('Failed to check One Time Password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + try + { + $usesPassword = PasswordManager::usesPassword($peer); + } + catch (DatabaseOperationException $e) + { + throw new StandardException('Failed to check password usage due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + // Password verification is required to set an OTP if a password is set + if($usesPassword) + { + if(!$rpcRequest->containsParameter('password')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'When a password is set, the current password must be provided to set an OTP'); + } + + if(!Cryptography::validateSha512($rpcRequest->getParameter('password'))) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The provided password is not a valid SHA-512 hash'); + } + + try + { + if(!PasswordManager::verifyPassword($peer, $rpcRequest->getParameter('password'))) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The provided password is incorrect'); + } + } + catch(Exception $e) + { + throw new StandardException('Failed to verify password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + } + } + + try + { + // Create a new OTP and return the OTP URI to the client + $totpUri = OneTimePasswordManager::createOtp($peer); + + // Remove the SET_PASSWORD flag & update the session flow if necessary + SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_OTP]); + } + catch(Exception $e) + { + throw new StandardException('Failed to set password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse($totpUri); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/VerificationOtpAuthentication.php b/src/Socialbox/Classes/StandardMethods/VerificationOtpAuthentication.php new file mode 100644 index 0000000..f92c23e --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/VerificationOtpAuthentication.php @@ -0,0 +1,63 @@ +containsParameter('code')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'code' parameter"); + } + + if(strlen($rpcRequest->getParameter('code')) !== Configuration::getSecurityConfiguration()->getOtpDigits()) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Invalid 'code' parameter length"); + } + + $session = $request->getSession(); + if(!$session->flagExists(SessionFlags::VER_OTP)) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'One Time Password verification is not required at this time'); + } + + try + { + $result = OneTimePasswordManager::verifyOtp($request->getPeer(), $rpcRequest->getParameter('code')); + + if($result) + { + // If the OTP is verified, remove the OTP verification flag + SessionManager::updateFlow($session, [SessionFlags::VER_OTP]); + } + } + catch (CryptographyException) + { + return $rpcRequest->produceResponse(false); + } + catch (Exception $e) + { + throw new StandardException('Failed to verify password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse($result); + } + } \ No newline at end of file diff --git a/src/Socialbox/Managers/OneTimePasswordManager.php b/src/Socialbox/Managers/OneTimePasswordManager.php new file mode 100644 index 0000000..cb437bb --- /dev/null +++ b/src/Socialbox/Managers/OneTimePasswordManager.php @@ -0,0 +1,233 @@ +getUuid(); + } + + try + { + $stmt = Database::getConnection()->prepare('SELECT COUNT(*) FROM authentication_otp WHERE peer_uuid=:uuid'); + $stmt->bindParam(':uuid', $peerUuid); + $stmt->execute(); + } + catch (PDOException $e) + { + throw new DatabaseOperationException('An error occurred while checking the OTP usage in the database', $e); + } + + return $stmt->fetchColumn() > 0; + } + + /** + * Creates and stores a new OTP (One-Time Password) secret for the specified peer, and generates a key URI. + * + * @param string|RegisteredPeerRecord $peer The unique identifier of the peer, either as a string UUID + * or an instance of RegisteredPeerRecord. + * @return string The generated OTP key URI that can be used for applications like authenticator apps. + * @throws DatabaseOperationException If there is an error during the database operation. + */ + public static function createOtp(string|RegisteredPeerRecord $peer): string + { + if(is_string($peer)) + { + $peer = RegisteredPeerManager::getPeer($peer); + } + + $secret = OtpCryptography::generateSecretKey(Configuration::getSecurityConfiguration()->getOtpSecretKeyLength()); + $encryptionKey = Configuration::getCryptographyConfiguration()->getRandomInternalEncryptionKey(); + $encryptedSecret = Cryptography::encryptMessage($secret, $encryptionKey, Configuration::getCryptographyConfiguration()->getEncryptionKeysAlgorithm()); + + try + { + $stmt = Database::getConnection()->prepare("INSERT INTO authentication_otp (peer_uuid, secret) VALUES (:peer_uuid, :secret)"); + $stmt->bindParam(':peer_uuid', $peer); + $stmt->bindParam(':secret', $encryptedSecret); + $stmt->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('An error occurred while creating the OTP secret in the database', $e); + } + + return OtpCryptography::generateKeyUri($peer->getAddress(), $secret, + Configuration::getInstanceConfiguration()->getDomain(), + Configuration::getSecurityConfiguration()->getOtpTimeStep(), + Configuration::getSecurityConfiguration()->getOtpDigits(), + Configuration::getSecurityConfiguration()->getOtpHashAlgorithm() + ); + } + + /** + * Verifies the provided OTP (One-Time Password) against the stored secret associated with the specified peer. + * + * @param string|RegisteredPeerRecord $peerUuid The unique identifier of the peer, either as a string UUID + * or an instance of RegisteredPeerRecord. + * @param string $otp The OTP to be verified. + * @return bool Returns true if the OTP is valid; otherwise, false. + * @throws DatabaseOperationException If there is an error during the database operation. + * @throws CryptographyException If there is a failure in decrypting the stored OTP secret. + */ + public static function verifyOtp(string|RegisteredPeerRecord $peerUuid, string $otp): bool + { + if($peerUuid instanceof RegisteredPeerRecord) + { + $peerUuid = $peerUuid->getUuid(); + } + + try + { + $stmt = Database::getConnection()->prepare('SELECT secret FROM authentication_otp WHERE peer_uuid=:uuid'); + $stmt->bindParam(':uuid', $peerUuid); + $stmt->execute(); + + $encryptedSecret = $stmt->fetchColumn(); + + if($encryptedSecret === false) + { + return false; + } + } + catch(PDOException $e) + { + throw new DatabaseOperationException('An error occurred while retrieving the OTP secret from the database', $e); + } + + $decryptedSecret = null; + foreach(Configuration::getCryptographyConfiguration()->getInternalEncryptionKeys() as $encryptionKey) + { + try + { + $decryptedSecret = Cryptography::decryptMessage($encryptedSecret, $encryptionKey, Configuration::getCryptographyConfiguration()->getEncryptionKeysAlgorithm()); + } + catch(CryptographyException) + { + continue; + } + } + + if($decryptedSecret === null) + { + throw new CryptographyException('Failed to decrypt the OTP secret'); + } + + return OtpCryptography::verifyOTP($decryptedSecret, $otp, + Configuration::getSecurityConfiguration()->getOtpTimeStep(), + Configuration::getSecurityConfiguration()->getOtpWindow(), + Configuration::getSecurityConfiguration()->getOtpDigits(), + Configuration::getSecurityConfiguration()->getOtpHashAlgorithm() + ); + } + + /** + * Deletes the OTP record associated with the specified peer. + * + * @param string|RegisteredPeerRecord $peerUuid The peer's UUID or an instance of RegisteredPeerRecord whose OTP record needs to be deleted. + * @return void + * @throws DatabaseOperationException if the database operation fails. + */ + public static function deleteOtp(string|RegisteredPeerRecord $peerUuid): void + { + if($peerUuid instanceof RegisteredPeerRecord) + { + $peerUuid = $peerUuid->getUuid(); + } + + try + { + $stmt = Database::getConnection()->prepare('DELETE FROM authentication_otp WHERE peer_uuid=:uuid'); + $stmt->bindParam(':uuid', $peerUuid); + $stmt->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('An error occurred while deleting the OTP secret from the database', $e); + } + } + + /** + * Retrieves the last updated timestamp for the OTP record of the specified peer. + * + * @param string|RegisteredPeerRecord $peerUuid The peer's UUID or an instance of RegisteredPeerRecord whose OTP record's last updated timestamp needs to be retrieved + * @return int The last updated timestamp of the OTP record, or 0 if no such record exists + */ + public static function getLastUpdated(string|RegisteredPeerRecord $peerUuid): int + { + if($peerUuid instanceof RegisteredPeerRecord) + { + $peerUuid = $peerUuid->getUuid(); + } + + try + { + $stmt = Database::getConnection()->prepare('SELECT updated FROM authentication_otp WHERE peer_uuid=:uuid'); + $stmt->bindParam(':uuid', $peerUuid); + $stmt->execute(); + + /** @var \DateTime $updated */ + $updated = $stmt->fetchColumn(); + + if($updated === false) + { + return 0; + } + } + catch(PDOException $e) + { + throw new DatabaseOperationException('An error occurred while retrieving the last updated timestamp from the database', $e); + } + + return $updated->getTimestamp(); + } + + /** + * Updates the last updated timestamp for the OTP record of the specified peer. + * + * @param string|RegisteredPeerRecord $peerUuid The peer's UUID or an instance of RegisteredPeerRecord whose OTP record needs to be updated. + * @return void + * @throws DatabaseOperationException if the database operation fails. + */ + public static function updateOtp(string|RegisteredPeerRecord $peerUuid): void + { + if($peerUuid instanceof RegisteredPeerRecord) + { + $peerUuid = $peerUuid->getUuid(); + } + + try + { + $stmt = Database::getConnection()->prepare('UPDATE authentication_otp SET updated=:updated WHERE peer_uuid=:uuid'); + $updated = (new DateTime())->setTimestamp(time()); + $stmt->bindParam(':updated', $updated); + $stmt->bindParam(':uuid', $peerUuid); + $stmt->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException(sprintf('Failed to update the OTP secret for user %s', $peerUuid), $e); + } + } + } \ No newline at end of file From d99ded728109d47a296b2404d05209d60fe4657c Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 7 Jan 2025 14:15:28 -0500 Subject: [PATCH 128/420] Remove unused updateOtp method from OneTimePasswordManager. --- .../Managers/OneTimePasswordManager.php | 28 ------------------- 1 file changed, 28 deletions(-) diff --git a/src/Socialbox/Managers/OneTimePasswordManager.php b/src/Socialbox/Managers/OneTimePasswordManager.php index cb437bb..18c4e26 100644 --- a/src/Socialbox/Managers/OneTimePasswordManager.php +++ b/src/Socialbox/Managers/OneTimePasswordManager.php @@ -202,32 +202,4 @@ return $updated->getTimestamp(); } - - /** - * Updates the last updated timestamp for the OTP record of the specified peer. - * - * @param string|RegisteredPeerRecord $peerUuid The peer's UUID or an instance of RegisteredPeerRecord whose OTP record needs to be updated. - * @return void - * @throws DatabaseOperationException if the database operation fails. - */ - public static function updateOtp(string|RegisteredPeerRecord $peerUuid): void - { - if($peerUuid instanceof RegisteredPeerRecord) - { - $peerUuid = $peerUuid->getUuid(); - } - - try - { - $stmt = Database::getConnection()->prepare('UPDATE authentication_otp SET updated=:updated WHERE peer_uuid=:uuid'); - $updated = (new DateTime())->setTimestamp(time()); - $stmt->bindParam(':updated', $updated); - $stmt->bindParam(':uuid', $peerUuid); - $stmt->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException(sprintf('Failed to update the OTP secret for user %s', $peerUuid), $e); - } - } } \ No newline at end of file From cabf1f35a8868f5d223f4845f4bd66837a6802f0 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 7 Jan 2025 14:16:29 -0500 Subject: [PATCH 129/420] Fix password verification flow update on failed attempts --- .../StandardMethods/VerificationPasswordAuthentication.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Socialbox/Classes/StandardMethods/VerificationPasswordAuthentication.php b/src/Socialbox/Classes/StandardMethods/VerificationPasswordAuthentication.php index 4bd4604..7108403 100644 --- a/src/Socialbox/Classes/StandardMethods/VerificationPasswordAuthentication.php +++ b/src/Socialbox/Classes/StandardMethods/VerificationPasswordAuthentication.php @@ -42,7 +42,11 @@ try { $result = PasswordManager::verifyPassword($request->getPeer()->getUuid(), $rpcRequest->getParameter('password')); - SessionManager::updateFlow($request->getSession(), [SessionFlags::VER_PASSWORD]); + + if($result) + { + SessionManager::updateFlow($request->getSession(), [SessionFlags::VER_PASSWORD]); + } } catch (CryptographyException) { From 70c0fb2e54759bec9d32b39bfc5256c4ff1e5ed1 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 7 Jan 2025 15:25:32 -0500 Subject: [PATCH 130/420] Add authentication configuration and allowed methods logic --- src/Socialbox/Classes/Configuration.php | 25 ++ .../AuthenticationConfiguration.php | 36 ++ .../StandardMethods/GetAllowedMethods.php | 38 +++ src/Socialbox/Enums/StandardMethods.php | 315 ++++++++++++++---- 4 files changed, 349 insertions(+), 65 deletions(-) create mode 100644 src/Socialbox/Classes/Configuration/AuthenticationConfiguration.php create mode 100644 src/Socialbox/Classes/StandardMethods/GetAllowedMethods.php diff --git a/src/Socialbox/Classes/Configuration.php b/src/Socialbox/Classes/Configuration.php index cf3c621..74a6fca 100644 --- a/src/Socialbox/Classes/Configuration.php +++ b/src/Socialbox/Classes/Configuration.php @@ -2,6 +2,7 @@ namespace Socialbox\Classes; + use Socialbox\Classes\Configuration\AuthenticationConfiguration; use Socialbox\Classes\Configuration\CacheConfiguration; use Socialbox\Classes\Configuration\CryptographyConfiguration; use Socialbox\Classes\Configuration\DatabaseConfiguration; @@ -22,6 +23,7 @@ private static ?LoggingConfiguration $loggingConfiguration = null; private static ?CacheConfiguration $cacheConfiguration = null; private static ?RegistrationConfiguration $registrationConfiguration = null; + private static ?AuthenticationConfiguration $authenticationConfiguration = null; private static ?PoliciesConfiguration $policiesConfiguration = null; private static ?StorageConfiguration $storageConfiguration = null; @@ -134,6 +136,10 @@ $config->setDefault('registration.birthday_required', false); $config->setDefault('registration.image_captcha_verification_required', true); + // Authentication configuration + $config->setDefault('authentication.enabled', true); + $config->setDefault('authentication.image_captcha_verification_required', true); + // Server Policies // The maximum number of signing keys a peer can register onto the server at once $config->setDefault('policies.max_signing_keys', 20); @@ -160,6 +166,7 @@ self::$loggingConfiguration = new LoggingConfiguration(self::$configuration->getConfiguration()['logging']); self::$cacheConfiguration = new CacheConfiguration(self::$configuration->getConfiguration()['cache']); self::$registrationConfiguration = new RegistrationConfiguration(self::$configuration->getConfiguration()['registration']); + self::$authenticationConfiguration = new AuthenticationConfiguration(self::$configuration->getConfiguration()['authentication']); self::$policiesConfiguration = new PoliciesConfiguration(self::$configuration->getConfiguration()['policies']); self::$storageConfiguration = new StorageConfiguration(self::$configuration->getConfiguration()['storage']); } @@ -329,6 +336,24 @@ return self::$registrationConfiguration; } + /** + * Retrieves the authentication configuration. + * + * This method returns the current AuthenticationConfiguration instance. + * If the configuration has not been initialized yet, it initializes it first. + * + * @return AuthenticationConfiguration The authentication configuration instance. + */ + public static function getAuthenticationConfiguration(): AuthenticationConfiguration + { + if(self::$authenticationConfiguration === null) + { + self::initializeConfiguration(); + } + + return self::$authenticationConfiguration; + } + /** * Retrieves the policies configuration. * diff --git a/src/Socialbox/Classes/Configuration/AuthenticationConfiguration.php b/src/Socialbox/Classes/Configuration/AuthenticationConfiguration.php new file mode 100644 index 0000000..18fea9d --- /dev/null +++ b/src/Socialbox/Classes/Configuration/AuthenticationConfiguration.php @@ -0,0 +1,36 @@ +enabled = (bool)$data['enabled']; + $this->imageCaptchaVerificationRequired = (bool)$data['image_captcha_verification_required']; + } + + /** + * @return bool + */ + public function isEnabled(): bool + { + return $this->enabled; + } + + /** + * @return bool + */ + public function isImageCaptchaVerificationRequired(): bool + { + return $this->imageCaptchaVerificationRequired; + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/GetAllowedMethods.php b/src/Socialbox/Classes/StandardMethods/GetAllowedMethods.php new file mode 100644 index 0000000..2baf0cd --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/GetAllowedMethods.php @@ -0,0 +1,38 @@ +value; + } + } + catch(DatabaseOperationException $e) + { + throw new StandardException('Failed to retrieve allowed methods due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse($allowedMethods); + } + } \ No newline at end of file diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index a6b8b12..34c399f 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -6,32 +6,46 @@ use Socialbox\Classes\StandardMethods\AcceptCommunityGuidelines; use Socialbox\Classes\StandardMethods\AcceptPrivacyPolicy; use Socialbox\Classes\StandardMethods\AcceptTermsOfService; + use Socialbox\Classes\StandardMethods\GetAllowedMethods; use Socialbox\Classes\StandardMethods\GetCommunityGuidelines; use Socialbox\Classes\StandardMethods\GetPrivacyPolicy; use Socialbox\Classes\StandardMethods\GetSessionState; use Socialbox\Classes\StandardMethods\GetTermsOfService; use Socialbox\Classes\StandardMethods\Ping; use Socialbox\Classes\StandardMethods\SettingsAddSigningKey; + use Socialbox\Classes\StandardMethods\SettingsDeleteBirthday; use Socialbox\Classes\StandardMethods\SettingsDeleteDisplayName; use Socialbox\Classes\StandardMethods\SettingsDeleteDisplayPicture; + use Socialbox\Classes\StandardMethods\SettingsDeleteEmailAddress; use Socialbox\Classes\StandardMethods\SettingsDeletePassword; + use Socialbox\Classes\StandardMethods\SettingsDeletePhoneNumber; use Socialbox\Classes\StandardMethods\SettingsGetSigningKeys; + use Socialbox\Classes\StandardMethods\SettingsSetBirthday; use Socialbox\Classes\StandardMethods\SettingsSetDisplayName; use Socialbox\Classes\StandardMethods\SettingsSetDisplayPicture; + use Socialbox\Classes\StandardMethods\SettingsSetEmailAddress; use Socialbox\Classes\StandardMethods\SettingsSetPassword; + use Socialbox\Classes\StandardMethods\SettingsSetPhoneNumber; use Socialbox\Classes\StandardMethods\SettingsUpdatePassword; use Socialbox\Classes\StandardMethods\VerificationAnswerImageCaptcha; use Socialbox\Classes\StandardMethods\VerificationGetImageCaptcha; + use Socialbox\Classes\StandardMethods\VerificationOtpAuthentication; + use Socialbox\Classes\StandardMethods\VerificationPasswordAuthentication; use Socialbox\Enums\Flags\SessionFlags; + use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\StandardException; use Socialbox\Interfaces\SerializableInterface; + use Socialbox\Managers\OneTimePasswordManager; + use Socialbox\Managers\PasswordManager; use Socialbox\Objects\ClientRequest; + use Socialbox\Objects\Database\SessionRecord; use Socialbox\Objects\RpcRequest; enum StandardMethods : string { case PING = 'ping'; case GET_SESSION_STATE = 'getSessionState'; + case GET_ALLOWED_METHODS = 'getAllowedMethods'; case GET_PRIVACY_POLICY = 'getPrivacyPolicy'; case ACCEPT_PRIVACY_POLICY = 'acceptPrivacyPolicy'; @@ -57,18 +71,25 @@ case VERIFICATION_GET_EXTERNAL_URL = 'verificationGetExternalUrl'; case VERIFICATION_ANSWER_EXTERNAL_URL = 'verificationAnswerExternalUrl'; + + case VERIFICATION_PASSWORD_AUTHENTICATION = 'verificationPasswordAuthentication'; + case VERIFICATION_OTP_AUTHENTICATION = 'verificationOtpAuthentication'; case SETTINGS_SET_PASSWORD = 'settingsSetPassword'; case SETTINGS_UPDATE_PASSWORD = 'settingsUpdatePassword'; case SETTINGS_DELETE_PASSWORD = 'settingsDeletePassword'; case SETTINGS_SET_OTP = 'settingsSetOtp'; + case SETTINGS_DELETE_OTP = 'settingsDeleteOtp'; case SETTINGS_SET_DISPLAY_NAME = 'settingsSetDisplayName'; case SETTINGS_DELETE_DISPLAY_NAME = 'settingsDeleteDisplayName'; case SETTINGS_SET_DISPLAY_PICTURE = 'settingsSetDisplayPicture'; case SETTINGS_DELETE_DISPLAY_PICTURE = 'settingsDeleteDisplayPicture'; case SETTINGS_SET_EMAIL = 'settingsSetEmail'; + case SETTINGS_DELETE_EMAIL = 'settingsDeleteEmail'; case SETTINGS_SET_PHONE = 'settingsSetPhone'; + case SETTINGS_DELETE_PHONE = 'settingsDeletePhone'; case SETTINGS_SET_BIRTHDAY = 'settingsSetBirthday'; + case SETTINGS_DELETE_BIRTHDAY = 'settingsDeleteBirthday'; case SETTINGS_ADD_SIGNING_KEY = 'settingsAddSigningKey'; case SETTINGS_GET_SIGNING_KEYS = 'settingsGetSigningKeys'; @@ -87,6 +108,7 @@ { self::PING => Ping::execute($request, $rpcRequest), self::GET_SESSION_STATE => GetSessionState::execute($request, $rpcRequest), + self::GET_ALLOWED_METHODS => GetAllowedMethods::execute($request, $rpcRequest), self::GET_PRIVACY_POLICY => GetPrivacyPolicy::execute($request, $rpcRequest), self::ACCEPT_PRIVACY_POLICY => AcceptPrivacyPolicy::execute($request, $rpcRequest), @@ -97,6 +119,9 @@ self::VERIFICATION_GET_IMAGE_CAPTCHA => VerificationGetImageCaptcha::execute($request, $rpcRequest), self::VERIFICATION_ANSWER_IMAGE_CAPTCHA => VerificationAnswerImageCaptcha::execute($request, $rpcRequest), + + self::VERIFICATION_PASSWORD_AUTHENTICATION => VerificationPasswordAuthentication::execute($request, $rpcRequest), + self::VERIFICATION_OTP_AUTHENTICATION => VerificationOtpAuthentication::execute($request, $rpcRequest), self::SETTINGS_SET_PASSWORD => SettingsSetPassword::execute($request, $rpcRequest), self::SETTINGS_UPDATE_PASSWORD => SettingsUpdatePassword::execute($request, $rpcRequest), @@ -105,6 +130,12 @@ self::SETTINGS_DELETE_DISPLAY_NAME => SettingsDeleteDisplayName::execute($request, $rpcRequest), self::SETTINGS_SET_DISPLAY_PICTURE => SettingsSetDisplayPicture::execute($request, $rpcRequest), self::SETTINGS_DELETE_DISPLAY_PICTURE => SettingsDeleteDisplayPicture::execute($request, $rpcRequest), + self::SETTINGS_SET_EMAIL => SettingsSetEmailAddress::execute($request, $rpcRequest), + self::SETTINGS_DELETE_EMAIL => SettingsDeleteEmailAddress::execute($request, $rpcRequest), + self::SETTINGS_SET_PHONE => SettingsSetPhoneNumber::execute($request, $rpcRequest), + self::SETTINGS_DELETE_PHONE => SettingsDeletePhoneNumber::execute($request, $rpcRequest), + self::SETTINGS_SET_BIRTHDAY => SettingsSetBirthday::execute($request, $rpcRequest), + self::SETTINGS_DELETE_BIRTHDAY => SettingsDeleteBirthday::execute($request, $rpcRequest), self::SETTINGS_ADD_SIGNING_KEY => SettingsAddSigningKey::execute($request, $rpcRequest), self::SETTINGS_GET_SIGNING_KEYS => SettingsGetSigningKeys::execute($request, $rpcRequest), @@ -118,6 +149,7 @@ * * @param ClientRequest $clientRequest The client request instance to check access against. * @return void + * @throws DatabaseOperationException If an error occurs while checking the database for session information. * @throws StandardException If the method is not allowed for the given client request. */ public function checkAccess(ClientRequest $clientRequest): void @@ -135,6 +167,7 @@ * * @param ClientRequest $clientRequest The client request for which allowed methods are determined. * @return array Returns an array of allowed methods for the provided client request. + * @throws DatabaseOperationException If an error occurs while checking the database for session information. */ public static function getAllowedMethods(ClientRequest $clientRequest): array { @@ -143,6 +176,7 @@ // Important methods self::PING, // Always allow the ping method self::GET_SESSION_STATE, // The session state should always be accessible + self::GET_ALLOWED_METHODS, // Client should always be able to get the allowed methods self::GET_PRIVACY_POLICY, // The user should always be able to get the privacy policy self::GET_TERMS_OF_SERVICE, // The user should always be able to get the terms of service self::GET_COMMUNITY_GUIDELINES, // The user should always be able to get the community guidelines @@ -150,86 +184,237 @@ $session = $clientRequest->getSession(); + if($session === null) + { + return $methods; + } + + try + { + $external = $session->isExternal(); + } + catch(DatabaseOperationException) + { + $external = false; + } + // If the session is external (eg; coming from a different server) // Servers will have their own access control mechanisms - if($session->isExternal()) + if($external) { - // TODO: Implement server access control + $methods = array_merge($methods, self::getExternalMethods($clientRequest)); } // If the session is authenticated, then allow additional method calls elseif($session->isAuthenticated()) { - // These methods are always allowed for authenticated users - $methods = array_merge($methods, [ - self::SETTINGS_ADD_SIGNING_KEY, - self::SETTINGS_GET_SIGNING_KEYS, - self::SETTINGS_SET_DISPLAY_NAME, - self::SETTINGS_SET_DISPLAY_PICTURE, - self::SETTINGS_SET_PASSWORD, - self::SETTINGS_UPDATE_PASSWORD, - ]); - - // Prevent the user from deleting their display name if it is required - if(!Configuration::getRegistrationConfiguration()->isDisplayNameRequired()) - { - $methods[] = self::SETTINGS_DELETE_DISPLAY_NAME; - } - - if(!Configuration::getRegistrationConfiguration()->isPasswordRequired()) - { - $methods[] = self::SETTINGS_DELETE_PASSWORD; - } - - if(!Configuration::getRegistrationConfiguration()->isDisplayPictureRequired()) - { - $methods[] = self::SETTINGS_DELETE_DISPLAY_PICTURE; - } + $methods = array_merge($methods, self::getAuthenticatedMethods()); } - // If the session isn't authenticated nor a host, a limited set of methods is available - else + // If the session isn't authenticated, check if it's a registering user + elseif($session->flagExists(SessionFlags::REGISTRATION_REQUIRED)) { - // If the flag `VER_PRIVACY_POLICY` is set, then the user can accept the privacy policy - if($session->flagExists(SessionFlags::VER_PRIVACY_POLICY)) - { - $methods[] = self::ACCEPT_PRIVACY_POLICY; - } + $methods = array_merge($methods, self::getRegistrationMethods($session)); + } + // If the user is a registering peer, check if it's an authenticating one + elseif($session->flagExists(SessionFlags::AUTHENTICATION_REQUIRED)) + { + $methods = array_merge($methods, self::getAuthenticationMethods($clientRequest)); + } - // If the flag `VER_TERMS_OF_SERVICE` is set, then the user can accept the terms of service - if($session->flagExists(SessionFlags::VER_TERMS_OF_SERVICE)) - { - $methods[] = self::ACCEPT_TERMS_OF_SERVICE; - } + return $methods; + } - // If the flag `VER_COMMUNITY_GUIDELINES` is set, then the user can accept the community guidelines - if($session->flagExists(SessionFlags::VER_COMMUNITY_GUIDELINES)) - { - $methods[] = self::ACCEPT_COMMUNITY_GUIDELINES; - } + /** + **/ + private static function getExternalMethods(ClientRequest $clientRequest): array + { + return []; + } - // If the flag `VER_IMAGE_CAPTCHA` is set, then the user has to get and answer an image captcha - if($session->flagExists(SessionFlags::VER_IMAGE_CAPTCHA)) - { - $methods[] = self::VERIFICATION_GET_IMAGE_CAPTCHA; - $methods[] = self::VERIFICATION_ANSWER_IMAGE_CAPTCHA; - } + /** + * Retrieves a list of authenticated user methods based on configuration settings. + * + * @return array An array of methods that are available to + */ + private static function getAuthenticatedMethods(): array + { - // If the flag `SET_PASSWORD` is set, then the user has to set a password - if($session->flagExists(SessionFlags::SET_PASSWORD)) - { - $methods[] = self::SETTINGS_SET_PASSWORD; - } + // These methods are always allowed for authenticated users + $methods = [ + self::SETTINGS_ADD_SIGNING_KEY, + self::SETTINGS_GET_SIGNING_KEYS, + self::SETTINGS_SET_DISPLAY_NAME, + self::SETTINGS_SET_DISPLAY_PICTURE, + self::SETTINGS_SET_PASSWORD, + self::SETTINGS_UPDATE_PASSWORD, + self::SETTINGS_SET_OTP, + self::SETTINGS_SET_EMAIL, + self::SETTINGS_SET_PHONE, + self::SETTINGS_SET_BIRTHDAY + ]; - // If the flag `SET_DISPLAY_NAME` is set, then the user has to set a display name - if($session->flagExists(SessionFlags::SET_DISPLAY_NAME)) - { - $methods[] = self::SETTINGS_SET_DISPLAY_NAME; - } + // Prevent the user from deleting their display name if it is required + if(!Configuration::getRegistrationConfiguration()->isDisplayNameRequired()) + { + $methods[] = self::SETTINGS_DELETE_DISPLAY_NAME; + } - // If the flag `SET_DISPLAY_PICTURE` is set, then the user has to set a display picture - if($session->flagExists(SessionFlags::SET_DISPLAY_PICTURE)) - { - $methods[] = self::SETTINGS_DELETE_DISPLAY_PICTURE; - } + // Prevent the user from deleting their password if it is required + if(!Configuration::getRegistrationConfiguration()->isPasswordRequired()) + { + $methods[] = self::SETTINGS_DELETE_PASSWORD; + } + + // Prevent the user from deleting their display picture if it is required + if(!Configuration::getRegistrationConfiguration()->isDisplayPictureRequired()) + { + $methods[] = self::SETTINGS_DELETE_DISPLAY_PICTURE; + } + + // Prevent the user from deleting their OTP if it is required + if(!Configuration::getRegistrationConfiguration()->isOtpRequired()) + { + $methods[] = self::SETTINGS_DELETE_OTP; + } + + // Prevent the user from deleting their Phone Number if it is required + if(!Configuration::getRegistrationConfiguration()->isPhoneNumberRequired()) + { + $methods[] = self::SETTINGS_DELETE_PHONE; + } + + // Prevent the user from deleting their email address if it is required + if(!Configuration::getRegistrationConfiguration()->isEmailAddressRequired()) + { + $methods[] = self::SETTINGS_DELETE_EMAIL; + } + + // Prevent the user from deleting their birthday if it is required + if(!Configuration::getRegistrationConfiguration()->isBirthdayRequired()) + { + $methods[] = self::SETTINGS_DELETE_BIRTHDAY; + } + + return $methods; + } + + /** + * Retrieves a list of registration methods based on the session flags. + * + * @param SessionRecord $session The session record containing flags that determine available registration methods. + * @return array An array of registration methods available for the session. + */ + private static function getRegistrationMethods(SessionRecord $session): array + { + // Don't allow registration methods if registration is disabled + if(!Configuration::getRegistrationConfiguration()->isRegistrationEnabled()) + { + return []; + } + + // If the flag `VER_PRIVACY_POLICY` is set, then the user can accept the privacy policy + if($session->flagExists(SessionFlags::VER_PRIVACY_POLICY)) + { + $methods[] = self::ACCEPT_PRIVACY_POLICY; + } + + // If the flag `VER_TERMS_OF_SERVICE` is set, then the user can accept the terms of service + if($session->flagExists(SessionFlags::VER_TERMS_OF_SERVICE)) + { + $methods[] = self::ACCEPT_TERMS_OF_SERVICE; + } + + // If the flag `VER_COMMUNITY_GUIDELINES` is set, then the user can accept the community guidelines + if($session->flagExists(SessionFlags::VER_COMMUNITY_GUIDELINES)) + { + $methods[] = self::ACCEPT_COMMUNITY_GUIDELINES; + } + + // If the flag `VER_IMAGE_CAPTCHA` is set, then the user has to get and answer an image captcha + if($session->flagExists(SessionFlags::VER_IMAGE_CAPTCHA)) + { + $methods[] = self::VERIFICATION_GET_IMAGE_CAPTCHA; + $methods[] = self::VERIFICATION_ANSWER_IMAGE_CAPTCHA; + } + + // If the flag `SET_PASSWORD` is set, then the user has to set a password + if($session->flagExists(SessionFlags::SET_PASSWORD)) + { + $methods[] = self::SETTINGS_SET_PASSWORD; + } + + // If the flag `SET_OTP` is set, then the user has to set an OTP + if($session->flagExists(SessionFLags::SET_OTP)) + { + $methods[] = self::SETTINGS_SET_OTP; + } + + // If the flag `SET_DISPLAY_NAME` is set, then the user has to set a display name + if($session->flagExists(SessionFlags::SET_DISPLAY_NAME)) + { + $methods[] = self::SETTINGS_SET_DISPLAY_NAME; + } + + // If the flag `SET_DISPLAY_PICTURE` is set, then the user has to set a display picture + if($session->flagExists(SessionFlags::SET_DISPLAY_PICTURE)) + { + $methods[] = self::SETTINGS_SET_DISPLAY_PICTURE; + } + + // If the flag `SET_EMAIL` is set, then the user has to set an email address + if($session->flagExists(SessionFlags::SET_EMAIL)) + { + $methods[] = self::SETTINGS_SET_EMAIL; + } + + // If the flag `SET_PHONE` is set, then the user has to set a phone number + if($session->flagExists(SessionFlags::SET_PHONE)) + { + $methods[] = self::SETTINGS_SET_PHONE; + } + + // If the flag `SET_BIRTHDAY` is set, then the user has to set a birthday + if($session->flagExists(SessionFlags::SET_BIRTHDAY)) + { + $methods[] = self::SETTINGS_SET_BIRTHDAY; + } + + return $methods; + } + + + /** + * Retrieves the list of authentication methods available for the given client request. + * + * @param ClientRequest $clientRequest The client request for which the authentication methods are determined. + * @return array The list of available authentication methods as an array of constants. + * @throws DatabaseOperationException If an error occurs while checking the database for authentication methods. + */ + private static function getAuthenticationMethods(ClientRequest $clientRequest): array + { + if(!Configuration::getAuthenticationConfiguration()->isEnabled()) + { + return []; + } + + $methods = []; + + if(Configuration::getAuthenticationConfiguration()->isImageCaptchaVerificationRequired()) + { + $methods[] = self::VERIFICATION_GET_IMAGE_CAPTCHA; + $methods[] = self::VERIFICATION_ANSWER_IMAGE_CAPTCHA; + } + + + $peer = $clientRequest->getPeer(); + + if(PasswordManager::usesPassword($peer)) + { + $methods[] = self::VERIFICATION_PASSWORD_AUTHENTICATION; + } + + if(OneTimePasswordManager::usesOtp($peer->getUuid())) + { + $methods[] = self::VERIFICATION_OTP_AUTHENTICATION; } return $methods; From 77911af6d93691c1cac64f6252932345bbd3ee22 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 7 Jan 2025 15:26:32 -0500 Subject: [PATCH 131/420] Label unimplemented methods in StandardMethods enum. --- src/Socialbox/Enums/StandardMethods.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index 34c399f..8cf92c8 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -54,23 +54,23 @@ case GET_COMMUNITY_GUIDELINES = 'getCommunityGuidelines'; case ACCEPT_COMMUNITY_GUIDELINES = 'acceptCommunityGuidelines'; - case VERIFICATION_EMAIL = 'verificationEmail'; - case VERIFICATION_ANSWER_EMAIL = 'verificationAnswerEmail'; + case VERIFICATION_EMAIL = 'verificationEmail'; // NOT IMPLEMENTED + case VERIFICATION_ANSWER_EMAIL = 'verificationAnswerEmail'; // NOT IMPLEMENTED - case VERIFICATION_SMS = 'verificationSms'; - case VERIFICATION_ANSWER_SMS = 'verificationAnswerSms'; + case VERIFICATION_SMS = 'verificationSms'; // NOT IMPLEMENTED + case VERIFICATION_ANSWER_SMS = 'verificationAnswerSms'; // NOT IMPLEMENTED - case VERIFICATION_PHONE_CALL = 'verificationPhoneCall'; - case VERIFICATION_ANSWER_PHONE_CALL = 'verificationAnswerPhoneCall'; + case VERIFICATION_PHONE_CALL = 'verificationPhoneCall'; // NOT IMPLEMENTED + case VERIFICATION_ANSWER_PHONE_CALL = 'verificationAnswerPhoneCall'; // NOT IMPLEMENTED case VERIFICATION_GET_IMAGE_CAPTCHA = 'verificationGetImageCaptcha'; case VERIFICATION_ANSWER_IMAGE_CAPTCHA = 'verificationAnswerImageCaptcha'; - case VERIFICATION_GET_TEXT_CAPTCHA = 'verificationGetTextCaptcha'; - case VERIFICATION_ANSWER_TEXT_CAPTCHA = 'verificationAnswerTextCaptcha'; + case VERIFICATION_GET_TEXT_CAPTCHA = 'verificationGetTextCaptcha'; // NOT IMPLEMENTED + case VERIFICATION_ANSWER_TEXT_CAPTCHA = 'verificationAnswerTextCaptcha'; // NOT IMPLEMENTED - case VERIFICATION_GET_EXTERNAL_URL = 'verificationGetExternalUrl'; - case VERIFICATION_ANSWER_EXTERNAL_URL = 'verificationAnswerExternalUrl'; + case VERIFICATION_GET_EXTERNAL_URL = 'verificationGetExternalUrl'; // NOT IMPLEMENTED + case VERIFICATION_ANSWER_EXTERNAL_URL = 'verificationAnswerExternalUrl'; // NOT IMPLEMENTED case VERIFICATION_PASSWORD_AUTHENTICATION = 'verificationPasswordAuthentication'; case VERIFICATION_OTP_AUTHENTICATION = 'verificationOtpAuthentication'; From 17f20b25b83f24a34d544c2600abc3549ca6fc66 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 7 Jan 2025 15:29:37 -0500 Subject: [PATCH 132/420] Add new RPC methods for client capabilities and authentication --- src/Socialbox/SocialClient.php | 36 +++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/Socialbox/SocialClient.php b/src/Socialbox/SocialClient.php index ad44546..20b2675 100644 --- a/src/Socialbox/SocialClient.php +++ b/src/Socialbox/SocialClient.php @@ -23,7 +23,6 @@ * * @param string|PeerAddress $peerAddress The address of the peer to connect to. * @param ExportedSession|null $exportedSession Optional. The exported session to use for communication. - * @param array $options Optional. Additional options to pass to the client. * @throws CryptographyException If the public key is invalid. * @throws ResolutionException If the domain cannot be resolved. * @throws RpcException If the RPC request fails. @@ -59,6 +58,19 @@ )->getResponse()->getResult()); } + /** + * Retrieves the list of allowed methods, these are the methods that can be called by the client. + * + * @return array The allowed methods returned from the RPC request. + * @throws RpcException Thrown if the RPC request fails. + */ + public function getAllowedMethods(): array + { + return $this->sendRequest( + new RpcRequest(StandardMethods::GET_ALLOWED_METHODS->value, Utilities::randomCrc32()) + )->getResponse()->getResult(); + } + /** * Fetches the privacy policy document by sending a remote procedure call request. * @@ -320,6 +332,28 @@ )->getResponse()->getResult(); } + /** + * Authenticates a password by sending a remote procedure call request with an optional hashing operation. + * + * @param string $password The password to authenticate. + * @param bool $hash Indicates whether the password should be hashed using SHA-512 before authentication. + * @return bool The result of the password authentication request. + * @throws RpcException Thrown if the RPC request fails. + */ + public function verificationPasswordAuthentication(string $password, bool $hash=true): bool + { + if($hash) + { + $password = hash('sha512', $password); + } + + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::VERIFICATION_PASSWORD_AUTHENTICATION->value, Utilities::randomCrc32(), [ + 'password' => $password + ]) + )->getResponse()->getResult(); + } + /** * Sets a new password for settings with optional hashing. * From deb46679755d99fda9d28e8efd4ee5fc5888cbfd Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 7 Jan 2025 15:50:06 -0500 Subject: [PATCH 133/420] Add password, OTP, and user data deletion methods --- src/Socialbox/SocialClient.php | 141 ++++++++++++++++++++++++++++++++- 1 file changed, 140 insertions(+), 1 deletion(-) diff --git a/src/Socialbox/SocialClient.php b/src/Socialbox/SocialClient.php index 20b2675..9579abe 100644 --- a/src/Socialbox/SocialClient.php +++ b/src/Socialbox/SocialClient.php @@ -4,6 +4,7 @@ namespace Socialbox; + use Socialbox\Classes\Cryptography; use Socialbox\Classes\RpcClient; use Socialbox\Classes\Utilities; use Socialbox\Enums\StandardMethods; @@ -338,6 +339,7 @@ * @param string $password The password to authenticate. * @param bool $hash Indicates whether the password should be hashed using SHA-512 before authentication. * @return bool The result of the password authentication request. + * @throws CryptographyException Thrown if the password hash is invalid. * @throws RpcException Thrown if the RPC request fails. */ public function verificationPasswordAuthentication(string $password, bool $hash=true): bool @@ -346,6 +348,10 @@ { $password = hash('sha512', $password); } + elseif(!Cryptography::validateSha512($password)) + { + throw new CryptographyException('Invalid SHA-512 hash provided'); + } return (bool)$this->sendRequest( new RpcRequest(StandardMethods::VERIFICATION_PASSWORD_AUTHENTICATION->value, Utilities::randomCrc32(), [ @@ -354,19 +360,79 @@ )->getResponse()->getResult(); } + /** + * Authenticates an OTP code for verification purposes + * + * @param string $code The OTP code to be authenticated. + * @return bool True if the OTP authentication is successful, otherwise false. + * @throws RpcException Thrown if the RPC request fails. + */ + public function verificationOtpAuthentication(string $code): bool + { + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::VERIFICATION_OTP_AUTHENTICATION->value, Utilities::randomCrc32(), [ + 'code' => $code + ]) + )->getResponse()->getResult(); + } + /** * Sets a new password for settings with optional hashing. * * @param string $password The password to be set. If hashing is enabled, the password will be hashed before being sent. * @param bool $hash Optional. Determines whether the password should be hashed. Default is true. If false, the input is expected to be hashed using sha512. * @return true Returns true if the password is successfully set. + * @throws CryptographyException Thrown if the password hash is invalid. * @throws RpcException Thrown if the RPC request fails. */ public function settingsSetPassword(string $password, bool $hash=true): true { + if($hash) + { + $password = Cryptography::hashPassword($password); + } + elseif(!Cryptography::validatePasswordHash($password)) + { + throw new CryptographyException('Invalid password hash provided'); + } + return (bool)$this->sendRequest( new RpcRequest(StandardMethods::SETTINGS_SET_PASSWORD->value, Utilities::randomCrc32(), [ - 'password' => $hash ? hash('sha512', $password) : $password + 'password' => $password + ]) + )->getResponse()->getResult(); + } + + /** + * Deletes the user's password settings by sending a remote procedure call request. + * + * @param string $password The password to be deleted. + * @return true Indicates successful deletion of the password. + * @throws RpcException Thrown if the RPC request fails. + */ + public function settingsDeletePassword(string $password): true + { + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_DELETE_PASSWORD->value, Utilities::randomCrc32(), [ + 'password' => $password + ]) + )->getResponse()->getResult(); + } + + /** + * Updates the user's password by sending a remote procedure call request. + * + * @param string $password The new password to be set. + * @param string $existingPassword The current password for authentication. + * @return bool True if the password was successfully updated, false otherwise. + * @throws RpcException Thrown if the RPC request fails. + */ + public function settingsUpdatePassword(string $password, string $existingPassword): bool + { + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_UPDATE_PASSWORD->value, Utilities::randomCrc32(), [ + 'password' => $password, + 'existing_password' => $existingPassword ]) )->getResponse()->getResult(); } @@ -386,6 +452,33 @@ )->getResponse()->getResult(); } + /** + * Deletes the one-time password (OTP) settings by sending a remote procedure call request. + * + * @param string|null $password The password to authenticate the request. If provided, it will be hashed using SHA-512 if $hash is true. + * @param bool $hash Indicates whether to hash the password before sending the request. Defaults to true. + * @return bool True if the OTP settings were successfully deleted, false otherwise. + * @throws CryptographyException Thrown if the password hash is invalid. + * @throws RpcException Thrown if the RPC request fails. + */ + public function settingsDeleteOtp(?string $password=null, bool $hash=true): bool + { + if($hash && $password !== null) + { + $password = hash('sha512', $password); + } + elseif($password !== null && !Cryptography::validateSha512($password)) + { + throw new CryptographyException('Invalid SHA-512 hash provided'); + } + + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_DELETE_OTP->value, Utilities::randomCrc32(), [ + 'password' => $password + ]) + )->getResponse()->getResult(); + } + /** * Sets the display name in the settings by sending a remote procedure call request. * @@ -402,6 +495,16 @@ )->getResponse()->getResult(); } + /** + * + */ + public function settingsDeleteDisplayName(): true + { + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_DELETE_DISPLAY_NAME->value, Utilities::randomCrc32()) + )->getResponse()->getResult(); + } + /** * Updates the display picture by sending a remote procedure call request with the specified file identifier. * @@ -434,6 +537,19 @@ )->getResponse()->getResult(); } + /** + * Deletes the email associated with the user settings by sending a remote procedure call request. + * + * @return true Returns true if the email deletion request is successful. + * @throws RpcException + */ + public function settingsDeleteEmail(): true + { + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_DELETE_EMAIL->value, Utilities::randomCrc32()) + )->getResponse()->getResult(); + } + /** * Updates the phone number in the settings by sending a remote procedure call request. * @@ -450,6 +566,16 @@ )->getResponse()->getResult(); } + /** + * + */ + public function settingsDeletePhone(): true + { + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_DELETE_PHONE->value, Utilities::randomCrc32()) + )->getResponse()->getResult(); + } + /** * Updates the user's birthday by sending a remote procedure call request with the specified date. * @@ -469,4 +595,17 @@ ]) )->getResponse()->getResult(); } + + /** + * Deletes the saved birthday setting by sending a remote procedure call request. + * + * @return true Returns true if the birthday deletion request is successful. + * @throws RpcException Thrown if the RPC request fails. + */ + public function deleteBirthday(): true + { + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_DELETE_BIRTHDAY->value, Utilities::randomCrc32()) + )->getResponse()->getResult(); + } } \ No newline at end of file From bfe8064a941de7eb3170c9df3952fb94f29e40f3 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 7 Jan 2025 21:03:53 -0500 Subject: [PATCH 134/420] Add mocking functionality to ServerResolver class --- src/Socialbox/Classes/ServerResolver.php | 67 ++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/src/Socialbox/Classes/ServerResolver.php b/src/Socialbox/Classes/ServerResolver.php index c74a656..d8338ca 100644 --- a/src/Socialbox/Classes/ServerResolver.php +++ b/src/Socialbox/Classes/ServerResolver.php @@ -3,12 +3,16 @@ namespace Socialbox\Classes; use InvalidArgumentException; + use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\ResolutionException; use Socialbox\Managers\ResolvedDnsRecordsManager; use Socialbox\Objects\DnsRecord; class ServerResolver { + private static bool $mockingEnabled = false; + private static array $mockedRecords = []; + /** * Resolves a domain by retrieving and parsing its DNS TXT records. * Optionally checks a database for cached resolution data before performing a DNS query. @@ -17,9 +21,19 @@ * @param bool $useDatabase Whether to check the database for cached resolution data; defaults to true. * @return DnsRecord The parsed DNS record for the given domain. * @throws ResolutionException If the DNS TXT records cannot be retrieved or parsed. + * @throws DatabaseOperationException If an error occurs while interacting with the database. (Only if $useDatabase is true) */ public static function resolveDomain(string $domain, bool $useDatabase=true): DnsRecord { + // Return the mocked record if mocking is enabled + if(self::$mockingEnabled) + { + if(isset(self::$mockedRecords[$domain])) + { + return self::$mockedRecords[$domain]; + } + } + // Check the database if enabled if ($useDatabase) { @@ -91,4 +105,57 @@ } return $fullRecordBuilder; } + + /** + * Determines if mocking is enabled. + * + * @return bool True if mocking is enabled, false otherwise. + */ + public static function isMockingEnabled(): bool + { + return self::$mockingEnabled; + } + + /** + * Enables or disables mocking functionality. + * + * @param bool $enabled Indicates whether mocking should be enabled (true) or disabled (false). + * @return void + */ + public static function setMockingEnabled(bool $enabled): void + { + self::$mockingEnabled = $enabled; + } + + /** + * Retrieves the mocked records. + * + * @return array The list of mocked records. + */ + public static function getMockedRecords(): array + { + return self::$mockedRecords; + } + + /** + * Adds a mock DNS record for a specific domain. + * + * @param string $domain The domain name for which the DNS record is being mocked. + * @param DnsRecord $record The DNS record to be associated with the specified domain. + * @return void + */ + public static function mockRecord(string $domain, DnsRecord $record): void + { + self::$mockedRecords[$domain] = $record; + } + + /** + * Clears all mocked records by resetting the mocked records array. + * + * @return void + */ + public static function clearMockedRecords(): void + { + self::$mockedRecords = []; + } } \ No newline at end of file From e784137480d89e0b46395d67c984143358deda04 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 8 Jan 2025 04:17:06 -0500 Subject: [PATCH 135/420] Add support for mock servers and DNS mocking. --- .../Classes/CliCommands/InitializeCommand.php | 57 ++++++++++++++++++- src/Socialbox/Classes/Configuration.php | 3 + 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/Socialbox/Classes/CliCommands/InitializeCommand.php b/src/Socialbox/Classes/CliCommands/InitializeCommand.php index 6a2f412..c978e1a 100644 --- a/src/Socialbox/Classes/CliCommands/InitializeCommand.php +++ b/src/Socialbox/Classes/CliCommands/InitializeCommand.php @@ -3,11 +3,13 @@ namespace Socialbox\Classes\CliCommands; use Exception; + use ncc\ThirdParty\Symfony\Process\Exception\InvalidArgumentException; use PDOException; use Socialbox\Abstracts\CacheLayer; use Socialbox\Classes\Configuration; use Socialbox\Classes\Cryptography; use Socialbox\Classes\Database; + use Socialbox\Classes\DnsHelper; use Socialbox\Classes\Logger; use Socialbox\Classes\Resources; use Socialbox\Enums\DatabaseObjects; @@ -358,12 +360,64 @@ } } + // Handle Mock Servers environment variables (SB_INSTANCE_MOCK_SERVER_*) + $mockServers = []; + foreach(self::getMockServerValues() as $mockServer) + { + $mockServer = explode(' ', $mockServer); + if(count($mockServer) !== 2) + { + Logger::getLogger()->warning(sprintf('Invalid Mock Server format: %s', implode(' ', $mockServer))); + continue; + } + + $domain = $mockServer[1]; + + try + { + $txt = DnsHelper::parseTxt($mockServer[2]); + } + catch(InvalidArgumentException $e) + { + Logger::getLogger()->warning(sprintf('Invalid TXT record format for %s: %s', $domain, $e->getMessage())); + continue; + } + + $mockServers[$domain] = $txt; + } + + if(count($mockServers) > 0) + { + Logger::getLogger()->info('Setting Mock Servers...'); + Configuration::getConfigurationLib()->set('instance.mock_servers', $mockServers); + } + // Apply changes & reload the configuration Logger::getLogger()->info('Updating configuration...'); Configuration::getConfigurationLib()->save(); // Save Configuration::reload(); // Reload } + /** + * Retrieves all environment variable values that start with the prefix 'SB_INSTANCE_MOCK_SERVER_'. + * + * @return array An array of environment variable values filtered by the specified prefix. + */ + private static function getMockServerValues(): array + { + // Fetch all environment variables + $envVars = getenv(); + + // Filter variables that start with the specified prefix + $filtered = array_filter($envVars, function ($key) + { + return str_starts_with($key, 'SB_INSTANCE_MOCK_SERVER_'); + }, ARRAY_FILTER_USE_KEY); + + // Return only the values as an array + return array_values($filtered); + } + /** * @inheritDoc */ @@ -398,7 +452,8 @@ " SB_CACHE_PORT - The cache port (default: 6379)\n" . " SB_CACHE_USERNAME - The cache username (default: null)\n" . " SB_CACHE_PASSWORD - The cache password (default: null)\n" . - " SB_CACHE_DATABASE - The cache database (default: 0)\n"; + " SB_CACHE_DATABASE - The cache database (default: 0)\n" . + " SB_INSTANCE_MOCK_SERVER_* - Mock server environment variables, format: ( ), eg; SB_INSTANCE_MOCK_SERVER_N64: teapot.com \n"; } /** diff --git a/src/Socialbox/Classes/Configuration.php b/src/Socialbox/Classes/Configuration.php index 74a6fca..dd07b14 100644 --- a/src/Socialbox/Classes/Configuration.php +++ b/src/Socialbox/Classes/Configuration.php @@ -42,6 +42,9 @@ $config->setDefault('instance.name', "Socialbox Server"); $config->setDefault('instance.domain', null); $config->setDefault('instance.rpc_endpoint', null); + // DNS Mocking Configuration, usually used for testing purposes + // Allows the user to mock a domain to use a specific TXT record + $config->setDefault('instance.dns_mocks', []); // Security Configuration $config->setDefault('security.display_internal_exceptions', false); From 7ac1bead49f32e4cc448c1a945fae92316945315 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 8 Jan 2025 14:53:48 -0500 Subject: [PATCH 136/420] Refactor DNS mocking implementation. --- .../Classes/CliCommands/InitializeCommand.php | 10 ++--- .../Configuration/InstanceConfiguration.php | 10 +++++ src/Socialbox/Classes/ServerResolver.php | 41 ++++++------------- 3 files changed, 27 insertions(+), 34 deletions(-) diff --git a/src/Socialbox/Classes/CliCommands/InitializeCommand.php b/src/Socialbox/Classes/CliCommands/InitializeCommand.php index c978e1a..c44c240 100644 --- a/src/Socialbox/Classes/CliCommands/InitializeCommand.php +++ b/src/Socialbox/Classes/CliCommands/InitializeCommand.php @@ -360,14 +360,14 @@ } } - // Handle Mock Servers environment variables (SB_INSTANCE_MOCK_SERVER_*) + // Handle Mock Servers environment variables (SB_INSTANCE_DNS_MOCK_*) $mockServers = []; foreach(self::getMockServerValues() as $mockServer) { $mockServer = explode(' ', $mockServer); if(count($mockServer) !== 2) { - Logger::getLogger()->warning(sprintf('Invalid Mock Server format: %s', implode(' ', $mockServer))); + Logger::getLogger()->warning(sprintf('Invalid DNS Mock Server format: %s', implode(' ', $mockServer))); continue; } @@ -399,7 +399,7 @@ } /** - * Retrieves all environment variable values that start with the prefix 'SB_INSTANCE_MOCK_SERVER_'. + * Retrieves all environment variable values that start with the prefix 'SB_INSTANCE_DNS_MOCK_'. * * @return array An array of environment variable values filtered by the specified prefix. */ @@ -411,7 +411,7 @@ // Filter variables that start with the specified prefix $filtered = array_filter($envVars, function ($key) { - return str_starts_with($key, 'SB_INSTANCE_MOCK_SERVER_'); + return str_starts_with($key, 'SB_INSTANCE_DNS_MOCK_'); }, ARRAY_FILTER_USE_KEY); // Return only the values as an array @@ -453,7 +453,7 @@ " SB_CACHE_USERNAME - The cache username (default: null)\n" . " SB_CACHE_PASSWORD - The cache password (default: null)\n" . " SB_CACHE_DATABASE - The cache database (default: 0)\n" . - " SB_INSTANCE_MOCK_SERVER_* - Mock server environment variables, format: ( ), eg; SB_INSTANCE_MOCK_SERVER_N64: teapot.com \n"; + " SB_INSTANCE_DNS_MOCK_* - Mock server environment variables, format: ( ), eg; SB_INSTANCE_DNS_MOCK_N64: teapot.com \n"; } /** diff --git a/src/Socialbox/Classes/Configuration/InstanceConfiguration.php b/src/Socialbox/Classes/Configuration/InstanceConfiguration.php index 431b9c4..7609e49 100644 --- a/src/Socialbox/Classes/Configuration/InstanceConfiguration.php +++ b/src/Socialbox/Classes/Configuration/InstanceConfiguration.php @@ -8,6 +8,7 @@ private string $name; private ?string $domain; private ?string $rpcEndpoint; + private array $dnsMocks; /** * Constructor that initializes object properties with the provided data. @@ -21,6 +22,7 @@ $this->name = $data['name']; $this->domain = $data['domain']; $this->rpcEndpoint = $data['rpc_endpoint']; + $this->dnsMocks = $data['dns_mocks']; } /** @@ -55,4 +57,12 @@ { return $this->rpcEndpoint; } + + /** + * @return array + */ + public function getDnsMocks(): array + { + return $this->dnsMocks; + } } \ No newline at end of file diff --git a/src/Socialbox/Classes/ServerResolver.php b/src/Socialbox/Classes/ServerResolver.php index d8338ca..c34138c 100644 --- a/src/Socialbox/Classes/ServerResolver.php +++ b/src/Socialbox/Classes/ServerResolver.php @@ -10,7 +10,6 @@ class ServerResolver { - private static bool $mockingEnabled = false; private static array $mockedRecords = []; /** @@ -25,18 +24,23 @@ */ public static function resolveDomain(string $domain, bool $useDatabase=true): DnsRecord { - // Return the mocked record if mocking is enabled - if(self::$mockingEnabled) + // Return the mocked record if the mocking record is set + if(isset(self::$mockedRecords[$domain])) { - if(isset(self::$mockedRecords[$domain])) - { - return self::$mockedRecords[$domain]; - } + return self::$mockedRecords[$domain]; + } + + // Return the mocked record from the configuration if one is set + if(isset(Configuration::getInstanceConfiguration()->getDnsMocks()[$domain])) + { + return DnsHelper::parseTxt(Configuration::getInstanceConfiguration()->getDnsMocks()[$domain]); } // Check the database if enabled if ($useDatabase) { + // Return from the database cache if one exists + // TODO: Implement renewal here $resolvedServer = ResolvedDnsRecordsManager::getDnsRecord($domain); if ($resolvedServer !== null) { @@ -44,12 +48,12 @@ } } + // Resolve DNS & Records $txtRecords = self::dnsGetTxtRecords($domain); if ($txtRecords === false) { throw new ResolutionException(sprintf("Failed to resolve DNS TXT records for %s", $domain)); } - $fullRecord = self::concatenateTxtRecords($txtRecords); try @@ -106,27 +110,6 @@ return $fullRecordBuilder; } - /** - * Determines if mocking is enabled. - * - * @return bool True if mocking is enabled, false otherwise. - */ - public static function isMockingEnabled(): bool - { - return self::$mockingEnabled; - } - - /** - * Enables or disables mocking functionality. - * - * @param bool $enabled Indicates whether mocking should be enabled (true) or disabled (false). - * @return void - */ - public static function setMockingEnabled(bool $enabled): void - { - self::$mockingEnabled = $enabled; - } - /** * Retrieves the mocked records. * From 86acae60a2c6770f73d103fe20a8a755776d7916 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 8 Jan 2025 15:02:44 -0500 Subject: [PATCH 137/420] Add support for authentication_otp database object --- .idea/sqldialects.xml | 1 + .../Resources/database/authentication_otp.sql | 18 ++++++++++++++++++ src/Socialbox/Enums/DatabaseObjects.php | 12 ++++++++++-- 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 src/Socialbox/Classes/Resources/database/authentication_otp.sql diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml index 4bbb771..38a0168 100644 --- a/.idea/sqldialects.xml +++ b/.idea/sqldialects.xml @@ -1,6 +1,7 @@ + diff --git a/src/Socialbox/Classes/Resources/database/authentication_otp.sql b/src/Socialbox/Classes/Resources/database/authentication_otp.sql new file mode 100644 index 0000000..788e100 --- /dev/null +++ b/src/Socialbox/Classes/Resources/database/authentication_otp.sql @@ -0,0 +1,18 @@ +create table authentication_otp +( + peer_uuid varchar(36) not null comment 'The Peer UUID associated with this record' + primary key comment 'The Peer UUID unique Index', + secret mediumtext not null comment 'The encrypted secret for the OTP', + updated timestamp default current_timestamp() not null comment 'The Timestamp for when the record was last updated', + constraint authentication_otp_peer_uuid_uindex + unique (peer_uuid) comment 'The Peer UUID unique Index', + constraint authentication_otp_registered_peers_uuid_fk + foreign key (peer_uuid) references registered_peers (uuid) + on update cascade on delete cascade +) + comment 'Table for housing encrypted OTP secrets for for verification'; + +create index authentication_otp_updated_index + on authentication_otp (updated) + comment 'The index for the updated column'; + diff --git a/src/Socialbox/Enums/DatabaseObjects.php b/src/Socialbox/Enums/DatabaseObjects.php index 14d7f8f..5a30afe 100644 --- a/src/Socialbox/Enums/DatabaseObjects.php +++ b/src/Socialbox/Enums/DatabaseObjects.php @@ -10,6 +10,7 @@ case REGISTERED_PEERS = 'registered_peers.sql'; case AUTHENTICATION_PASSWORDS = 'authentication_passwords.sql'; + case AUTHENTICATION_OTP = 'authentication_otp.sql'; case CAPTCHA_IMAGES = 'captcha_images.sql'; case SESSIONS = 'sessions.sql'; case EXTERNAL_SESSIONS = 'external_sessions.sql'; @@ -23,9 +24,16 @@ { return match ($this) { - self::VARIABLES, self::RESOLVED_DNS_RECORDS => 0, + self::VARIABLES, + self::RESOLVED_DNS_RECORDS => 0, + self::REGISTERED_PEERS => 1, - self::AUTHENTICATION_PASSWORDS, self::CAPTCHA_IMAGES, self::SESSIONS, self::EXTERNAL_SESSIONS => 2, + + self::AUTHENTICATION_PASSWORDS, + self::AUTHENTICATION_OTP, + self::CAPTCHA_IMAGES, + self::SESSIONS, + self::EXTERNAL_SESSIONS => 2, }; } From 8b5d2e23e38fd54d0c997afd6996039bcdeef441 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 8 Jan 2025 15:05:04 -0500 Subject: [PATCH 138/420] Add libsodium-dev and enable libsodium in Dockerfile --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 22628b7..7078dbd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,6 +32,7 @@ RUN apt-get update -yqq && apt-get install -yqq --no-install-recommends \ libmemcached-dev \ redis \ libgd-dev \ + libsodium-dev \ nginx \ && apt-get clean && rm -rf /var/lib/apt/lists/* @@ -47,7 +48,7 @@ RUN docker-php-ext-install -j$(nproc) \ zip \ pcntl && \ pecl install redis memcached && \ - docker-php-ext-enable redis memcached + docker-php-ext-enable redis memcached libsodium23 \ # ----------------------------- Additional Tools ----------------------------- # Install Phive (Package Manager for PHAR libraries) and global tools in one step From b028e0245181955bfbd82be214d6be6c8da949c6 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 8 Jan 2025 15:49:25 -0500 Subject: [PATCH 139/420] ??? Reverted Changes, works now. --- Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7078dbd..22628b7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,7 +32,6 @@ RUN apt-get update -yqq && apt-get install -yqq --no-install-recommends \ libmemcached-dev \ redis \ libgd-dev \ - libsodium-dev \ nginx \ && apt-get clean && rm -rf /var/lib/apt/lists/* @@ -48,7 +47,7 @@ RUN docker-php-ext-install -j$(nproc) \ zip \ pcntl && \ pecl install redis memcached && \ - docker-php-ext-enable redis memcached libsodium23 \ + docker-php-ext-enable redis memcached # ----------------------------- Additional Tools ----------------------------- # Install Phive (Package Manager for PHAR libraries) and global tools in one step From e7f86bcd1d53b97748f6587fd210113649cc2918 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 9 Jan 2025 13:29:09 -0500 Subject: [PATCH 140/420] Refactor service configurations and add Coffee/Teapot setup. --- .env | 12 +- docker-compose.test.yml | 255 +++++++++++++++++++++++----------------- docker-compose.yml | 3 - redis.conf | 1 + 4 files changed, 155 insertions(+), 116 deletions(-) diff --git a/.env b/.env index 5dab48f..903fd2e 100644 --- a/.env +++ b/.env @@ -26,7 +26,11 @@ MYSQL_PASSWORD=socialbox REDIS_PASSWORD=root # Test Configuration, can be ignored. Used for docker-compose-test.yml -SB_ALICE_DOMAIN=localhost -SB_ALICE_RPC_ENDPOINT=http://127.0.0.0:8086/ -SB_BOB_DOMAIN=localhost -SB_BOB_RPC_ENDPOINT=http://127.0.0.0:8087/ \ No newline at end of file +SB_COFFEE_NAME=coffee +SB_COFFEE_DOMAIN=coffee.com +SB_COFFEE_RPC_ENDPOINT=http://127.0.0.0:8086/ +SB_INSTANCE_DNS_MOCK_COFFEE="coffee.com " + +SB_TEAPOT_DOMAIN=teapot.com +SB_TEAPOT_RPC_ENDPOINT=http://127.0.0.0:8087/ +SB_INSTANCE_DNS_MOCK_TEAPOT="teapot.com " \ No newline at end of file diff --git a/docker-compose.test.yml b/docker-compose.test.yml index ad9a48f..beec78d 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -1,66 +1,29 @@ -services: - alice_socialbox: - container_name: alice_socialbox - build: - context: . - dockerfile: Dockerfile - ports: - - "8087:8085" - depends_on: - alice_mariadb: - condition: service_healthy - alice_redis: - condition: service_healthy - networks: - - alice_network - restart: unless-stopped - volumes: - - ./alice_socialbox/config:/etc/config - - ./alice_socialbox/logs:/var/log - - ./alice_socialbox/data:/etc/socialbox - environment: - # No need to change these values - LOG_LEVEL: ${LOG_LEVEL:-debug} - CONFIGLIB_PATH: /etc/config - LOGGING_DIRECTORY: /var/log - SB_MODE: automated - SB_STORAGE_PATH: /etc/socialbox - # Updated environment variables for Alice - SB_INSTANCE_DOMAIN: ${SB_ALICE_DOMAIN:-localhost} - SB_INSTANCE_RPC_ENDPOINT: ${SB_ALICE_RPC_ENDPOINT:-http://127.0.0.1:8087/} - SB_DATABASE_HOST: alice_mariadb - SB_DATABASE_USERNAME: ${MYSQL_USER:-socialbox} - SB_DATABASE_PASSWORD: ${MYSQL_PASSWORD:-socialbox} - SB_DATABASE_NAME: ${MYSQL_DATABASE:-socialbox} - SB_CACHE_ENGINE: redis - SB_CACHE_HOST: alice_redis - SB_CACHE_PASSWORD: ${REDIS_PASSWORD:-root} - healthcheck: - test: ["CMD", "curl", "-f", "-H", "Request-Type: ping", "${SB_INSTANCE_RPC_ENDPOINT-http://127.0.0.0:8085/}"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s +# Test docker-compose file for SocialBox service to setup two instances of the service: +# 1. Teapot Service (teapot.com) +# 2. Coffee Service (coffee.com) - bob_socialbox: - container_name: bob_socialbox +services: + + # Coffee Service (coffee.com test) + socialbox_coffee: + container_name: socialbox_coffee build: context: . dockerfile: Dockerfile ports: - "8086:8085" depends_on: - bob_mariadb: + mariadb_coffee: condition: service_healthy - bob_redis: + redis_coffee: condition: service_healthy networks: - - bob_network + - coffee_network restart: unless-stopped volumes: - - ./bob_socialbox/config:/etc/config - - ./bob_socialbox/logs:/var/log - - ./bob_socialbox/data:/etc/socialbox + - ./socialbox_coffee/config:/etc/config + - ./socialbox_coffee/logs:/var/log + - ./socialbox_coffee/data:/etc/socialbox environment: # No need to change these values LOG_LEVEL: ${LOG_LEVEL:-debug} @@ -68,25 +31,44 @@ services: LOGGING_DIRECTORY: /var/log SB_MODE: automated SB_STORAGE_PATH: /etc/socialbox - # Updated environment variables for Bob - SB_INSTANCE_DOMAIN: ${SB_BOB_DOMAIN:-localhost} - SB_INSTANCE_RPC_ENDPOINT: ${SB_BOB_RPC_ENDPOINT:-http://127.0.0.1:8086/} - SB_DATABASE_HOST: bob_mariadb + # Change these values to match your environment or update the .env file + SB_INSTANCE_NAME: ${SB_COFFEE_NAME:-coffee} # Instance name SB_COFFEE_NAME + SB_INSTANCE_DOMAIN: ${SB_COFFEE_DOMAIN:-coffee.com} # Instance domain SB_COFFEE_DOMAIN + SB_INSTANCE_RPC_ENDPOINT: ${SB_COFFEE_RPC_ENDPOINT:-http://127.0.0.0:8086/} # Instance RPC endpoint SB_COFFEE_RPC_ENDPOINT + SB_LOGGING_CONSOLE_ENABLED: ${SB_LOGGING_CONSOLE_ENABLED:-true} + SB_LOGGING_CONSOLE_LEVEL: ${SB_LOGGING_CONSOLE_LEVEL:-debug} + SB_LOGGING_FILE_ENABLED: ${SB_LOGGING_FILE_ENABLED:-true} + SB_LOGGING_FILE_LEVEL: ${SB_LOGGING_FILE_LEVEL:-debug} + SB_SECURITY_DISPLAY_INTERNAL_EXCEPTIONS: ${SB_SECURITY_DISPLAY_INTERNAL_EXCEPTIONS:-true} + SB_CRYPTO_KEYPAIR_EXPIRES: ${SB_CRYPTO_KEYPAIR_EXPIRES} + SB_CRYPTO_ENCRYPTION_KEYS_COUNT: ${SB_CRYPTO_ENCRYPTION_KEYS_COUNT:-10} + SB_CRYPTO_ENCRYPTION_KEYS_ALGORITHM: ${SB_CRYPTO_ENCRYPTION_KEYS_ALGORITHM:-xchacha20} + SB_CRYPTO_TRANSPORT_ENCRYPTION_ALGORITHM: ${SB_CRYPTO_TRANSPORT_ENCRYPTION_ALGORITHM:-chacha20} + SB_DATABASE_HOST: mariadb_coffee SB_DATABASE_USERNAME: ${MYSQL_USER:-socialbox} SB_DATABASE_PASSWORD: ${MYSQL_PASSWORD:-socialbox} SB_DATABASE_NAME: ${MYSQL_DATABASE:-socialbox} + SB_CACHE_ENABLED: ${SB_CACHE_ENABLED:-true} SB_CACHE_ENGINE: redis - SB_CACHE_HOST: bob_redis - SB_CACHE_PASSWORD: ${REDIS_PASSWORD:-root} + SB_CACHE_HOST: redis_coffee + SB_CACHE_PORT: ${SB_CACHE_PORT:-6379} + SB_CACHE_USERNAME: ${SB_CACHE_USERNAME:-root} + SB_CACHE_PASSWORD: ${SB_CACHE_PASSWORD:-root} + SB_CACHE_DATABASE: ${SB_CACHE_DATABASE:-0} + # Mocking, required for testing without the need for configuring actual DNS records + # Usage: SB_INSTANCE_DNS_MOCK_: + # Environment Variable name is ignored, only the value is used with the prefix being used to detect + # the instance name and the suffix being used to detect the TXT record + SB_INSTANCE_DNS_MOCK_COFFEE: ${SB_INSTANCE_DNS_MOCK_COFFEE:-http://127.0.0.1:8086/} + SB_INSTANCE_DNS_MOCK_TEAPOT: ${SB_INSTANCE_DNS_MOCK_TEAPOT:-http://127.0.0.1:8087/} healthcheck: - test: ["CMD", "curl", "-f", "-H", "Request-Type: ping", "${SB_INSTANCE_RPC_ENDPOINT-http://127.0.0.0:8085/}"] + test: ["CMD", "curl", "-f", "-H", "Request-Type: ping", "${SB_INSTANCE_RPC_ENDPOINT-http://127.0.0.0:8086/}"] interval: 30s timeout: 10s retries: 3 start_period: 40s - - alice_mariadb: - container_name: alice_mariadb + mariadb_coffee: + container_name: socialbox_coffee_mariadb image: mariadb:10.5 restart: unless-stopped environment: @@ -95,53 +77,27 @@ services: MYSQL_USER: ${MYSQL_USER:-socialbox} MYSQL_PASSWORD: ${MYSQL_PASSWORD:-socialbox} volumes: - - alice_mariadb_data:/var/lib/mysql + - coffee_mariadb_data:/var/lib/mysql networks: - - alice_network + - coffee_network expose: - "3306" healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "alice_mariadb", "-u", "${MYSQL_USER:-socialbox}", "-p${MYSQL_PASSWORD:-socialbox}"] + test: ["CMD", "mysqladmin", "ping", "-h", "mariadb_coffee", "-u", "${MYSQL_USER:-socialbox}", "-p${MYSQL_PASSWORD:-socialbox}"] interval: 10s timeout: 5s retries: 3 start_period: 30s - - bob_mariadb: - container_name: bob_mariadb - image: mariadb:10.5 - restart: unless-stopped - environment: - MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-sb_root} - MYSQL_DATABASE: ${MYSQL_DATABASE:-socialbox} - MYSQL_USER: ${MYSQL_USER:-socialbox} - MYSQL_PASSWORD: ${MYSQL_PASSWORD:-socialbox} - volumes: - - bob_mariadb_data:/var/lib/mysql - networks: - - bob_network - expose: - - "3306" - healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "bob_mariadb", "-u", "${MYSQL_USER:-socialbox}", "-p${MYSQL_PASSWORD:-socialbox}"] - interval: 10s - timeout: 5s - retries: 3 - start_period: 30s - - alice_redis: - container_name: alice_redis + redis_coffee: + container_name: socialbox_coffee_redis image: redis:alpine restart: unless-stopped command: redis-server /usr/local/etc/redis/redis.conf --appendonly yes volumes: - - alice_redis_data:/data + - coffee_redis_data:/data - ./redis.conf:/usr/local/etc/redis/redis.conf networks: - - alice_network - environment: - REDIS_PASSWORD: ${REDIS_PASSWORD:-root} - REDIS_DB: 0 + - coffee_network expose: - "6379" healthcheck: @@ -151,19 +107,100 @@ services: retries: 3 start_period: 5s - bob_redis: - container_name: bob_redis + # Teapot Service (teapot.com test) + socialbox_teapot: + container_name: socialbox_teapot + build: + context: . + dockerfile: Dockerfile + ports: + - "8087:8085" # Unique port for Teapot instance + depends_on: + mariadb_teapot: + condition: service_healthy + redis_teapot: + condition: service_healthy + networks: + - teapot_network + restart: unless-stopped + volumes: + - ./socialbox_teapot/config:/etc/config + - ./socialbox_teapot/logs:/var/log + - ./socialbox_teapot/data:/etc/socialbox + environment: + # No need to change these values + LOG_LEVEL: ${LOG_LEVEL:-debug} + CONFIGLIB_PATH: /etc/config + LOGGING_DIRECTORY: /var/log + SB_MODE: automated + SB_STORAGE_PATH: /etc/socialbox + # Change these values to match your environment or update the .env file + SB_INSTANCE_NAME: ${SB_TEAPOT_NAME:-teapot} # Instance name SB_TEAPOT_NAME + SB_INSTANCE_DOMAIN: ${SB_TEAPOT_DOMAIN:-teapot.com} # Instance domain SB_TEAPOT_DOMAIN + SB_INSTANCE_RPC_ENDPOINT: ${SB_TEAPOT_RPC_ENDPOINT:-http://127.0.0.0:8087/} # Instance RPC endpoint SB_TEAPOT_RPC_ENDPOINT + SB_LOGGING_CONSOLE_ENABLED: ${SB_LOGGING_CONSOLE_ENABLED:-true} + SB_LOGGING_CONSOLE_LEVEL: ${SB_LOGGING_CONSOLE_LEVEL:-debug} + SB_LOGGING_FILE_ENABLED: ${SB_LOGGING_FILE_ENABLED:-true} + SB_LOGGING_FILE_LEVEL: ${SB_LOGGING_FILE_LEVEL:-debug} + SB_SECURITY_DISPLAY_INTERNAL_EXCEPTIONS: ${SB_SECURITY_DISPLAY_INTERNAL_EXCEPTIONS:-true} + SB_CRYPTO_KEYPAIR_EXPIRES: ${SB_CRYPTO_KEYPAIR_EXPIRES} + SB_CRYPTO_ENCRYPTION_KEYS_COUNT: ${SB_CRYPTO_ENCRYPTION_KEYS_COUNT:-10} + SB_CRYPTO_ENCRYPTION_KEYS_ALGORITHM: ${SB_CRYPTO_ENCRYPTION_KEYS_ALGORITHM:-xchacha20} + SB_CRYPTO_TRANSPORT_ENCRYPTION_ALGORITHM: ${SB_CRYPTO_TRANSPORT_ENCRYPTION_ALGORITHM:-chacha20} + SB_DATABASE_HOST: mariadb_teapot + SB_DATABASE_USERNAME: ${MYSQL_USER:-socialbox} + SB_DATABASE_PASSWORD: ${MYSQL_PASSWORD:-socialbox} + SB_DATABASE_NAME: ${MYSQL_DATABASE:-socialbox} + SB_CACHE_ENABLED: ${SB_CACHE_ENABLED:-true} + SB_CACHE_ENGINE: redis + SB_CACHE_HOST: redis_teapot + SB_CACHE_PORT: ${SB_CACHE_PORT:-6379} + SB_CACHE_USERNAME: ${SB_CACHE_USERNAME:-root} + SB_CACHE_PASSWORD: ${SB_CACHE_PASSWORD:-root} + SB_CACHE_DATABASE: ${SB_CACHE_DATABASE:-0} + # Mocking, required for testing without the need for configuring actual DNS records + # Usage: SB_INSTANCE_DNS_MOCK_: + # Environment Variable name is ignored, only the value is used with the prefix being used to detect + # the instance name and the suffix being used to detect the TXT record + SB_INSTANCE_DNS_MOCK_COFFEE: ${SB_INSTANCE_DNS_MOCK_COFFEE:-http://127.0.0.1:8086/} + SB_INSTANCE_DNS_MOCK_TEAPOT: ${SB_INSTANCE_DNS_MOCK_TEAPOT:-http://127.0.0.1:8087/} + healthcheck: + test: ["CMD", "curl", "-f", "-H", "Request-Type: ping", "${SB_INSTANCE_RPC_ENDPOINT-http://127.0.0.0:8087/}"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + mariadb_teapot: + container_name: socialbox_teapot_mariadb + image: mariadb:10.5 + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-sb_root} + MYSQL_DATABASE: ${MYSQL_DATABASE:-socialbox} + MYSQL_USER: ${MYSQL_USER:-socialbox} + MYSQL_PASSWORD: ${MYSQL_PASSWORD:-socialbox} + volumes: + - teapot_mariadb_data:/var/lib/mysql + networks: + - teapot_network + expose: + - "3306" + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "mariadb_teapot", "-u", "${MYSQL_USER:-socialbox}", "-p${MYSQL_PASSWORD:-socialbox}"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 30s + redis_teapot: + container_name: socialbox_teapot_redis image: redis:alpine restart: unless-stopped command: redis-server /usr/local/etc/redis/redis.conf --appendonly yes volumes: - - bob_redis_data:/data + - teapot_redis_data:/data - ./redis.conf:/usr/local/etc/redis/redis.conf networks: - - bob_network - environment: - REDIS_PASSWORD: ${REDIS_PASSWORD:-root} - REDIS_DB: 0 + - teapot_network expose: - "6379" healthcheck: @@ -174,19 +211,19 @@ services: start_period: 5s volumes: - alice_mariadb_data: + teapot_mariadb_data: driver: local - bob_mariadb_data: + teapot_redis_data: driver: local - alice_redis_data: + coffee_redis_data: driver: local - bob_redis_data: + coffee_mariadb_data: driver: local networks: - alice_network: + teapot_network: driver: bridge - name: alice_network - bob_network: + name: teapot_network + coffee_network: driver: bridge - name: bob_network \ No newline at end of file + name: coffee_network \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index d6e28fc..d4d0f75 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -88,9 +88,6 @@ services: - ./redis.conf:/usr/local/etc/redis/redis.conf networks: - internal_network - environment: - REDIS_PASSWORD: ${REDIS_PASSWORD:-root} - REDIS_DB: 0 expose: - "6379" healthcheck: diff --git a/redis.conf b/redis.conf index 6192ab8..dc6be78 100644 --- a/redis.conf +++ b/redis.conf @@ -2,3 +2,4 @@ bind 0.0.0.0 protected-mode yes port 6379 appendonly yes +requirepass root \ No newline at end of file From fc9e80785d35bd7e62fa4bf2214dc2a964bfbe71 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 9 Jan 2025 14:49:11 -0500 Subject: [PATCH 141/420] Rename service and container identifiers for clarity. --- docker-compose.test.yml | 64 ++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/docker-compose.test.yml b/docker-compose.test.yml index beec78d..955d552 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -5,25 +5,25 @@ services: # Coffee Service (coffee.com test) - socialbox_coffee: - container_name: socialbox_coffee + coffee_socialbox: + container_name: coffee_socialbox build: context: . dockerfile: Dockerfile ports: - "8086:8085" depends_on: - mariadb_coffee: + coffee_mariadb: condition: service_healthy - redis_coffee: + coffee_redis: condition: service_healthy networks: - coffee_network restart: unless-stopped volumes: - - ./socialbox_coffee/config:/etc/config - - ./socialbox_coffee/logs:/var/log - - ./socialbox_coffee/data:/etc/socialbox + - ./coffee_socialbox/config:/etc/config + - ./coffee_socialbox/logs:/var/log + - ./coffee_socialbox/data:/etc/socialbox environment: # No need to change these values LOG_LEVEL: ${LOG_LEVEL:-debug} @@ -44,13 +44,13 @@ services: SB_CRYPTO_ENCRYPTION_KEYS_COUNT: ${SB_CRYPTO_ENCRYPTION_KEYS_COUNT:-10} SB_CRYPTO_ENCRYPTION_KEYS_ALGORITHM: ${SB_CRYPTO_ENCRYPTION_KEYS_ALGORITHM:-xchacha20} SB_CRYPTO_TRANSPORT_ENCRYPTION_ALGORITHM: ${SB_CRYPTO_TRANSPORT_ENCRYPTION_ALGORITHM:-chacha20} - SB_DATABASE_HOST: mariadb_coffee + SB_DATABASE_HOST: coffee_mariadb SB_DATABASE_USERNAME: ${MYSQL_USER:-socialbox} SB_DATABASE_PASSWORD: ${MYSQL_PASSWORD:-socialbox} SB_DATABASE_NAME: ${MYSQL_DATABASE:-socialbox} SB_CACHE_ENABLED: ${SB_CACHE_ENABLED:-true} SB_CACHE_ENGINE: redis - SB_CACHE_HOST: redis_coffee + SB_CACHE_HOST: coffee_redis SB_CACHE_PORT: ${SB_CACHE_PORT:-6379} SB_CACHE_USERNAME: ${SB_CACHE_USERNAME:-root} SB_CACHE_PASSWORD: ${SB_CACHE_PASSWORD:-root} @@ -59,16 +59,16 @@ services: # Usage: SB_INSTANCE_DNS_MOCK_: # Environment Variable name is ignored, only the value is used with the prefix being used to detect # the instance name and the suffix being used to detect the TXT record - SB_INSTANCE_DNS_MOCK_COFFEE: ${SB_INSTANCE_DNS_MOCK_COFFEE:-http://127.0.0.1:8086/} - SB_INSTANCE_DNS_MOCK_TEAPOT: ${SB_INSTANCE_DNS_MOCK_TEAPOT:-http://127.0.0.1:8087/} + SB_INSTANCE_DNS_MOCK_COFFEE: ${SB_INSTANCE_DNS_MOCK_COFFEE:-"coffee.com "} + SB_INSTANCE_DNS_MOCK_TEAPOT: ${SB_INSTANCE_DNS_MOCK_TEAPOT:-"teapot.com "} healthcheck: test: ["CMD", "curl", "-f", "-H", "Request-Type: ping", "${SB_INSTANCE_RPC_ENDPOINT-http://127.0.0.0:8086/}"] interval: 30s timeout: 10s retries: 3 start_period: 40s - mariadb_coffee: - container_name: socialbox_coffee_mariadb + coffee_mariadb: + container_name: coffee_socialbox_mariadb image: mariadb:10.5 restart: unless-stopped environment: @@ -83,13 +83,13 @@ services: expose: - "3306" healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "mariadb_coffee", "-u", "${MYSQL_USER:-socialbox}", "-p${MYSQL_PASSWORD:-socialbox}"] + test: ["CMD", "mysqladmin", "ping", "-h", "coffee_mariadb", "-u", "${MYSQL_USER:-socialbox}", "-p${MYSQL_PASSWORD:-socialbox}"] interval: 10s timeout: 5s retries: 3 start_period: 30s - redis_coffee: - container_name: socialbox_coffee_redis + coffee_redis: + container_name: coffee_socialbox_redis image: redis:alpine restart: unless-stopped command: redis-server /usr/local/etc/redis/redis.conf --appendonly yes @@ -108,25 +108,25 @@ services: start_period: 5s # Teapot Service (teapot.com test) - socialbox_teapot: - container_name: socialbox_teapot + teapot_socialbox: + container_name: teapot_socialbox build: context: . dockerfile: Dockerfile ports: - "8087:8085" # Unique port for Teapot instance depends_on: - mariadb_teapot: + teapot_mariadb: condition: service_healthy - redis_teapot: + teapot_redis: condition: service_healthy networks: - teapot_network restart: unless-stopped volumes: - - ./socialbox_teapot/config:/etc/config - - ./socialbox_teapot/logs:/var/log - - ./socialbox_teapot/data:/etc/socialbox + - ./teapot_socialbox/config:/etc/config + - ./teapot_socialbox/logs:/var/log + - ./teapot_socialbox/data:/etc/socialbox environment: # No need to change these values LOG_LEVEL: ${LOG_LEVEL:-debug} @@ -147,13 +147,13 @@ services: SB_CRYPTO_ENCRYPTION_KEYS_COUNT: ${SB_CRYPTO_ENCRYPTION_KEYS_COUNT:-10} SB_CRYPTO_ENCRYPTION_KEYS_ALGORITHM: ${SB_CRYPTO_ENCRYPTION_KEYS_ALGORITHM:-xchacha20} SB_CRYPTO_TRANSPORT_ENCRYPTION_ALGORITHM: ${SB_CRYPTO_TRANSPORT_ENCRYPTION_ALGORITHM:-chacha20} - SB_DATABASE_HOST: mariadb_teapot + SB_DATABASE_HOST: teapot_mariadb SB_DATABASE_USERNAME: ${MYSQL_USER:-socialbox} SB_DATABASE_PASSWORD: ${MYSQL_PASSWORD:-socialbox} SB_DATABASE_NAME: ${MYSQL_DATABASE:-socialbox} SB_CACHE_ENABLED: ${SB_CACHE_ENABLED:-true} SB_CACHE_ENGINE: redis - SB_CACHE_HOST: redis_teapot + SB_CACHE_HOST: teapot_redis SB_CACHE_PORT: ${SB_CACHE_PORT:-6379} SB_CACHE_USERNAME: ${SB_CACHE_USERNAME:-root} SB_CACHE_PASSWORD: ${SB_CACHE_PASSWORD:-root} @@ -162,16 +162,16 @@ services: # Usage: SB_INSTANCE_DNS_MOCK_: # Environment Variable name is ignored, only the value is used with the prefix being used to detect # the instance name and the suffix being used to detect the TXT record - SB_INSTANCE_DNS_MOCK_COFFEE: ${SB_INSTANCE_DNS_MOCK_COFFEE:-http://127.0.0.1:8086/} - SB_INSTANCE_DNS_MOCK_TEAPOT: ${SB_INSTANCE_DNS_MOCK_TEAPOT:-http://127.0.0.1:8087/} + SB_INSTANCE_DNS_MOCK_COFFEE: ${SB_INSTANCE_DNS_MOCK_COFFEE:-"coffee.com "} + SB_INSTANCE_DNS_MOCK_TEAPOT: ${SB_INSTANCE_DNS_MOCK_TEAPOT:-"teapot.com "} healthcheck: test: ["CMD", "curl", "-f", "-H", "Request-Type: ping", "${SB_INSTANCE_RPC_ENDPOINT-http://127.0.0.0:8087/}"] interval: 30s timeout: 10s retries: 3 start_period: 40s - mariadb_teapot: - container_name: socialbox_teapot_mariadb + teapot_mariadb: + container_name: teapot_socialbox_mariadb image: mariadb:10.5 restart: unless-stopped environment: @@ -186,13 +186,13 @@ services: expose: - "3306" healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "mariadb_teapot", "-u", "${MYSQL_USER:-socialbox}", "-p${MYSQL_PASSWORD:-socialbox}"] + test: ["CMD", "mysqladmin", "ping", "-h", "teapot_mariadb", "-u", "${MYSQL_USER:-socialbox}", "-p${MYSQL_PASSWORD:-socialbox}"] interval: 10s timeout: 5s retries: 3 start_period: 30s - redis_teapot: - container_name: socialbox_teapot_redis + teapot_redis: + container_name: teapot_socialbox_redis image: redis:alpine restart: unless-stopped command: redis-server /usr/local/etc/redis/redis.conf --appendonly yes From 5d1ba6191686fc4f8e15c69da0cf4c1bb0d1a4f6 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 9 Jan 2025 15:00:07 -0500 Subject: [PATCH 142/420] Fix handling of DNS Mock Server input in InitializeCommand --- .../Classes/CliCommands/InitializeCommand.php | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/Socialbox/Classes/CliCommands/InitializeCommand.php b/src/Socialbox/Classes/CliCommands/InitializeCommand.php index c44c240..a9d57ef 100644 --- a/src/Socialbox/Classes/CliCommands/InitializeCommand.php +++ b/src/Socialbox/Classes/CliCommands/InitializeCommand.php @@ -371,19 +371,23 @@ continue; } - $domain = $mockServer[1]; - - try + $domain = $mockServer[0] ?? null; + $txt = $mockServer[1] ?? null; + if($domain === null || $txt === null) { - $txt = DnsHelper::parseTxt($mockServer[2]); - } - catch(InvalidArgumentException $e) - { - Logger::getLogger()->warning(sprintf('Invalid TXT record format for %s: %s', $domain, $e->getMessage())); + Logger::getLogger()->warning(sprintf('Invalid DNS Mock Server format, domain or txt missing: %s', implode(' ', $mockServer))); continue; } - $mockServers[$domain] = $txt; + try + { + $mockServers[$domain] = DnsHelper::parseTxt($txt); + } + catch(InvalidArgumentException $e) + { + Logger::getLogger()->error(sprintf('Invalid TXT record format for %s', $domain), $e); + continue; + } } if(count($mockServers) > 0) From b75946c70b1b8cbfa50ac70792652e0741504b09 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 9 Jan 2025 15:00:15 -0500 Subject: [PATCH 143/420] Refactor DNS record generation in DnsRecordCommand. --- src/Socialbox/Classes/CliCommands/DnsRecordCommand.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Socialbox/Classes/CliCommands/DnsRecordCommand.php b/src/Socialbox/Classes/CliCommands/DnsRecordCommand.php index 28ee6d5..4f56180 100644 --- a/src/Socialbox/Classes/CliCommands/DnsRecordCommand.php +++ b/src/Socialbox/Classes/CliCommands/DnsRecordCommand.php @@ -5,6 +5,7 @@ use Socialbox\Classes\Configuration; use Socialbox\Classes\Logger; use Socialbox\Interfaces\CliCommandInterface; + use Socialbox\Socialbox; class DnsRecordCommand implements CliCommandInterface { @@ -13,13 +14,8 @@ */ public static function execute(array $args): int { - $txt_record = sprintf('v=socialbox;sb-rpc=%s;sb-key=%s', - Configuration::getInstanceConfiguration()->getRpcEndpoint(), - Configuration::getCryptographyConfiguration()->getHostPublicKey() - ); - Logger::getLogger()->info('Please set the following DNS TXT record for the domain:'); - Logger::getLogger()->info(sprintf(' %s', $txt_record)); + Logger::getLogger()->info(sprintf(' %s', Socialbox::getDnsRecord())); return 0; } From 2a624305964542f9f807ba2a79365fa9b5a74b26 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 9 Jan 2025 15:06:29 -0500 Subject: [PATCH 144/420] Update DNS mock data and add key files for Coffee and Teapot --- .env | 4 ++-- docker-compose.test.yml | 8 ++++---- docker_test_keys/coffee.json | 20 ++++++++++++++++++++ docker_test_keys/coffee.txt | 1 + docker_test_keys/teapot.json | 20 ++++++++++++++++++++ docker_test_keys/teapot.txt | 1 + entrypoint.sh | 1 - 7 files changed, 48 insertions(+), 7 deletions(-) create mode 100644 docker_test_keys/coffee.json create mode 100644 docker_test_keys/coffee.txt create mode 100644 docker_test_keys/teapot.json create mode 100644 docker_test_keys/teapot.txt diff --git a/.env b/.env index 903fd2e..94b5d60 100644 --- a/.env +++ b/.env @@ -29,8 +29,8 @@ REDIS_PASSWORD=root SB_COFFEE_NAME=coffee SB_COFFEE_DOMAIN=coffee.com SB_COFFEE_RPC_ENDPOINT=http://127.0.0.0:8086/ -SB_INSTANCE_DNS_MOCK_COFFEE="coffee.com " +SB_INSTANCE_DNS_MOCK_COFFEE="coffee.com v=socialbox;sb-rpc=http://127.0.0.0:8086/;sb-key=sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc;sb-exp=0" SB_TEAPOT_DOMAIN=teapot.com SB_TEAPOT_RPC_ENDPOINT=http://127.0.0.0:8087/ -SB_INSTANCE_DNS_MOCK_TEAPOT="teapot.com " \ No newline at end of file +SB_INSTANCE_DNS_MOCK_TEAPOT="teapot.com v=socialbox;sb-rpc=http://127.0.0.0:8087/;sb-key=sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY;sb-exp=0" \ No newline at end of file diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 955d552..b8d5fa2 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -59,8 +59,8 @@ services: # Usage: SB_INSTANCE_DNS_MOCK_: # Environment Variable name is ignored, only the value is used with the prefix being used to detect # the instance name and the suffix being used to detect the TXT record - SB_INSTANCE_DNS_MOCK_COFFEE: ${SB_INSTANCE_DNS_MOCK_COFFEE:-"coffee.com "} - SB_INSTANCE_DNS_MOCK_TEAPOT: ${SB_INSTANCE_DNS_MOCK_TEAPOT:-"teapot.com "} + SB_INSTANCE_DNS_MOCK_COFFEE: ${SB_INSTANCE_DNS_MOCK_COFFEE:-"coffee.com v=socialbox;sb-rpc=http://127.0.0.0:8086/;sb-key=sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc;sb-exp=0"} + SB_INSTANCE_DNS_MOCK_TEAPOT: ${SB_INSTANCE_DNS_MOCK_TEAPOT:-"teapot.com v=socialbox;sb-rpc=http://127.0.0.0:8087/;sb-key=sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY;sb-exp=0"} healthcheck: test: ["CMD", "curl", "-f", "-H", "Request-Type: ping", "${SB_INSTANCE_RPC_ENDPOINT-http://127.0.0.0:8086/}"] interval: 30s @@ -162,8 +162,8 @@ services: # Usage: SB_INSTANCE_DNS_MOCK_: # Environment Variable name is ignored, only the value is used with the prefix being used to detect # the instance name and the suffix being used to detect the TXT record - SB_INSTANCE_DNS_MOCK_COFFEE: ${SB_INSTANCE_DNS_MOCK_COFFEE:-"coffee.com "} - SB_INSTANCE_DNS_MOCK_TEAPOT: ${SB_INSTANCE_DNS_MOCK_TEAPOT:-"teapot.com "} + SB_INSTANCE_DNS_MOCK_COFFEE: ${SB_INSTANCE_DNS_MOCK_COFFEE:-"coffee.com v=socialbox;sb-rpc=http://127.0.0.0:8086/;sb-key=sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc;sb-exp=0"} + SB_INSTANCE_DNS_MOCK_TEAPOT: ${SB_INSTANCE_DNS_MOCK_TEAPOT:-"teapot.com v=socialbox;sb-rpc=http://127.0.0.0:8087/;sb-key=sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY;sb-exp=0"} healthcheck: test: ["CMD", "curl", "-f", "-H", "Request-Type: ping", "${SB_INSTANCE_RPC_ENDPOINT-http://127.0.0.0:8087/}"] interval: 30s diff --git a/docker_test_keys/coffee.json b/docker_test_keys/coffee.json new file mode 100644 index 0000000..c0371f8 --- /dev/null +++ b/docker_test_keys/coffee.json @@ -0,0 +1,20 @@ +{ + "host_keypair_expires": 0, + "host_public_key": "sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc", + "host_private_key": "sig:tTVe59Ko5XuwgS8PneR92FAOqbgSHTKYn8U-lQRB9KODn0J_yPXCZCZGDUyS95hul2Jn7X7-EVT15FEmZADCZw", + "internal_encryption_keys": [ + "c2cpdTkYqIWI93cJPpAuCsoQJcHi9l37lYHA2TpUo9A", + "XUuWyWcKmtCUNVZ7Y0ZDbCE72klHZIniRihIIo78Vbs", + "SGg4GM_0-hO95Q6hBq2UjzGrp9mhVHyklNTHo-OZSNw", + "43WrkV6rDyc04S41E4uwJ1nQFhlll_CflsPW_hMOiqE", + "QDh9KecIdU-6be5ScPagL_WrWp8hQAersLQvLv9YtNQ", + "z4SnLU9Xw9F3yjPH_TmV4HuvZrpaVE0bqxzUGHyXQ-k", + "vg7lWOzkL_59u3o2RKcdrdwc7KVh07NrZRQzBPoJXEU", + "UW6X3XGGLj_e8xYd1bUwX9KYPTczHFtYTmy4FfiqfG0", + "sh-sRIQ3lWgkqR87wcTtZkDrgDKY2FOLuzdtpAvi9Wg", + "SDweTV1kNH0s5Ah1pwbfDo3ThAXAVKo9qJ4V9-hsHIs" + ], + "encryption_keys_count": 10, + "encryption_keys_algorithm": "xchacha20", + "transport_encryption_algorithm": "chacha20" +} \ No newline at end of file diff --git a/docker_test_keys/coffee.txt b/docker_test_keys/coffee.txt new file mode 100644 index 0000000..ae0e32a --- /dev/null +++ b/docker_test_keys/coffee.txt @@ -0,0 +1 @@ +v=socialbox;sb-rpc=http://127.0.0.0:8086/;sb-key=sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc;sb-exp=0 \ No newline at end of file diff --git a/docker_test_keys/teapot.json b/docker_test_keys/teapot.json new file mode 100644 index 0000000..8b69e65 --- /dev/null +++ b/docker_test_keys/teapot.json @@ -0,0 +1,20 @@ +{ + "host_keypair_expires": 0, + "host_public_key": "sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY", + "host_private_key": "sig:kPfGxpsnisJIp5pKuD1AI7-T1bLk1S-EGOr7jBq5AO4wNdS6uKkCj8gC_4RlMSgWGkh2GxfF8ws26dKdDPFiJg", + "internal_encryption_keys": [ + "ql5xP8OmKbSm_Fj4uz28aJ2Dpxdpg994Ker9Yp6eWVc", + "1mbn_MrV_XlPG7bqTrLXHd8H9bBUN27Rm-oIZmldOYQ", + "PL5YB2_C3I3gZAzrA2AP37tZKPsM5BQg0RH7LAgREGs", + "VEU4sF9v3AzDKEBLEJCbhirERouNdvMjGBoLLn0A9M4", + "5HeBwyDdUh8rbF5EKM9rQ0uU8PyzMIljoGKnn9CPk1U", + "YOGrfjs2dYnf7OL6FLhCMmfig-xCHNjGF7Gny7DisEQ", + "N-ouk7GBRqn1wLXtJ7BCcE5kPh4X0kgHuKzgOJ6i3Oo", + "bTo0YnNOVB4XtqiFpSx9kRaufDRDoYt6or-MRodngWQ", + "HTAvX3Nnf61-UOFQYcJOqfGcJVps_EOxo7KUf5Z0Zk0", + "UelTVnfWENEZEt6a7j5jxWq_pXWZ4X9pF8lOrdb6PKc" + ], + "encryption_keys_count": 10, + "encryption_keys_algorithm": "xchacha20", + "transport_encryption_algorithm": "chacha20" +} \ No newline at end of file diff --git a/docker_test_keys/teapot.txt b/docker_test_keys/teapot.txt new file mode 100644 index 0000000..c3ea413 --- /dev/null +++ b/docker_test_keys/teapot.txt @@ -0,0 +1 @@ +v=socialbox;sb-rpc=http://127.0.0.0:8087/;sb-key=sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY;sb-exp=0 \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh index 22a954b..6eaf44b 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -19,5 +19,4 @@ echo "Initializing Socialbox..." /usr/bin/socialbox init --log-level=${LOG_LEVEL-INFO} # Run supervisord, final command -echo "Starting supervisord..." /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf \ No newline at end of file From fc4b2b50f85c1af76aff5452958675de2f10a5b9 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 9 Jan 2025 15:07:59 -0500 Subject: [PATCH 145/420] Add configurations for Coffee and Teapot instances --- .gitignore | 6 +- coffee_socialbox/config/socialbox.conf | 100 +++++++++++++++++++++++++ teapot_socialbox/config/socialbox.conf | 100 +++++++++++++++++++++++++ 3 files changed, 205 insertions(+), 1 deletion(-) create mode 100755 coffee_socialbox/config/socialbox.conf create mode 100755 teapot_socialbox/config/socialbox.conf diff --git a/.gitignore b/.gitignore index a056964..e20b758 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,8 @@ .phpunit.result.cache /socialbox /bob_socialbox -/alice_socialbox \ No newline at end of file +/alice_socialbox +/coffee_socialbox/data/ +/coffee_socialbox/logs/ +/teapot_socialbox/data/ +/teapot_socialbox/logs/ diff --git a/coffee_socialbox/config/socialbox.conf b/coffee_socialbox/config/socialbox.conf new file mode 100755 index 0000000..5c10b3f --- /dev/null +++ b/coffee_socialbox/config/socialbox.conf @@ -0,0 +1,100 @@ +{ + "instance": { + "enabled": true, + "name": "coffee", + "domain": "coffee.com", + "rpc_endpoint": "http://127.0.0.0:8086/", + "dns_mocks": [] + }, + "security": { + "display_internal_exceptions": false, + "resolved_servers_ttl": 600, + "captcha_ttl": 200, + "otp_secret_key_length": 32, + "otp_time_step": 30, + "otp_digits": 6, + "otp_hash_algorithm": "sha512", + "otp_window": 1 + }, + "cryptography": { + "host_keypair_expires": 0, + "host_public_key": "sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc", + "host_private_key": "sig:tTVe59Ko5XuwgS8PneR92FAOqbgSHTKYn8U-lQRB9KODn0J_yPXCZCZGDUyS95hul2Jn7X7-EVT15FEmZADCZw", + "internal_encryption_keys": [ + "c2cpdTkYqIWI93cJPpAuCsoQJcHi9l37lYHA2TpUo9A", + "XUuWyWcKmtCUNVZ7Y0ZDbCE72klHZIniRihIIo78Vbs", + "SGg4GM_0-hO95Q6hBq2UjzGrp9mhVHyklNTHo-OZSNw", + "43WrkV6rDyc04S41E4uwJ1nQFhlll_CflsPW_hMOiqE", + "QDh9KecIdU-6be5ScPagL_WrWp8hQAersLQvLv9YtNQ", + "z4SnLU9Xw9F3yjPH_TmV4HuvZrpaVE0bqxzUGHyXQ-k", + "vg7lWOzkL_59u3o2RKcdrdwc7KVh07NrZRQzBPoJXEU", + "UW6X3XGGLj_e8xYd1bUwX9KYPTczHFtYTmy4FfiqfG0", + "sh-sRIQ3lWgkqR87wcTtZkDrgDKY2FOLuzdtpAvi9Wg", + "SDweTV1kNH0s5Ah1pwbfDo3ThAXAVKo9qJ4V9-hsHIs" + ], + "encryption_keys_count": 10, + "encryption_keys_algorithm": "xchacha20", + "transport_encryption_algorithm": "chacha20" + }, + "database": { + "host": "coffee_mariadb", + "port": 0, + "username": "socialbox", + "password": "socialbox", + "name": "socialbox" + }, + "logging": { + "console_logging_enabled": true, + "console_logging_level": "info", + "file_logging_enabled": true, + "file_logging_level": "debug" + }, + "cache": { + "enabled": true, + "engine": "redis", + "host": "coffee_redis", + "port": 6379, + "username": "root", + "password": "root", + "database": "0", + "sessions": { + "enabled": true, + "ttl": 3600, + "max": 1000 + } + }, + "registration": { + "enabled": true, + "privacy_policy_document": null, + "privacy_policy_date": 1734985525, + "accept_privacy_policy": true, + "terms_of_service_document": null, + "terms_of_service_date": 1734985525, + "accept_terms_of_service": true, + "community_guidelines_document": null, + "community_guidelines_date": 1734985525, + "accept_community_guidelines": true, + "password_required": true, + "otp_required": false, + "display_name_required": true, + "display_picture_required": false, + "email_address_required": false, + "phone_number_required": false, + "birthday_required": false, + "image_captcha_verification_required": true + }, + "authentication": { + "enabled": true, + "image_captcha_verification_required": true + }, + "policies": { + "max_signing_keys": 20, + "session_inactivity_expires": 43200, + "image_captcha_expires": 300 + }, + "storage": { + "path": "/etc/socialbox", + "user_display_images_path": "user_profiles", + "user_display_images_max_size": 3145728 + } +} \ No newline at end of file diff --git a/teapot_socialbox/config/socialbox.conf b/teapot_socialbox/config/socialbox.conf new file mode 100755 index 0000000..1195bb6 --- /dev/null +++ b/teapot_socialbox/config/socialbox.conf @@ -0,0 +1,100 @@ +{ + "instance": { + "enabled": true, + "name": "teapot", + "domain": "teapot.com", + "rpc_endpoint": "http://127.0.0.0:8087/", + "dns_mocks": [] + }, + "security": { + "display_internal_exceptions": false, + "resolved_servers_ttl": 600, + "captcha_ttl": 200, + "otp_secret_key_length": 32, + "otp_time_step": 30, + "otp_digits": 6, + "otp_hash_algorithm": "sha512", + "otp_window": 1 + }, + "cryptography": { + "host_keypair_expires": 0, + "host_public_key": "sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY", + "host_private_key": "sig:kPfGxpsnisJIp5pKuD1AI7-T1bLk1S-EGOr7jBq5AO4wNdS6uKkCj8gC_4RlMSgWGkh2GxfF8ws26dKdDPFiJg", + "internal_encryption_keys": [ + "ql5xP8OmKbSm_Fj4uz28aJ2Dpxdpg994Ker9Yp6eWVc", + "1mbn_MrV_XlPG7bqTrLXHd8H9bBUN27Rm-oIZmldOYQ", + "PL5YB2_C3I3gZAzrA2AP37tZKPsM5BQg0RH7LAgREGs", + "VEU4sF9v3AzDKEBLEJCbhirERouNdvMjGBoLLn0A9M4", + "5HeBwyDdUh8rbF5EKM9rQ0uU8PyzMIljoGKnn9CPk1U", + "YOGrfjs2dYnf7OL6FLhCMmfig-xCHNjGF7Gny7DisEQ", + "N-ouk7GBRqn1wLXtJ7BCcE5kPh4X0kgHuKzgOJ6i3Oo", + "bTo0YnNOVB4XtqiFpSx9kRaufDRDoYt6or-MRodngWQ", + "HTAvX3Nnf61-UOFQYcJOqfGcJVps_EOxo7KUf5Z0Zk0", + "UelTVnfWENEZEt6a7j5jxWq_pXWZ4X9pF8lOrdb6PKc" + ], + "encryption_keys_count": 10, + "encryption_keys_algorithm": "xchacha20", + "transport_encryption_algorithm": "chacha20" + }, + "database": { + "host": "teapot_mariadb", + "port": 0, + "username": "socialbox", + "password": "socialbox", + "name": "socialbox" + }, + "logging": { + "console_logging_enabled": true, + "console_logging_level": "info", + "file_logging_enabled": true, + "file_logging_level": "debug" + }, + "cache": { + "enabled": true, + "engine": "redis", + "host": "teapot_redis", + "port": 6379, + "username": "root", + "password": "root", + "database": "0", + "sessions": { + "enabled": true, + "ttl": 3600, + "max": 1000 + } + }, + "registration": { + "enabled": true, + "privacy_policy_document": null, + "privacy_policy_date": 1734985525, + "accept_privacy_policy": true, + "terms_of_service_document": null, + "terms_of_service_date": 1734985525, + "accept_terms_of_service": true, + "community_guidelines_document": null, + "community_guidelines_date": 1734985525, + "accept_community_guidelines": true, + "password_required": true, + "otp_required": false, + "display_name_required": true, + "display_picture_required": false, + "email_address_required": false, + "phone_number_required": false, + "birthday_required": false, + "image_captcha_verification_required": true + }, + "authentication": { + "enabled": true, + "image_captcha_verification_required": true + }, + "policies": { + "max_signing_keys": 20, + "session_inactivity_expires": 43200, + "image_captcha_expires": 300 + }, + "storage": { + "path": "/etc/socialbox", + "user_display_images_path": "user_profiles", + "user_display_images_max_size": 3145728 + } +} \ No newline at end of file From acc53a492f28ffb21de9ead51aad4796233b67cf Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 9 Jan 2025 15:10:51 -0500 Subject: [PATCH 146/420] Refactor mock server configuration logging and key. --- src/Socialbox/Classes/CliCommands/InitializeCommand.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Socialbox/Classes/CliCommands/InitializeCommand.php b/src/Socialbox/Classes/CliCommands/InitializeCommand.php index a9d57ef..226e083 100644 --- a/src/Socialbox/Classes/CliCommands/InitializeCommand.php +++ b/src/Socialbox/Classes/CliCommands/InitializeCommand.php @@ -382,6 +382,7 @@ try { $mockServers[$domain] = DnsHelper::parseTxt($txt); + Logger::getLogger()->info(sprintf('Added Mock Server %s = %s', $domain, $txt)); } catch(InvalidArgumentException $e) { @@ -393,7 +394,7 @@ if(count($mockServers) > 0) { Logger::getLogger()->info('Setting Mock Servers...'); - Configuration::getConfigurationLib()->set('instance.mock_servers', $mockServers); + Configuration::getConfigurationLib()->set('instance.dns_mocks', $mockServers); } // Apply changes & reload the configuration From 26c7cd650f54f59203272c4af732b5f11aa8989f Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 9 Jan 2025 15:47:21 -0500 Subject: [PATCH 147/420] Handle null values and validate inputs in RegisteredPeerRecord --- .../Objects/Database/RegisteredPeerRecord.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Socialbox/Objects/Database/RegisteredPeerRecord.php b/src/Socialbox/Objects/Database/RegisteredPeerRecord.php index a88f48e..f2c04f4 100644 --- a/src/Socialbox/Objects/Database/RegisteredPeerRecord.php +++ b/src/Socialbox/Objects/Database/RegisteredPeerRecord.php @@ -41,7 +41,11 @@ $this->emailAddress = $data['email_address'] ?? null; $this->phoneNumber = $data['phone_number'] ?? null; - if(is_int($data['birthday'])) + if(!isset($data['birthday'])) + { + $this->birthday = null; + } + elseif(is_int($data['birthday'])) { $this->birthday = (new DateTime())->setTimestamp($data['birthday']); } @@ -55,7 +59,7 @@ } else { - $this->birthday = null; + throw new \InvalidArgumentException("The birthday field must be a valid timestamp or date string."); } if($data['flags']) @@ -69,7 +73,11 @@ $this->enabled = $data['enabled']; - if(is_int($data['created'])) + if(!isset($data['created'])) + { + $this->created = new DateTime(); + } + elseif(is_int($data['created'])) { $this->created = (new DateTime())->setTimestamp($data['created']); } From 0e88689a50a6da847ea8734a87f5bc72a421eb81 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 10 Jan 2025 04:54:35 -0500 Subject: [PATCH 148/420] Removed test keys as it's no longer needed, the keys are found in the configuration files. --- docker_test_keys/coffee.json | 20 -------------------- docker_test_keys/coffee.txt | 1 - docker_test_keys/teapot.json | 20 -------------------- docker_test_keys/teapot.txt | 1 - 4 files changed, 42 deletions(-) delete mode 100644 docker_test_keys/coffee.json delete mode 100644 docker_test_keys/coffee.txt delete mode 100644 docker_test_keys/teapot.json delete mode 100644 docker_test_keys/teapot.txt diff --git a/docker_test_keys/coffee.json b/docker_test_keys/coffee.json deleted file mode 100644 index c0371f8..0000000 --- a/docker_test_keys/coffee.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "host_keypair_expires": 0, - "host_public_key": "sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc", - "host_private_key": "sig:tTVe59Ko5XuwgS8PneR92FAOqbgSHTKYn8U-lQRB9KODn0J_yPXCZCZGDUyS95hul2Jn7X7-EVT15FEmZADCZw", - "internal_encryption_keys": [ - "c2cpdTkYqIWI93cJPpAuCsoQJcHi9l37lYHA2TpUo9A", - "XUuWyWcKmtCUNVZ7Y0ZDbCE72klHZIniRihIIo78Vbs", - "SGg4GM_0-hO95Q6hBq2UjzGrp9mhVHyklNTHo-OZSNw", - "43WrkV6rDyc04S41E4uwJ1nQFhlll_CflsPW_hMOiqE", - "QDh9KecIdU-6be5ScPagL_WrWp8hQAersLQvLv9YtNQ", - "z4SnLU9Xw9F3yjPH_TmV4HuvZrpaVE0bqxzUGHyXQ-k", - "vg7lWOzkL_59u3o2RKcdrdwc7KVh07NrZRQzBPoJXEU", - "UW6X3XGGLj_e8xYd1bUwX9KYPTczHFtYTmy4FfiqfG0", - "sh-sRIQ3lWgkqR87wcTtZkDrgDKY2FOLuzdtpAvi9Wg", - "SDweTV1kNH0s5Ah1pwbfDo3ThAXAVKo9qJ4V9-hsHIs" - ], - "encryption_keys_count": 10, - "encryption_keys_algorithm": "xchacha20", - "transport_encryption_algorithm": "chacha20" -} \ No newline at end of file diff --git a/docker_test_keys/coffee.txt b/docker_test_keys/coffee.txt deleted file mode 100644 index ae0e32a..0000000 --- a/docker_test_keys/coffee.txt +++ /dev/null @@ -1 +0,0 @@ -v=socialbox;sb-rpc=http://127.0.0.0:8086/;sb-key=sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc;sb-exp=0 \ No newline at end of file diff --git a/docker_test_keys/teapot.json b/docker_test_keys/teapot.json deleted file mode 100644 index 8b69e65..0000000 --- a/docker_test_keys/teapot.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "host_keypair_expires": 0, - "host_public_key": "sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY", - "host_private_key": "sig:kPfGxpsnisJIp5pKuD1AI7-T1bLk1S-EGOr7jBq5AO4wNdS6uKkCj8gC_4RlMSgWGkh2GxfF8ws26dKdDPFiJg", - "internal_encryption_keys": [ - "ql5xP8OmKbSm_Fj4uz28aJ2Dpxdpg994Ker9Yp6eWVc", - "1mbn_MrV_XlPG7bqTrLXHd8H9bBUN27Rm-oIZmldOYQ", - "PL5YB2_C3I3gZAzrA2AP37tZKPsM5BQg0RH7LAgREGs", - "VEU4sF9v3AzDKEBLEJCbhirERouNdvMjGBoLLn0A9M4", - "5HeBwyDdUh8rbF5EKM9rQ0uU8PyzMIljoGKnn9CPk1U", - "YOGrfjs2dYnf7OL6FLhCMmfig-xCHNjGF7Gny7DisEQ", - "N-ouk7GBRqn1wLXtJ7BCcE5kPh4X0kgHuKzgOJ6i3Oo", - "bTo0YnNOVB4XtqiFpSx9kRaufDRDoYt6or-MRodngWQ", - "HTAvX3Nnf61-UOFQYcJOqfGcJVps_EOxo7KUf5Z0Zk0", - "UelTVnfWENEZEt6a7j5jxWq_pXWZ4X9pF8lOrdb6PKc" - ], - "encryption_keys_count": 10, - "encryption_keys_algorithm": "xchacha20", - "transport_encryption_algorithm": "chacha20" -} \ No newline at end of file diff --git a/docker_test_keys/teapot.txt b/docker_test_keys/teapot.txt deleted file mode 100644 index c3ea413..0000000 --- a/docker_test_keys/teapot.txt +++ /dev/null @@ -1 +0,0 @@ -v=socialbox;sb-rpc=http://127.0.0.0:8087/;sb-key=sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY;sb-exp=0 \ No newline at end of file From 46a006eecf79db4b1a61da2792b32a1f6744cba3 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 10 Jan 2025 04:54:48 -0500 Subject: [PATCH 149/420] Add DNS mocks configuration for coffee.com and teapot.com --- coffee_socialbox/config/socialbox.conf | 5 ++++- teapot_socialbox/config/socialbox.conf | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/coffee_socialbox/config/socialbox.conf b/coffee_socialbox/config/socialbox.conf index 5c10b3f..13b5588 100755 --- a/coffee_socialbox/config/socialbox.conf +++ b/coffee_socialbox/config/socialbox.conf @@ -4,7 +4,10 @@ "name": "coffee", "domain": "coffee.com", "rpc_endpoint": "http://127.0.0.0:8086/", - "dns_mocks": [] + "dns_mocks": { + "teapot.com": "v=socialbox;sb-rpc=http://127.0.0.0:8087/;sb-key=sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY;sb-exp=0", + "coffee.com": "v=socialbox;sb-rpc=http://127.0.0.0:8086/;sb-key=sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc;sb-exp=0" + } }, "security": { "display_internal_exceptions": false, diff --git a/teapot_socialbox/config/socialbox.conf b/teapot_socialbox/config/socialbox.conf index 1195bb6..07e0b5b 100755 --- a/teapot_socialbox/config/socialbox.conf +++ b/teapot_socialbox/config/socialbox.conf @@ -4,7 +4,10 @@ "name": "teapot", "domain": "teapot.com", "rpc_endpoint": "http://127.0.0.0:8087/", - "dns_mocks": [] + "dns_mocks": { + "teapot.com": "v=socialbox;sb-rpc=http://127.0.0.0:8087/;sb-key=sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY;sb-exp=0", + "coffee.com": "v=socialbox;sb-rpc=http://127.0.0.0:8086/;sb-key=sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc;sb-exp=0" + } }, "security": { "display_internal_exceptions": false, From a41313c22110177e2c1a860d794430ca5f3cd379 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 10 Jan 2025 13:30:18 -0500 Subject: [PATCH 150/420] Normalize header keys to lowercase for consistency. --- src/Socialbox/Objects/ClientRequest.php | 28 +++++++++++++++---------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/Socialbox/Objects/ClientRequest.php b/src/Socialbox/Objects/ClientRequest.php index 15e60cd..d822d35 100644 --- a/src/Socialbox/Objects/ClientRequest.php +++ b/src/Socialbox/Objects/ClientRequest.php @@ -34,15 +34,21 @@ */ public function __construct(array $headers, ?string $requestBody) { - $this->headers = $headers; + $parsedHeaders = []; + foreach($headers as $key => $value) + { + $parsedHeaders[strtolower($key)] = $value; + } + + $this->headers = $parsedHeaders; $this->requestBody = $requestBody; - $this->clientName = $headers[StandardHeaders::CLIENT_NAME->value] ?? null; - $this->clientVersion = $headers[StandardHeaders::CLIENT_VERSION->value] ?? null; - $this->requestType = RequestType::tryFrom($headers[StandardHeaders::REQUEST_TYPE->value]); - $this->identifyAs = $headers[StandardHeaders::IDENTIFY_AS->value] ?? null; - $this->sessionUuid = $headers[StandardHeaders::SESSION_UUID->value] ?? null; - $this->signature = $headers[StandardHeaders::SIGNATURE->value] ?? null; + $this->clientName = $parsedHeaders[strtolower(StandardHeaders::CLIENT_NAME->value)] ?? null; + $this->clientVersion = $parsedHeaders[strtolower(StandardHeaders::CLIENT_VERSION->value)] ?? null; + $this->requestType = RequestType::tryFrom($parsedHeaders[strtolower(StandardHeaders::REQUEST_TYPE->value)]); + $this->identifyAs = $parsedHeaders[strtolower(StandardHeaders::IDENTIFY_AS->value)] ?? null; + $this->sessionUuid = $parsedHeaders[strtolower(StandardHeaders::SESSION_UUID->value)] ?? null; + $this->signature = $parsedHeaders[strtolower(StandardHeaders::SIGNATURE->value)] ?? null; } /** @@ -65,10 +71,10 @@ { if(is_string($header)) { - return isset($this->headers[$header]); + return isset($this->headers[strtolower($header)]); } - return isset($this->headers[$header->value]); + return isset($this->headers[strtolower($header->value)]); } /** @@ -86,10 +92,10 @@ if(is_string($header)) { - return $this->headers[$header]; + return $this->headers[strtolower($header)]; } - return $this->headers[$header->value]; + return $this->headers[strtolower($header->value)]; } /** From ec7adec9bf7fe10661ce352605fb17b4ae34ce22 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 10 Jan 2025 13:30:35 -0500 Subject: [PATCH 151/420] Fix incorrect null parameter in generichash function, as null is no longer an acceptable value --- src/Socialbox/Classes/Cryptography.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Socialbox/Classes/Cryptography.php b/src/Socialbox/Classes/Cryptography.php index 4c60541..d4337a9 100644 --- a/src/Socialbox/Classes/Cryptography.php +++ b/src/Socialbox/Classes/Cryptography.php @@ -175,7 +175,7 @@ } $sharedSecret = sodium_crypto_scalarmult($decodedPrivateKey, $decodedPublicKey); - $derivedKey = sodium_crypto_generichash($sharedSecret, null, SODIUM_CRYPTO_SECRETBOX_KEYBYTES); + $derivedKey = sodium_crypto_generichash($sharedSecret, '', SODIUM_CRYPTO_SECRETBOX_KEYBYTES); $result = sodium_bin2base64($derivedKey, self::BASE64_VARIANT); // Clean up sensitive data From e9d51a19d34fe1677b6b151b33edfbbc90179cd7 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 10 Jan 2025 13:30:50 -0500 Subject: [PATCH 152/420] Update mock server initialization by simplifying TXT parsing --- src/Socialbox/Classes/CliCommands/InitializeCommand.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Socialbox/Classes/CliCommands/InitializeCommand.php b/src/Socialbox/Classes/CliCommands/InitializeCommand.php index 226e083..5cd7d31 100644 --- a/src/Socialbox/Classes/CliCommands/InitializeCommand.php +++ b/src/Socialbox/Classes/CliCommands/InitializeCommand.php @@ -381,8 +381,8 @@ try { - $mockServers[$domain] = DnsHelper::parseTxt($txt); - Logger::getLogger()->info(sprintf('Added Mock Server %s = %s', $domain, $txt)); + $mockServers[$domain] = $txt; + Logger::getLogger()->info(sprintf('Added Mock Server %s: %s', $domain, $txt)); } catch(InvalidArgumentException $e) { From 36a2d52041cbc1f6ca8e11bf34cd93ca2066ba05 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 10 Jan 2025 13:34:02 -0500 Subject: [PATCH 153/420] Ensure server keypair expiration checks handle invalid values --- src/Socialbox/Classes/RpcClient.php | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/Socialbox/Classes/RpcClient.php b/src/Socialbox/Classes/RpcClient.php index 99dc878..f43e5a6 100644 --- a/src/Socialbox/Classes/RpcClient.php +++ b/src/Socialbox/Classes/RpcClient.php @@ -51,7 +51,7 @@ if($exportedSession !== null) { // Check if the server keypair has expired from the exported session - if(time() > $exportedSession->getServerKeypairExpires()) + if($exportedSession->getServerKeypairExpires() > 0 && time() > $exportedSession->getServerKeypairExpires()) { throw new RpcException('The server keypair has expired, a new session must be created'); } @@ -71,7 +71,7 @@ $this->serverInformation = self::getServerInformation(); // Check if the active keypair has expired - if(time() > $this->serverInformation->getServerKeypairExpires()) + if($this->serverInformation->getServerKeypairExpires() > 0 && time() > $this->serverInformation->getServerKeypairExpires()) { throw new RpcException('The server keypair has expired but the server has not provided a new keypair, contact the server administrator'); } @@ -110,7 +110,7 @@ $this->serverInformation = self::getServerInformation(); // Check if the server keypair has expired - if(time() > $this->serverInformation->getServerKeypairExpires()) + if($this->serverInformation->getServerKeypairExpires() > 0 && time() > $this->serverInformation->getServerKeypairExpires()) { throw new RpcException('The server keypair has expired but the server has not provided a new keypair, contact the server administrator'); } @@ -500,14 +500,6 @@ { throw new RpcException(sprintf('Failed to get server information at %s, server responded with ' . $responseCode, $this->rpcEndpoint)); } - - throw new RpcException(sprintf('Failed to get server information at %s, server responded with ' . $responseCode . ': ' . $response, $this->rpcEndpoint)); - } - - if(empty($response)) - { - curl_close($ch); - throw new RpcException(sprintf('Failed to get server information at %s, server returned an empty response', $this->rpcEndpoint)); } curl_close($ch); From 2c836040573628f96010f8983a244e6fb15e8679 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 10 Jan 2025 13:34:18 -0500 Subject: [PATCH 154/420] Refactor mockRecord method to support string input. --- src/Socialbox/Classes/ServerResolver.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Socialbox/Classes/ServerResolver.php b/src/Socialbox/Classes/ServerResolver.php index c34138c..8d166ac 100644 --- a/src/Socialbox/Classes/ServerResolver.php +++ b/src/Socialbox/Classes/ServerResolver.php @@ -127,8 +127,13 @@ * @param DnsRecord $record The DNS record to be associated with the specified domain. * @return void */ - public static function mockRecord(string $domain, DnsRecord $record): void + public static function addMock(string $domain, DnsRecord|string $record): void { + if(is_string($record)) + { + $record = DnsHelper::parseTxt($record); + } + self::$mockedRecords[$domain] = $record; } From ec32978050ecf02783b6a37694317eefe7586778 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 10 Jan 2025 13:34:40 -0500 Subject: [PATCH 155/420] Handle missing session in DHE exchange requests --- src/Socialbox/Socialbox.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index 519b3ba..45064a7 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -286,8 +286,14 @@ return; } - // Check if the session is awaiting a DHE exchange, forbidden if not $session = $clientRequest->getSession(); + if($session === null) + { + self::returnError(404, StandardError::SESSION_NOT_FOUND, 'Session not found'); + return; + } + + // Check if the session is awaiting a DHE exchange, forbidden if not if($session->getState() !== SessionState::AWAITING_DHE) { self::returnError(403, StandardError::FORBIDDEN, 'Bad request: The session is not awaiting a DHE exchange'); From da3fe9c5a732f212d58551fdd0b4130cc3fe011a Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 10 Jan 2025 13:34:53 -0500 Subject: [PATCH 156/420] Add mock server resolvers for testing purposes --- tests/test.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test.php b/tests/test.php index 9963e5f..f3734d2 100644 --- a/tests/test.php +++ b/tests/test.php @@ -3,6 +3,9 @@ require 'ncc'; import('net.nosial.socialbox'); + \Socialbox\Classes\ServerResolver::addMock('coffee.com', 'v=socialbox;sb-rpc=http://127.0.0.0:8086/;sb-key=sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc;sb-exp=0'); + \Socialbox\Classes\ServerResolver::addMock('teapot.com', 'v=socialbox;sb-rpc=http://127.0.0.0:8087/;sb-key=sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY;sb-exp=0'); + $client = new \Socialbox\SocialClient(generateRandomPeer()); var_dump($client->getSessionState()); @@ -18,5 +21,5 @@ $randomString .= $characters[rand(0, $charactersLength - 1)]; } - return 'userTest' . $randomString . '@intvo.id'; + return 'userTest' . $randomString . '@coffee.com'; } \ No newline at end of file From fde3ccfc68b1821d5a34114bc0f898c11fdcc2f5 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 10 Jan 2025 14:58:24 -0500 Subject: [PATCH 157/420] Add external session management and support for remote servers --- .idea/sqldialects.xml | 1 + .../Resources/database/external_sessions.sql | 49 ++--- src/Socialbox/Classes/RpcClient.php | 33 ++-- .../Managers/ExternalSessionManager.php | 167 ++++++++++++++++++ src/Socialbox/Objects/ExportedSession.php | 13 ++ src/Socialbox/SocialClient.php | 9 +- src/Socialbox/Socialbox.php | 47 +++++ 7 files changed, 272 insertions(+), 47 deletions(-) create mode 100644 src/Socialbox/Managers/ExternalSessionManager.php diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml index 38a0168..85003a8 100644 --- a/.idea/sqldialects.xml +++ b/.idea/sqldialects.xml @@ -8,6 +8,7 @@ + diff --git a/src/Socialbox/Classes/Resources/database/external_sessions.sql b/src/Socialbox/Classes/Resources/database/external_sessions.sql index 96b198f..11e2035 100644 --- a/src/Socialbox/Classes/Resources/database/external_sessions.sql +++ b/src/Socialbox/Classes/Resources/database/external_sessions.sql @@ -1,35 +1,22 @@ create table external_sessions ( - uuid varchar(36) default uuid() not null comment 'The UUID of the session for the external connection' - primary key comment 'The Unique Primary Index for the session UUID', - peer_uuid varchar(36) not null comment 'The peer UUID that opened the connection', - session_uuid varchar(36) null comment 'The UUID of the parent session responsible for this external session', - server varchar(255) null comment 'The domain of the remote server that ths external session is authorized for', - created timestamp default current_timestamp() not null comment 'The Timestamp for when this record was created', - last_used timestamp default current_timestamp() not null comment 'The Timestamp for when this session was last used', - constraint external_sessions_uuid_uindex - unique (uuid) comment 'The Unique Primary Index for the session UUID', - constraint external_sessions_registered_peers_uuid_fk - foreign key (peer_uuid) references registered_peers (uuid) - on update cascade on delete cascade, - constraint external_sessions_sessions_uuid_fk - foreign key (session_uuid) references sessions (uuid) + domain varchar(256) not null comment 'The unique domain name that this session belongs to' + primary key comment 'The Unique Primary index for the external session', + rpc_endpoint text not null comment 'The RPC endpoint of the external connection', + session_uuid varchar(36) not null comment 'The UUID of the session to the external server', + transport_encryption_algorithm enum ('xchacha20', 'chacha20', 'aes256gcm') default 'xchacha20' not null comment 'The transport encryption algorithm used', + server_keypair_expires int not null comment 'The Timestamp for when the server keypair expires', + server_public_signing_key varchar(64) not null comment 'The public signing key of the server resolved from DNS records', + server_public_encryption_key varchar(64) not null comment 'The public encryption key of the server for this session', + host_public_encryption_key varchar(64) not null comment 'The public encryption key for the host', + host_private_encryption_key varchar(64) not null comment 'The private encryption key for host', + private_shared_secret varchar(64) not null comment 'The private shared secret obtained from the DHE procedure', + host_transport_encryption_key varchar(64) not null comment 'The transport encryption key for the host', + server_transport_encryption_key varchar(64) not null comment 'The transport encryption key for the server', + last_accessed timestamp default current_timestamp() not null comment 'The Timestamp for when the record was last accessed', + created timestamp default current_timestamp() not null comment 'The Timestamp for when this record was created', + constraint external_sessions_domain_uindex + unique (domain) comment 'The Unique Primary index for the external session' ) - comment 'Table for housing external sessions from local to remote servers'; - -create index external_sessions_created_index - on external_sessions (created) - comment 'The Index for the created column'; - -create index external_sessions_last_used_index - on external_sessions (last_used) - comment 'The inex for the last used column'; - -create index external_sessions_peer_uuid_index - on external_sessions (peer_uuid) - comment 'The Index for the peer UUID'; - -create index external_sessions_session_uuid_index - on external_sessions (session_uuid) - comment 'The index for the original session uuid'; + comment 'Table for housing external sessions to external servers'; diff --git a/src/Socialbox/Classes/RpcClient.php b/src/Socialbox/Classes/RpcClient.php index f43e5a6..ce57809 100644 --- a/src/Socialbox/Classes/RpcClient.php +++ b/src/Socialbox/Classes/RpcClient.php @@ -6,6 +6,7 @@ use Socialbox\Enums\StandardHeaders; use Socialbox\Enums\Types\RequestType; use Socialbox\Exceptions\CryptographyException; + use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\ResolutionException; use Socialbox\Exceptions\RpcException; use Socialbox\Objects\ExportedSession; @@ -21,7 +22,7 @@ private const string CLIENT_VERSION = '1.0'; private bool $bypassSignatureVerification; - private PeerAddress $peerAddress; + private PeerAddress $identifiedAs; private string $serverPublicSigningKey; private string $serverPublicEncryptionKey; private KeyPair $clientSigningKeyPair; @@ -31,18 +32,21 @@ private string $serverTransportEncryptionKey; private ServerInformation $serverInformation; private string $rpcEndpoint; + private string $remoteServer; private string $sessionUuid; /** * Constructs a new instance with the specified peer address. * - * @param string|PeerAddress $peerAddress The peer address to be used for the instance (eg; johndoe@example.com) + * @param string|PeerAddress $identifiedAs The peer address to be used for the instance (eg; johndoe@example.com) + * @param string|null $server Optional. The domain of the server to connect to if different from the identified * @param ExportedSession|null $exportedSession Optional. An exported session to be used to re-connect. * @throws CryptographyException If there is an error in the cryptographic operations. - * @throws RpcException If there is an error in the RPC request or if no response is received. * @throws ResolutionException If there is an error in the resolution process. + * @throws RpcException If there is an error in the RPC request or if no response is received. + * @throws DatabaseOperationException */ - public function __construct(string|PeerAddress $peerAddress, ?ExportedSession $exportedSession=null) + public function __construct(string|PeerAddress $identifiedAs, ?string $server=null, ?ExportedSession $exportedSession=null) { $this->bypassSignatureVerification = false; @@ -56,8 +60,9 @@ throw new RpcException('The server keypair has expired, a new session must be created'); } - $this->peerAddress = PeerAddress::fromAddress($exportedSession->getPeerAddress()); + $this->identifiedAs = PeerAddress::fromAddress($exportedSession->getPeerAddress()); $this->rpcEndpoint = $exportedSession->getRpcEndpoint(); + $this->remoteServer = $exportedSession->getRemoteServer(); $this->sessionUuid = $exportedSession->getSessionUuid(); $this->serverPublicSigningKey = $exportedSession->getServerPublicSigningKey(); $this->serverPublicEncryptionKey = $exportedSession->getServerPublicEncryptionKey(); @@ -86,16 +91,17 @@ } // If the peer address is a string, we need to convert it to a PeerAddress object - if(is_string($peerAddress)) + if(is_string($identifiedAs)) { - $peerAddress = PeerAddress::fromAddress($peerAddress); + $identifiedAs = PeerAddress::fromAddress($identifiedAs); } // Set the initial properties - $this->peerAddress = $peerAddress; + $this->identifiedAs = $identifiedAs; + $this->remoteServer = $server ?? $identifiedAs->getDomain(); // Resolve the domain and get the server's Public Key & RPC Endpoint - $resolvedServer = ServerResolver::resolveDomain($this->peerAddress->getDomain(), false); + $resolvedServer = ServerResolver::resolveDomain($this->remoteServer, false); // Import the RPC Endpoint & the server's public key. $this->serverPublicSigningKey = $resolvedServer->getPublicSigningKey(); @@ -117,7 +123,7 @@ // If the username is `host` and the domain is the same as this server's domain, we use our signing keypair // Essentially this is a special case for the server to contact another server - if($this->peerAddress->isHost()) + if($this->identifiedAs->isHost()) { $this->clientSigningKeyPair = new KeyPair(Configuration::getCryptographyConfiguration()->getHostPublicKey(), Configuration::getCryptographyConfiguration()->getHostPrivateKey()); } @@ -157,14 +163,14 @@ StandardHeaders::REQUEST_TYPE->value . ': ' . RequestType::INITIATE_SESSION->value, StandardHeaders::CLIENT_NAME->value . ': ' . self::CLIENT_NAME, StandardHeaders::CLIENT_VERSION->value . ': ' . self::CLIENT_VERSION, - StandardHeaders::IDENTIFY_AS->value . ': ' . $this->peerAddress->getAddress(), + StandardHeaders::IDENTIFY_AS->value . ': ' . $this->identifiedAs->getAddress(), // Always provide our generated encrypted public key StandardHeaders::ENCRYPTION_PUBLIC_KEY->value . ': ' . $this->clientEncryptionKeyPair->getPublicKey() ]; // If we're not connecting as the host, we need to provide our public key // Otherwise, the server will obtain the public key itself from DNS records rather than trusting the client - if(!$this->peerAddress->isHost()) + if(!$this->identifiedAs->isHost()) { $headers[] = StandardHeaders::SIGNING_PUBLIC_KEY->value . ': ' . $this->clientSigningKeyPair->getPublicKey(); } @@ -567,8 +573,9 @@ public function exportSession(): ExportedSession { return new ExportedSession([ - 'peer_address' => $this->peerAddress->getAddress(), + 'peer_address' => $this->identifiedAs->getAddress(), 'rpc_endpoint' => $this->rpcEndpoint, + 'remote_server' => $this->remoteServer, 'session_uuid' => $this->sessionUuid, 'transport_encryption_algorithm' => $this->serverInformation->getTransportEncryptionAlgorithm(), 'server_keypair_expires' => $this->serverInformation->getServerKeypairExpires(), diff --git a/src/Socialbox/Managers/ExternalSessionManager.php b/src/Socialbox/Managers/ExternalSessionManager.php new file mode 100644 index 0000000..2fc5f3d --- /dev/null +++ b/src/Socialbox/Managers/ExternalSessionManager.php @@ -0,0 +1,167 @@ +prepare("SELECT COUNT(*) FROM external_sessions WHERE domain=:domain LIMIT 1"); + $stmt->bindParam(':domain', $domain); + $stmt->execute(); + + return $stmt->fetchColumn() > 0; + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to check if a session exists in the database', $e); + } + } + + /** + * Adds a new external session to the database. + * + * @param ExportedSession $exportedSession The session data to be added, containing all necessary attributes + * such as server keys, client keys, and other metadata. + * @return void + * @throws DatabaseOperationException If the database operation fails. + */ + public static function addSession(ExportedSession $exportedSession): void + { + try + { + $stmt = Database::getConnection()->prepare("INSERT INTO external_sessions (domain, rpc_endpoint, session_uuid, server_keypair_expires, server_public_signing_key, server_public_encryption_key, host_public_encryption_key, host_private_encryption_key, private_shared_secret, host_transport_encryption_key, server_transport_encryption_key) VALUES (:domain, :rpc_endpoint, :session_uuid, :server_keypair_expires, :server_public_signing_key, :server_public_encryption_key, :host_public_encryption_key, :host_private_encryption_key, :private_shared_secret, :host_transport_encryption_key, :server_transport_encryption_key)"); + $domain = $exportedSession->getRemoteServer(); + $stmt->bindParam(':domain', $domain); + $rpcEndpoint = $exportedSession->getRpcEndpoint(); + $stmt->bindParam(':rpc_endpoint', $rpcEndpoint); + $sessionUuid = $exportedSession->getSessionUuid(); + $stmt->bindParam(':session_uuid', $sessionUuid); + $serverKeypairExpires = $exportedSession->getServerKeypairExpires(); + $stmt->bindParam(':server_keypair_expires', $serverKeypairExpires); + $serverPublicSigningKey = $exportedSession->getServerPublicSigningKey(); + $stmt->bindParam(':server_public_signing_key', $serverPublicSigningKey); + $serverPublicEncryptionKey = $exportedSession->getServerPublicEncryptionKey(); + $stmt->bindParam(':server_public_encryption_key', $serverPublicEncryptionKey); + $hostPublicEncryptionKey = $exportedSession->getClientPublicEncryptionKey(); + $stmt->bindParam(':host_public_encryption_key', $hostPublicEncryptionKey); + $hostPrivateEncryptionKey = $exportedSession->getClientPrivateEncryptionKey(); + $stmt->bindParam(':host_private_encryption_key', $hostPrivateEncryptionKey); + $privateSharedSecret = $exportedSession->getPrivateSharedSecret(); + $stmt->bindParam(':private_shared_secret', $privateSharedSecret); + $hostTransportEncryptionKey = $exportedSession->getClientTransportEncryptionKey(); + $stmt->bindParam(':host_transport_encryption_key', $hostTransportEncryptionKey); + $serverTransportEncryptionKey = $exportedSession->getServerTransportEncryptionKey(); + $stmt->bindParam(':server_transport_encryption_key', $serverTransportEncryptionKey); + + $stmt->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to add a session to the database', $e); + } + } + + /** + * Retrieves a session associated with the specified domain from the database. + * + * @param string $domain The domain for which the session should be retrieved. + * @return ExportedSession|null The retrieved session as an ExportedSession object, or null if no session is found. + * @throws DatabaseOperationException If the operation fails due to a database error. + */ + public static function getSession(string $domain): ?ExportedSession + { + try + { + $stmt = Database::getConnection()->prepare("SELECT * FROM external_sessions WHERE domain=:domain LIMIT 1"); + $stmt->bindParam(':domain', $domain); + $stmt->execute(); + $result = $stmt->fetch(); + + if($result === false) + { + return null; + } + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to retrieve the session from the database', $e); + } + + return ExportedSession::fromArray([ + 'peer_address' => sprintf('%s@%s', ReservedUsernames::HOST->value, Configuration::getInstanceConfiguration()->getDomain()), + 'rpc_endpoint' => $result['rpc_endpoint'], + 'remote_server' => $result['domain'], + 'session_uuid' => $result['session_uuid'], + 'transport_encryption_algorithm' => $result['transport_encryption_algorithm'], + 'server_keypair_expires' => $result['server_keypair_expires'], + 'server_public_encryption_key' => $result['server_public_encryption_key'], + 'client_public_signing_key' => Configuration::getCryptographyConfiguration()->getHostPublicKey(), + 'client_private_signing_key' => Configuration::getCryptographyConfiguration()->getHostPrivateKey(), + 'client_public_encryption_key' => $result['host_public_encryption_key'], + 'client_private_encryption_key' => $result['host_private_encryption_key'], + 'private_shared_secret' => $result['private_shared_secret'], + 'client_transport_encryption_key' => $result['host_transport_encryption_key'], + 'server_transport_encryption_key' => $result['server_transport_encryption_key'] + ]); + } + + /** + * Removes a session associated with the specified domain from the database. + * + * @param string $domain The domain for which the session should be removed. + * @return void + * @throws DatabaseOperationException If the operation fails due to a database error. + */ + public static function removeSession(string $domain): void + { + try + { + $stmt = Database::getConnection()->prepare("DELETE FROM external_sessions WHERE domain=:domain"); + $stmt->bindParam(':domain', $domain); + $stmt->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to remove a session from the database', $e); + } + } + + + /** + * Updates the last accessed timestamp for a specific external session in the database. + * + * @param string $domain The domain associated with the external session to update. + * @return void + * @throws DatabaseOperationException If the update operation fails. + */ + public static function updateLastAccessed(string $domain): void + { + try + { + $stmt = Database::getConnection()->prepare("UPDATE external_sessions SET last_accessed=CURRENT_TIMESTAMP WHERE domain=:domain"); + $stmt->bindParam(':domain', $domain); + $stmt->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to update the last accessed time of a session in the database', $e); + } + } + } \ No newline at end of file diff --git a/src/Socialbox/Objects/ExportedSession.php b/src/Socialbox/Objects/ExportedSession.php index 84be108..4c7752c 100644 --- a/src/Socialbox/Objects/ExportedSession.php +++ b/src/Socialbox/Objects/ExportedSession.php @@ -11,6 +11,7 @@ { private string $peerAddress; private string $rpcEndpoint; + private string $remoteServer; private string $sessionUUID; private string $transportEncryptionAlgorithm; private int $serverKeypairExpires; @@ -41,6 +42,7 @@ { $this->peerAddress = $data['peer_address']; $this->rpcEndpoint = $data['rpc_endpoint']; + $this->remoteServer = $data['remote_server']; $this->sessionUUID = $data['session_uuid']; $this->transportEncryptionAlgorithm = $data['transport_encryption_algorithm']; $this->serverKeypairExpires = $data['server_keypair_expires']; @@ -75,6 +77,16 @@ return $this->rpcEndpoint; } + /** + * Retrieves the remote server. + * + * @return string The remote server. + */ + public function getRemoteServer(): string + { + return $this->remoteServer; + } + /** * Retrieves the session UUID associated with the current instance. * @@ -203,6 +215,7 @@ return [ 'peer_address' => $this->peerAddress, 'rpc_endpoint' => $this->rpcEndpoint, + 'remote_server' => $this->remoteServer, 'session_uuid' => $this->sessionUUID, 'transport_encryption_algorithm' => $this->transportEncryptionAlgorithm, 'server_keypair_expires' => $this->serverKeypairExpires, diff --git a/src/Socialbox/SocialClient.php b/src/Socialbox/SocialClient.php index 9579abe..7407e95 100644 --- a/src/Socialbox/SocialClient.php +++ b/src/Socialbox/SocialClient.php @@ -9,6 +9,7 @@ use Socialbox\Classes\Utilities; use Socialbox\Enums\StandardMethods; use Socialbox\Exceptions\CryptographyException; + use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\ResolutionException; use Socialbox\Exceptions\RpcException; use Socialbox\Objects\ExportedSession; @@ -22,15 +23,17 @@ /** * Constructs the object from an array of data. * - * @param string|PeerAddress $peerAddress The address of the peer to connect to. + * @param string|PeerAddress $identifiedAs The address of the peer to connect to. + * @param string|null $server Optional. The domain of the server to connect to if different from the identified * @param ExportedSession|null $exportedSession Optional. The exported session to use for communication. * @throws CryptographyException If the public key is invalid. + * @throws DatabaseOperationException If the database operation fails. * @throws ResolutionException If the domain cannot be resolved. * @throws RpcException If the RPC request fails. */ - public function __construct(string|PeerAddress $peerAddress, ?ExportedSession $exportedSession=null) + public function __construct(string|PeerAddress $identifiedAs, ?string $server=null, ?ExportedSession $exportedSession=null) { - parent::__construct($peerAddress, $exportedSession); + parent::__construct($identifiedAs, $server, $exportedSession); } /** diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index 45064a7..995506c 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -8,6 +8,7 @@ use Socialbox\Classes\Cryptography; use Socialbox\Classes\DnsHelper; use Socialbox\Classes\Logger; + use Socialbox\Classes\RpcClient; use Socialbox\Classes\ServerResolver; use Socialbox\Classes\Utilities; use Socialbox\Classes\Validator; @@ -20,7 +21,10 @@ use Socialbox\Exceptions\CryptographyException; use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\RequestException; + use Socialbox\Exceptions\ResolutionException; + use Socialbox\Exceptions\RpcException; use Socialbox\Exceptions\StandardException; + use Socialbox\Managers\ExternalSessionManager; use Socialbox\Managers\RegisteredPeerManager; use Socialbox\Managers\SessionManager; use Socialbox\Objects\ClientRequest; @@ -598,6 +602,39 @@ } } + /** + * Retrieves an external session associated with the given domain. + * + * If a session already exists for the specified domain, it retrieves and uses the existing session. + * Otherwise, it establishes a new connection, creates a session, and stores it for later use. + * + * @param string $domain The domain for which the external session is to be retrieved. + * @return RpcClient The RPC client initialized with the external session for the given domain. + * @throws CryptographyException If there was an error in the cryptography + * @throws DatabaseOperationException If there was an error while processing the session against the database + * @throws RpcException If there is an RPC exception while connecting to the remote server + * @throws ResolutionException If the connection to the remote server fails. + */ + public static function getExternalSession(string $domain): RpcClient + { + if(ExternalSessionManager::sessionExists($domain)) + { + return new SocialClient(self::getServerAddress(), $domain, ExternalSessionManager::getSession($domain)); + } + + try + { + $client = new SocialClient(self::getServerAddress(), $domain); + } + catch (Exception $e) + { + throw new ResolutionException(sprintf('Failed to connect to remote server %s: %s', $domain, $e->getMessage()), $e->getCode(), $e); + } + + ExternalSessionManager::addSession($client->exportSession()); + return $client; + } + /** * Retrieves the server information by assembling data from the configuration settings. * @@ -613,6 +650,16 @@ ]); } + /** + * Retrieves the server address. + * + * @return PeerAddress The constructed server address containing the host and domain information. + */ + public static function getServerAddress(): PeerAddress + { + return new PeerAddress(ReservedUsernames::HOST->value, Configuration::getInstanceConfiguration()->getDomain()); + } + /** * Retrieves the DNS record by generating a TXT record using the RPC endpoint, * host public key, and host key pair expiration from the configuration. From 62c8d332a91c33f803a9e1f13eb7bc0f86dcd8de Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 10 Jan 2025 15:16:02 -0500 Subject: [PATCH 158/420] Add peer resolution through the `resolvePeer` method --- .../Classes/StandardMethods/ResolvePeer.php | 86 +++++++++++++++++++ src/Socialbox/Enums/StandardMethods.php | 12 ++- .../Managers/RegisteredPeerManager.php | 7 ++ .../Objects/Database/RegisteredPeerRecord.php | 2 +- src/Socialbox/SocialClient.php | 22 +++++ src/Socialbox/Socialbox.php | 6 +- 6 files changed, 129 insertions(+), 6 deletions(-) create mode 100644 src/Socialbox/Classes/StandardMethods/ResolvePeer.php diff --git a/src/Socialbox/Classes/StandardMethods/ResolvePeer.php b/src/Socialbox/Classes/StandardMethods/ResolvePeer.php new file mode 100644 index 0000000..73ce94f --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/ResolvePeer.php @@ -0,0 +1,86 @@ +containsParameter('peer')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'peer' parameter"); + } + + // Parse the peer address + try + { + $peerAddress = PeerAddress::fromAddress($rpcRequest->getParameter('peer')); + } + catch(InvalidArgumentException $e) + { + throw new StandardException('Peer Address Error: ' . $e->getMessage(), StandardError::RPC_INVALID_ARGUMENTS, $e); + } + + // If the requested peer resides in the server, resolve the peer internally. + if($peerAddress->getDomain() === Configuration::getInstanceConfiguration()->getDomain()) + { + try + { + $registeredPeer = RegisteredPeerManager::getPeerByAddress($peerAddress); + } + catch (DatabaseOperationException $e) + { + throw new StandardException('There was an unexpected error while trying to resolve the peer internally', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + // Return not found if the returned record is null or if the registered peer isn't enabled + if($registeredPeer === null || !$registeredPeer->isEnabled()) + { + return $rpcRequest->produceError(StandardError::PEER_NOT_FOUND, sprintf('Peer %s not found', $peerAddress->getAddress())); + } + + // Return standard peer representation + return $rpcRequest->produceResponse($registeredPeer->toStandardPeer()); + } + + // Otherwise, resolve the peer from the remote server + try + { + $client = Socialbox::getExternalSession($peerAddress->getDomain()); + } + catch(Exception $e) + { + throw new StandardException(sprintf('There was an error while trying to connect to %s: %s', $peerAddress->getDomain(), $e->getMessage()), StandardError::RESOLUTION_FAILED, $e); + } + + // Return the result/error of the resolution + try + { + return $rpcRequest->produceResponse($client->resolvePeer($peerAddress)); + } + catch(RpcException $e) + { + throw new StandardException($e->getMessage(), StandardError::tryFrom($e->getCode()) ?? StandardError::UNKNOWN, $e); + } + } + } \ No newline at end of file diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index 8cf92c8..f9ee7f5 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -12,6 +12,7 @@ use Socialbox\Classes\StandardMethods\GetSessionState; use Socialbox\Classes\StandardMethods\GetTermsOfService; use Socialbox\Classes\StandardMethods\Ping; + use Socialbox\Classes\StandardMethods\ResolvePeer; use Socialbox\Classes\StandardMethods\SettingsAddSigningKey; use Socialbox\Classes\StandardMethods\SettingsDeleteBirthday; use Socialbox\Classes\StandardMethods\SettingsDeleteDisplayName; @@ -94,6 +95,8 @@ case SETTINGS_ADD_SIGNING_KEY = 'settingsAddSigningKey'; case SETTINGS_GET_SIGNING_KEYS = 'settingsGetSigningKeys'; + case RESOLVE_PEER = 'resolvePeer'; + /** * Executes the appropriate operation based on the current context and requests provided. * @@ -140,6 +143,8 @@ self::SETTINGS_ADD_SIGNING_KEY => SettingsAddSigningKey::execute($request, $rpcRequest), self::SETTINGS_GET_SIGNING_KEYS => SettingsGetSigningKeys::execute($request, $rpcRequest), + self::RESOLVE_PEER => ResolvePeer::execute($request, $rpcRequest), + default => $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, sprintf("The method %s is not supported by the server", $rpcRequest->getMethod())) }; } @@ -227,7 +232,9 @@ **/ private static function getExternalMethods(ClientRequest $clientRequest): array { - return []; + return [ + self::RESOLVE_PEER + ]; } /** @@ -249,7 +256,8 @@ self::SETTINGS_SET_OTP, self::SETTINGS_SET_EMAIL, self::SETTINGS_SET_PHONE, - self::SETTINGS_SET_BIRTHDAY + self::SETTINGS_SET_BIRTHDAY, + self::RESOLVE_PEER ]; // Prevent the user from deleting their display name if it is required diff --git a/src/Socialbox/Managers/RegisteredPeerManager.php b/src/Socialbox/Managers/RegisteredPeerManager.php index 1838226..9310846 100644 --- a/src/Socialbox/Managers/RegisteredPeerManager.php +++ b/src/Socialbox/Managers/RegisteredPeerManager.php @@ -165,6 +165,13 @@ $username = $address->getUsername(); $statement->bindParam(1, $username); $server = $address->getDomain(); + + // Convert to 'host' if the domain is the same as the server's host + if($server === Configuration::getInstanceConfiguration()->getDomain()) + { + $server = 'host'; + } + $statement->bindParam(2, $server); $statement->execute(); diff --git a/src/Socialbox/Objects/Database/RegisteredPeerRecord.php b/src/Socialbox/Objects/Database/RegisteredPeerRecord.php index f2c04f4..f36cd32 100644 --- a/src/Socialbox/Objects/Database/RegisteredPeerRecord.php +++ b/src/Socialbox/Objects/Database/RegisteredPeerRecord.php @@ -261,7 +261,7 @@ * * @return Peer The Peer representation of the current instance. */ - public function toPeer(): Peer + public function toStandardPeer(): Peer { return Peer::fromArray($this->toArray()); } diff --git a/src/Socialbox/SocialClient.php b/src/Socialbox/SocialClient.php index 7407e95..57870ec 100644 --- a/src/Socialbox/SocialClient.php +++ b/src/Socialbox/SocialClient.php @@ -15,6 +15,7 @@ use Socialbox\Objects\ExportedSession; use Socialbox\Objects\PeerAddress; use Socialbox\Objects\RpcRequest; + use Socialbox\Objects\Standard\Peer; use Socialbox\Objects\Standard\ServerDocument; use Socialbox\Objects\Standard\SessionState; @@ -611,4 +612,25 @@ new RpcRequest(StandardMethods::SETTINGS_DELETE_BIRTHDAY->value, Utilities::randomCrc32()) )->getResponse()->getResult(); } + + /** + * Resolves a peer by its address or a PeerAddress instance through a remote procedure call. + * + * @param string|PeerAddress $peerAddress The peer address as a string or an instance of PeerAddress. + * @return Peer The resolved peer object. + * @throws RpcException Thrown if the RPC request fails. + */ + public function resolvePeer(string|PeerAddress $peerAddress): Peer + { + if($peerAddress instanceof PeerAddress) + { + $peerAddress = $peerAddress->getAddress(); + } + + return Peer::fromArray($this->sendRequest( + new RpcRequest(StandardMethods::RESOLVE_PEER->value, Utilities::randomCrc32(), [ + 'peer_address' => $peerAddress + ]) + )->getResponse()->getResult()); + } } \ No newline at end of file diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index 995506c..6c59135 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -609,13 +609,13 @@ * Otherwise, it establishes a new connection, creates a session, and stores it for later use. * * @param string $domain The domain for which the external session is to be retrieved. - * @return RpcClient The RPC client initialized with the external session for the given domain. + * @return SocialClient The RPC client initialized with the external session for the given domain. * @throws CryptographyException If there was an error in the cryptography * @throws DatabaseOperationException If there was an error while processing the session against the database - * @throws RpcException If there is an RPC exception while connecting to the remote server * @throws ResolutionException If the connection to the remote server fails. + * @throws RpcException If there is an RPC exception while connecting to the remote server */ - public static function getExternalSession(string $domain): RpcClient + public static function getExternalSession(string $domain): SocialClient { if(ExternalSessionManager::sessionExists($domain)) { From a4b17f91e0fdfde473de634db136315f8aa0f9bb Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 10 Jan 2025 15:17:12 -0500 Subject: [PATCH 159/420] Minor Correction --- src/Socialbox/Socialbox.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index 6c59135..7d52361 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -8,7 +8,6 @@ use Socialbox\Classes\Cryptography; use Socialbox\Classes\DnsHelper; use Socialbox\Classes\Logger; - use Socialbox\Classes\RpcClient; use Socialbox\Classes\ServerResolver; use Socialbox\Classes\Utilities; use Socialbox\Classes\Validator; From ffa3eac722329cd059bdf36c2609424f9d96ea8f Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 10 Jan 2025 15:24:38 -0500 Subject: [PATCH 160/420] Prevent duplicate mock DNS records in ServerResolver --- src/Socialbox/Classes/ServerResolver.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Socialbox/Classes/ServerResolver.php b/src/Socialbox/Classes/ServerResolver.php index 8d166ac..d2be2cf 100644 --- a/src/Socialbox/Classes/ServerResolver.php +++ b/src/Socialbox/Classes/ServerResolver.php @@ -129,6 +129,11 @@ */ public static function addMock(string $domain, DnsRecord|string $record): void { + if(isset(self::$mockedRecords[$domain])) + { + return; + } + if(is_string($record)) { $record = DnsHelper::parseTxt($record); From bed4e10fe28186dd0e9168940542569b7a68299d Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 10 Jan 2025 15:25:19 -0500 Subject: [PATCH 161/420] Disable mandatory user agreement acceptance and CAPTCHA. --- coffee_socialbox/config/socialbox.conf | 10 +++++----- teapot_socialbox/config/socialbox.conf | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/coffee_socialbox/config/socialbox.conf b/coffee_socialbox/config/socialbox.conf index 13b5588..63c3be4 100755 --- a/coffee_socialbox/config/socialbox.conf +++ b/coffee_socialbox/config/socialbox.conf @@ -70,13 +70,13 @@ "enabled": true, "privacy_policy_document": null, "privacy_policy_date": 1734985525, - "accept_privacy_policy": true, + "accept_privacy_policy": false, "terms_of_service_document": null, "terms_of_service_date": 1734985525, - "accept_terms_of_service": true, + "accept_terms_of_service": false, "community_guidelines_document": null, "community_guidelines_date": 1734985525, - "accept_community_guidelines": true, + "accept_community_guidelines": false, "password_required": true, "otp_required": false, "display_name_required": true, @@ -84,7 +84,7 @@ "email_address_required": false, "phone_number_required": false, "birthday_required": false, - "image_captcha_verification_required": true + "image_captcha_verification_required": false }, "authentication": { "enabled": true, @@ -100,4 +100,4 @@ "user_display_images_path": "user_profiles", "user_display_images_max_size": 3145728 } -} \ No newline at end of file +} diff --git a/teapot_socialbox/config/socialbox.conf b/teapot_socialbox/config/socialbox.conf index 07e0b5b..d3d3c9d 100755 --- a/teapot_socialbox/config/socialbox.conf +++ b/teapot_socialbox/config/socialbox.conf @@ -70,13 +70,13 @@ "enabled": true, "privacy_policy_document": null, "privacy_policy_date": 1734985525, - "accept_privacy_policy": true, + "accept_privacy_policy": false, "terms_of_service_document": null, "terms_of_service_date": 1734985525, - "accept_terms_of_service": true, + "accept_terms_of_service": false, "community_guidelines_document": null, "community_guidelines_date": 1734985525, - "accept_community_guidelines": true, + "accept_community_guidelines": false, "password_required": true, "otp_required": false, "display_name_required": true, @@ -84,7 +84,7 @@ "email_address_required": false, "phone_number_required": false, "birthday_required": false, - "image_captcha_verification_required": true + "image_captcha_verification_required": false }, "authentication": { "enabled": true, @@ -100,4 +100,4 @@ "user_display_images_path": "user_profiles", "user_display_images_max_size": 3145728 } -} \ No newline at end of file +} From 9d9a4b46b399904dfb1ec36b20efd439d0b706ee Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 10 Jan 2025 18:09:47 -0500 Subject: [PATCH 162/420] Add Authenticate method to handle external peer authentication --- .../Classes/StandardMethods/Authenticate.php | 45 +++++++++++++++++++ src/Socialbox/Enums/StandardMethods.php | 24 ++++++++-- src/Socialbox/Managers/SessionManager.php | 6 ++- 3 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 src/Socialbox/Classes/StandardMethods/Authenticate.php diff --git a/src/Socialbox/Classes/StandardMethods/Authenticate.php b/src/Socialbox/Classes/StandardMethods/Authenticate.php new file mode 100644 index 0000000..87a52e3 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/Authenticate.php @@ -0,0 +1,45 @@ +getPeer()->isExternal()) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'Only external peers can authenticate'); + } + + if($request->getSession()->isAuthenticated()) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'Peer is already authenticated'); + } + + SessionManager::removeFlags($request->getPeer()->getUuid(), [SessionFlags::AUTHENTICATION_REQUIRED]); + SessionManager::setAuthenticated($request->getPeer()->getUuid(), true); + } + catch(Exception $e) + { + throw new StandardException('An error occurred while authenticating the peer', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index f9ee7f5..c4e8cf9 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -6,6 +6,7 @@ use Socialbox\Classes\StandardMethods\AcceptCommunityGuidelines; use Socialbox\Classes\StandardMethods\AcceptPrivacyPolicy; use Socialbox\Classes\StandardMethods\AcceptTermsOfService; + use Socialbox\Classes\StandardMethods\Authenticate; use Socialbox\Classes\StandardMethods\GetAllowedMethods; use Socialbox\Classes\StandardMethods\GetCommunityGuidelines; use Socialbox\Classes\StandardMethods\GetPrivacyPolicy; @@ -95,6 +96,7 @@ case SETTINGS_ADD_SIGNING_KEY = 'settingsAddSigningKey'; case SETTINGS_GET_SIGNING_KEYS = 'settingsGetSigningKeys'; + case AUTHENTICATE = 'authenticate'; case RESOLVE_PEER = 'resolvePeer'; /** @@ -143,6 +145,7 @@ self::SETTINGS_ADD_SIGNING_KEY => SettingsAddSigningKey::execute($request, $rpcRequest), self::SETTINGS_GET_SIGNING_KEYS => SettingsGetSigningKeys::execute($request, $rpcRequest), + self::AUTHENTICATE => Authenticate::execute($request, $rpcRequest), self::RESOLVE_PEER => ResolvePeer::execute($request, $rpcRequest), default => $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, sprintf("The method %s is not supported by the server", $rpcRequest->getMethod())) @@ -229,12 +232,25 @@ } /** - **/ + * Retrieves a list of external methods based on the client's session state. + * + * @param ClientRequest + */ private static function getExternalMethods(ClientRequest $clientRequest): array { - return [ - self::RESOLVE_PEER - ]; + $methods = []; + + $session = $clientRequest->getSession(); + if(!$session->isAuthenticated() || $session->flagExists(SessionFlags::AUTHENTICATION_REQUIRED)) + { + $methods[] = self::AUTHENTICATE; + } + else + { + $methods[] = self::RESOLVE_PEER; + } + + return $methods; } /** diff --git a/src/Socialbox/Managers/SessionManager.php b/src/Socialbox/Managers/SessionManager.php index 0a86bef..4fd60dc 100644 --- a/src/Socialbox/Managers/SessionManager.php +++ b/src/Socialbox/Managers/SessionManager.php @@ -55,7 +55,11 @@ $flags = []; // TODO: Update this to support `host` peers - if($peer->isEnabled()) + if($peer->isExternal()) + { + $flags[] = SessionFlags::AUTHENTICATION_REQUIRED; + } + else if($peer->isEnabled()) { $flags[] = SessionFlags::AUTHENTICATION_REQUIRED; From 4d01947092c261ecad1071637081cbb6f99fd24a Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 10 Jan 2025 18:11:24 -0500 Subject: [PATCH 163/420] Add authentication method to SocialClient --- src/Socialbox/SocialClient.php | 15 +++++++++++++++ src/Socialbox/Socialbox.php | 1 + 2 files changed, 16 insertions(+) diff --git a/src/Socialbox/SocialClient.php b/src/Socialbox/SocialClient.php index 57870ec..c3a6a7e 100644 --- a/src/Socialbox/SocialClient.php +++ b/src/Socialbox/SocialClient.php @@ -613,6 +613,21 @@ )->getResponse()->getResult(); } + /** + * Authenticates the user by sending a remote procedure call request. + * Only applicable for server to server communication, this is the first method to call + * after connecting to the server. + * + * @return true Returns true if the authentication is successful. + * @throws RpcException Thrown if the RPC request fails. + */ + public function authenticate(): true + { + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::AUTHENTICATE->value, Utilities::randomCrc32()) + )->getResponse()->getResult(); + } + /** * Resolves a peer by its address or a PeerAddress instance through a remote procedure call. * diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index 7d52361..1fb3543 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -624,6 +624,7 @@ try { $client = new SocialClient(self::getServerAddress(), $domain); + $client->authenticate(); } catch (Exception $e) { From 9e9596989d1553372271f6ced43fcb7a0455baf0 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 10 Jan 2025 18:17:28 -0500 Subject: [PATCH 164/420] Refactor session flag handling and add VER_AUTHENTICATION. --- .../Classes/StandardMethods/Authenticate.php | 3 +- src/Socialbox/Enums/Flags/SessionFlags.php | 49 ++++++++----------- src/Socialbox/Managers/SessionManager.php | 1 + 3 files changed, 22 insertions(+), 31 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/Authenticate.php b/src/Socialbox/Classes/StandardMethods/Authenticate.php index 87a52e3..b78368b 100644 --- a/src/Socialbox/Classes/StandardMethods/Authenticate.php +++ b/src/Socialbox/Classes/StandardMethods/Authenticate.php @@ -31,8 +31,7 @@ return $rpcRequest->produceError(StandardError::FORBIDDEN, 'Peer is already authenticated'); } - SessionManager::removeFlags($request->getPeer()->getUuid(), [SessionFlags::AUTHENTICATION_REQUIRED]); - SessionManager::setAuthenticated($request->getPeer()->getUuid(), true); + SessionManager::updateFlow($request->getSession(), [SessionFlags::AUTHENTICATION_REQUIRED]); } catch(Exception $e) { diff --git a/src/Socialbox/Enums/Flags/SessionFlags.php b/src/Socialbox/Enums/Flags/SessionFlags.php index 0a678a6..01c37bf 100644 --- a/src/Socialbox/Enums/Flags/SessionFlags.php +++ b/src/Socialbox/Enums/Flags/SessionFlags.php @@ -27,6 +27,7 @@ case VER_IMAGE_CAPTCHA = 'VER_IMAGE_CAPTCHA'; // Peer has to solve an image captcha case VER_TEXT_CAPTCHA = 'VER_TEXT_CAPTCHA'; // Peer has to solve a text captcha case VER_EXTERNAL_URL = 'VER_EXTERNAL_URL'; // Peer has to visit an external URL + case VER_AUTHENTICATION = 'VER_AUTHENTICATION'; // External peer has to run authenticate() on their end // Login, require fields case VER_PASSWORD = 'VER_PASSWORD'; // Peer has to enter their password @@ -37,37 +38,43 @@ case RATE_LIMITED = 'RATE_LIMITED'; // Peer is temporarily rate limited /** - * Determines whether the current value corresponds to a registration method flag. + * Retrieves a list of registration-related flags. * - * @return bool True if the value is a registration method flag, otherwise false. + * @return array Array of registration flags applicable for the process. */ - public function isRegistrationFlag(): bool + public static function getRegistrationFlags(): array { - return in_array($this->value, [ + return [ self::SET_PASSWORD->value, self::SET_OTP->value, self::SET_DISPLAY_NAME->value, + self::SET_DISPLAY_PICTURE->value, + self::SET_PHONE->value, + self::SET_BIRTHDAY->value, + self::SET_EMAIL->value, self::VER_PRIVACY_POLICY->value, self::VER_TERMS_OF_SERVICE->value, + self::VER_COMMUNITY_GUIDELINES->value, self::VER_EMAIL->value, self::VER_SMS->value, self::VER_PHONE_CALL->value, self::VER_IMAGE_CAPTCHA->value - ]); + ]; } /** - * Determines whether the current value corresponds to an authentication method flag. + * Retrieves an array of authentication flags to be used for verifying user identity. * - * @return bool True if the value is an authentication method flag, otherwise false. + * @return array Returns an array containing the values of defined authentication flags. */ - public function isAuthenticationFlag(): bool + public static function getAuthenticationFlags(): array { - return in_array($this->value, [ + return [ self::VER_IMAGE_CAPTCHA->value, self::VER_PASSWORD->value, - self::VER_OTP->value - ]); + self::VER_OTP->value, + self::VER_AUTHENTICATION->value + ]; } /** @@ -105,33 +112,17 @@ */ public static function isComplete(array $flags): bool { - // todo: refactor this to use the isRegistrationFlag & isAuthenticationFlag methods $flags = array_map(function ($flag) {return is_string($flag) ? SessionFlags::from($flag) : $flag;}, $flags); $flags = array_map(fn(SessionFlags $flag) => $flag->value, $flags); if (in_array(SessionFlags::REGISTRATION_REQUIRED->value, $flags)) { - $flagsToComplete = [ - SessionFlags::SET_PASSWORD->value, - SessionFlags::SET_OTP->value, - SessionFlags::SET_DISPLAY_NAME->value, - SessionFlags::VER_PRIVACY_POLICY->value, - SessionFlags::VER_TERMS_OF_SERVICE->value, - SessionFlags::VER_EMAIL->value, - SessionFlags::VER_SMS->value, - SessionFlags::VER_PHONE_CALL->value, - SessionFlags::VER_IMAGE_CAPTCHA->value - ]; - return !array_intersect($flagsToComplete, $flags); // Check if the intersection is empty + return !array_intersect(self::getRegistrationFlags(), $flags); // Check if the intersection is empty } if (in_array(SessionFlags::AUTHENTICATION_REQUIRED->value, $flags)) { - $flagsToComplete = [ - SessionFlags::VER_PASSWORD->value, - SessionFlags::VER_OTP->value - ]; - return !array_intersect($flagsToComplete, $flags); // Check if the intersection is empty + return !array_intersect(self::getAuthenticationFlags(), $flags); // Check if the intersection is empty } diff --git a/src/Socialbox/Managers/SessionManager.php b/src/Socialbox/Managers/SessionManager.php index 4fd60dc..2c2e848 100644 --- a/src/Socialbox/Managers/SessionManager.php +++ b/src/Socialbox/Managers/SessionManager.php @@ -58,6 +58,7 @@ if($peer->isExternal()) { $flags[] = SessionFlags::AUTHENTICATION_REQUIRED; + $flags[] = SessionFlags::VER_AUTHENTICATION; } else if($peer->isEnabled()) { From 72a6aaec5bafdb0fa01a8b38b0ba218ad66699dc Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 10 Jan 2025 18:18:17 -0500 Subject: [PATCH 165/420] Update error messages in Authenticate.php for clarity --- src/Socialbox/Classes/StandardMethods/Authenticate.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/Authenticate.php b/src/Socialbox/Classes/StandardMethods/Authenticate.php index b78368b..0ede4b4 100644 --- a/src/Socialbox/Classes/StandardMethods/Authenticate.php +++ b/src/Socialbox/Classes/StandardMethods/Authenticate.php @@ -23,12 +23,12 @@ { if(!$request->getPeer()->isExternal()) { - return $rpcRequest->produceError(StandardError::FORBIDDEN, 'Only external peers can authenticate'); + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'Only external peers can authenticate using this method'); } if($request->getSession()->isAuthenticated()) { - return $rpcRequest->produceError(StandardError::FORBIDDEN, 'Peer is already authenticated'); + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'External host is already authenticated'); } SessionManager::updateFlow($request->getSession(), [SessionFlags::AUTHENTICATION_REQUIRED]); From 3662f668f045edd0db39e0c634496b04da998edf Mon Sep 17 00:00:00 2001 From: netkas Date: Sat, 11 Jan 2025 19:37:49 -0500 Subject: [PATCH 166/420] Add optional "identifiedAs" parameter for RPC requests --- src/Socialbox/Classes/RpcClient.php | 40 ++++++++++++++++++----------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/src/Socialbox/Classes/RpcClient.php b/src/Socialbox/Classes/RpcClient.php index ce57809..b8e21f8 100644 --- a/src/Socialbox/Classes/RpcClient.php +++ b/src/Socialbox/Classes/RpcClient.php @@ -329,10 +329,11 @@ * Sends an RPC request with the given JSON data. * * @param string $jsonData The JSON data to be sent in the request. + * @param string|null $identifiedAs Optional. The username to identify as, usually the requesting peer. Required for server-to-server communication. * @return RpcResult[] An array of RpcResult objects. * @throws RpcException If the request fails, the response is invalid, or the decryption/signature verification fails. */ - public function sendRawRequest(string $jsonData): array + public function sendRawRequest(string $jsonData, ?string $identifiedAs=null): array { try { @@ -353,28 +354,34 @@ } $ch = curl_init(); - $headers = []; + $returnHeaders = []; + $headers = [ + StandardHeaders::REQUEST_TYPE->value . ': ' . RequestType::RPC->value, + StandardHeaders::SESSION_UUID->value . ': ' . $this->sessionUuid, + StandardHeaders::SIGNATURE->value . ': ' . $signature + ]; + + if($identifiedAs) + { + $headers[] = StandardHeaders::IDENTIFY_AS->value . ': ' . $identifiedAs; + } curl_setopt($ch, CURLOPT_URL, $this->rpcEndpoint); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($curl, $header) use (&$headers) + curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($curl, $header) use (&$returnHeaders) { $len = strlen($header); $header = explode(':', $header, 2); - if (count($header) < 2) // ignore invalid headers + if (count($header) < 2) // ignore invalid returnHeaders { return $len; } - $headers[strtolower(trim($header[0]))][] = trim($header[1]); + $returnHeaders[strtolower(trim($header[0]))][] = trim($header[1]); return $len; }); - curl_setopt($ch, CURLOPT_HTTPHEADER, [ - StandardHeaders::REQUEST_TYPE->value . ': ' . RequestType::RPC->value, - StandardHeaders::SESSION_UUID->value . ': ' . $this->sessionUuid, - StandardHeaders::SIGNATURE->value . ': ' . $signature - ]); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_POSTFIELDS, $encryptedData); $response = curl_exec($ch); @@ -428,7 +435,7 @@ if (!$this->bypassSignatureVerification) { - $signature = $headers[strtolower(StandardHeaders::SIGNATURE->value)][0] ?? null; + $signature = $returnHeaders[strtolower(StandardHeaders::SIGNATURE->value)][0] ?? null; if ($signature === null) { throw new RpcException('The server did not provide a signature for the response'); @@ -516,12 +523,14 @@ * Sends an RPC request and retrieves the corresponding RPC response. * * @param RpcRequest $request The RPC request to be sent. + * @param bool $throwException Optional. Whether to throw an exception if the response contains an error. + * @param string|null $identifiedAs Optional. The username to identify as, usually the requesting peer. Required for server-to-server communication. * @return RpcResult The received RPC response. * @throws RpcException If no response is received from the request. */ - public function sendRequest(RpcRequest $request, bool $throwException=true): RpcResult + public function sendRequest(RpcRequest $request, bool $throwException=true, ?string $identifiedAs=null): RpcResult { - $response = $this->sendRawRequest(json_encode($request->toArray())); + $response = $this->sendRawRequest(json_encode($request->toArray()), $identifiedAs); if (count($response) === 0) { @@ -544,10 +553,11 @@ * and handles the response. * * @param RpcRequest[] $requests An array of RpcRequest objects to be sent to the server. + * @param string|null $identifiedAs Optional. The username to identify as, usually the requesting peer. Required for server-to-server communication. * @return RpcResult[] An array of RpcResult objects received from the server. * @throws RpcException If no response is received from the server. */ - public function sendRequests(array $requests): array + public function sendRequests(array $requests, ?string $identifiedAs=null): array { $parsedRequests = []; foreach ($requests as $request) @@ -555,7 +565,7 @@ $parsedRequests[] = $request->toArray(); } - $responses = $this->sendRawRequest(json_encode($parsedRequests)); + $responses = $this->sendRawRequest(json_encode($parsedRequests), $identifiedAs); if (count($responses) === 0) { From 24e21038e82d03e6ee69b7b28b2d9da4aa28bf6b Mon Sep 17 00:00:00 2001 From: netkas Date: Sat, 11 Jan 2025 19:38:09 -0500 Subject: [PATCH 167/420] Handle host peer identification and synchronization logic --- src/Socialbox/Socialbox.php | 68 ++++++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index 1fb3543..59cbaae 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -172,7 +172,7 @@ // If the peer is identifying as an external domain else { - // Only allow the host to identify as an external peer + // Only allow the host to identify as a host if($clientRequest->getIdentifyAs()->getUsername() !== ReservedUsernames::HOST->value) { self::returnError(403, StandardError::FORBIDDEN, 'Forbidden: Any external peer must identify as the host, only the host can preform actions on behalf of it\'s peers'); @@ -211,6 +211,25 @@ self::returnError(403, StandardError::FORBIDDEN, 'Unauthorized: The requested peer is disabled/banned'); return; } + // If-clause for handling the host peer, host peers are always enabled unless the fist clause is true + // in which case the host was blocked by this server. + elseif($clientRequest->getIdentifyAs() === ReservedUsernames::HOST->value) + { + $serverInformation = self::getExternalServerInformation($clientRequest->getIdentifyAs()->getDomain()); + + // If the host is not registered, register it + if($registeredPeer === null) + { + $peerUuid = RegisteredPeerManager::createPeer(PeerAddress::fromAddress($clientRequest->getHeader(StandardHeaders::IDENTIFY_AS))); + RegisteredPeerManager::updateDisplayName($peerUuid, $serverInformation->getServerName()); + RegisteredPeerManager::enablePeer($peerUuid); + } + // Otherwise, update the display name if it has changed + else + { + RegisteredPeerManager::updateDisplayName($registeredPeer->getUuid(), $serverInformation->getServerName()); + } + } // Otherwise the peer isn't registered, so we need to register it else { @@ -451,6 +470,34 @@ return; } + // If the client has provided an identification header, further validation and resolution is required + if($clientRequest->getIdentifyAs() !== null) + { + // First check if the client is identifying as the host + if($clientRequest->getPeer()->getAddress() !== ReservedUsernames::HOST->value) + { + // TODO: Maybe allow user client to change identification but within an RPC method rather than the headers + self::returnError(403, StandardError::FORBIDDEN, 'Unauthorized: Not allowed to identify as a different peer'); + return; + } + + // Synchronize the peer + try + { + $client = self::getExternalSession($clientRequest->getIdentifyAs()->getDomain()); + RegisteredPeerManager::synchronizeExternalPeer($client->resolvePeer($clientRequest->getIdentifyAs())); + } + catch (DatabaseOperationException $e) + { + self::returnError(500, StandardError::INTERNAL_SERVER_ERROR, 'Failed to synchronize external peer', $e); + return; + } + catch (Exception $e) + { + throw new ResolutionException(sprintf('Failed to synchronize external peer %s: %s', $clientRequest->getIdentifyAs()->getAddress(), $e->getMessage()), $e->getCode(), $e); + } + } + try { $clientRequests = $clientRequest->getRpcRequests($decryptedContent); @@ -635,6 +682,25 @@ return $client; } + /** + * Retrieves external server information for the specified domain. + * + * @param string $domain The domain from which the server information is to be retrieved. + * @return ServerInformation The server information retrieved from the external session. + * @throws ResolutionException If unable to retrieve server information for the given domain. + */ + public static function getExternalServerInformation(string $domain): ServerInformation + { + try + { + return self::getExternalSession($domain)->getServerInformation(); + } + catch (Exception $e) + { + throw new ResolutionException(sprintf('Failed to retrieve server information from %s: %s', $domain, $e->getMessage()), $e->getCode(), $e); + } + } + /** * Retrieves the server information by assembling data from the configuration settings. * From 107ca0b9cadf94e3b189f34d66d1bc029d8c58eb Mon Sep 17 00:00:00 2001 From: netkas Date: Sat, 11 Jan 2025 19:38:21 -0500 Subject: [PATCH 168/420] Add method to synchronize external peers in the database --- .../Managers/RegisteredPeerManager.php | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/src/Socialbox/Managers/RegisteredPeerManager.php b/src/Socialbox/Managers/RegisteredPeerManager.php index 9310846..5adebe6 100644 --- a/src/Socialbox/Managers/RegisteredPeerManager.php +++ b/src/Socialbox/Managers/RegisteredPeerManager.php @@ -12,9 +12,11 @@ use Socialbox\Classes\Logger; use Socialbox\Classes\Validator; use Socialbox\Enums\Flags\PeerFlags; + use Socialbox\Enums\ReservedUsernames; use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Objects\Database\RegisteredPeerRecord; use Socialbox\Objects\PeerAddress; + use Socialbox\Objects\Standard\Peer; use Symfony\Component\Uid\Uuid; class RegisteredPeerManager @@ -190,6 +192,75 @@ } } + /** + * Synchronizes the provided external peer by adding its details to the registered peers in the database. + * + * @param Peer $peer The peer object representing the external peer to be synchronized. + * @return void This method does not return any value. + * @throws InvalidArgumentException If the given peer is not an external peer or if it represents a host peer. + * @throws DatabaseOperationException If there is an error during the database operation to insert the peer's details. + */ + public static function synchronizeExternalPeer(Peer $peer): void + { + if($peer->getAddress()->getDomain() === Configuration::getInstanceConfiguration()->getDomain()) + { + throw new InvalidArgumentException('Given peer is not an external peer'); + } + + if($peer->getAddress()->getUsername() === ReservedUsernames::HOST->value) + { + throw new InvalidArgumentException('Cannot synchronize an external host peer'); + } + + $existingPeer = self::getPeerByAddress($peer->getAddress()); + if($existingPeer !== null) + { + // getUpdated is DateTime() if it's older than 1 hour, update it + if($existingPeer->getUpdated()->diff(new DateTime())->h < 1) + { + return; + } + + try + { + $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET display_name=?, updated=? WHERE uuid=?'); + $displayName = $peer->getDisplayName(); + $statement->bindParam(1, $displayName); + $updated = new DateTime(); + $statement->bindParam(2, $updated); + $uuid = $existingPeer->getUuid(); + $statement->bindParam(3, $uuid); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to update the external peer in the database', $e); + } + + return; + } + + $uuid = Uuid::v4()->toRfc4122(); + + try + { + $statement = Database::getConnection()->prepare('INSERT INTO `registered_peers` (uuid, username, server, display_name, enabled) VALUES (:uuid, :username, :server, :display_name, 1)'); + $statement->bindParam(':uuid', $uuid); + $username = $peer->getAddress()->getUsername(); + $statement->bindParam(':username', $username); + $server = $peer->getAddress()->getDomain(); + $statement->bindParam(':server', $server); + $displayName = $peer->getDisplayName(); + $statement->bindParam(':display_name', $displayName); + + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to synchronize the external peer in the database', $e); + } + } + /** * Enables a peer identified by the given UUID or RegisteredPeerRecord. * From 7eae62cfd8638635a0c64516fffdd3d6ec027232 Mon Sep 17 00:00:00 2001 From: netkas Date: Sat, 11 Jan 2025 19:39:09 -0500 Subject: [PATCH 169/420] Add `updated` field to RegisteredPeerRecord --- .../Objects/Database/RegisteredPeerRecord.php | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/Socialbox/Objects/Database/RegisteredPeerRecord.php b/src/Socialbox/Objects/Database/RegisteredPeerRecord.php index f36cd32..a3ff8bd 100644 --- a/src/Socialbox/Objects/Database/RegisteredPeerRecord.php +++ b/src/Socialbox/Objects/Database/RegisteredPeerRecord.php @@ -3,6 +3,7 @@ namespace Socialbox\Objects\Database; use DateTime; + use InvalidArgumentException; use Socialbox\Classes\Configuration; use Socialbox\Enums\Flags\PeerFlags; use Socialbox\Interfaces\SerializableInterface; @@ -25,6 +26,7 @@ private ?array $flags; private bool $enabled; private DateTime $created; + private DateTime $updated; /** * Constructor for initializing class properties from provided data. @@ -59,7 +61,7 @@ } else { - throw new \InvalidArgumentException("The birthday field must be a valid timestamp or date string."); + throw new InvalidArgumentException("The birthday field must be a valid timestamp or date string."); } if($data['flags']) @@ -87,7 +89,24 @@ } else { - throw new \InvalidArgumentException("The created field must be a valid timestamp or date string."); + throw new InvalidArgumentException("The created field must be a valid timestamp or date string."); + } + + if(!isset($data['updated'])) + { + $this->updated = new DateTime(); + } + elseif(is_int($data['updated'])) + { + $this->updated = (new DateTime())->setTimestamp($data['updated']); + } + elseif(is_string($data['updated'])) + { + $this->updated = new DateTime($data['updated']); + } + else + { + throw new InvalidArgumentException("The updated field must be a valid timestamp or date string."); } } @@ -235,6 +254,16 @@ return $this->created; } + /** + * Retrieves the last update date and time. + * + * @return DateTime The last update date and time. + */ + public function getUpdated(): DateTime + { + return $this->updated; + } + /** * Determines if the user is considered external by checking if the username is 'host' and the server * is not the same as the domain from the configuration. @@ -290,7 +319,8 @@ 'birthday' => $this->birthday?->getTimestamp(), 'flags' => PeerFlags::toString($this->flags), 'enabled' => $this->enabled, - 'created' => $this->created + 'created' => $this->created, + 'updated' => $this->updated ]; } } \ No newline at end of file From 33452a42ef9feb666caf90ebd6852d6b3c2a9050 Mon Sep 17 00:00:00 2001 From: netkas Date: Sat, 11 Jan 2025 19:39:17 -0500 Subject: [PATCH 170/420] Add SQL dialect for ContactManager.php --- .idea/sqldialects.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml index 85003a8..2e3f2ff 100644 --- a/.idea/sqldialects.xml +++ b/.idea/sqldialects.xml @@ -8,6 +8,7 @@ + From 0fbb824f1050a47dfd631aa1df0e3ae8519b1025 Mon Sep 17 00:00:00 2001 From: netkas Date: Sat, 11 Jan 2025 19:49:20 -0500 Subject: [PATCH 171/420] Add error handling and refactor peer synchronization logic --- src/Socialbox/Socialbox.php | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index 59cbaae..c00e18f 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -39,6 +39,9 @@ * missing or invalid request types. * * @return void + * @throws CryptographyException + * @throws DatabaseOperationException + * @throws ResolutionException */ public static function handleRequest(): void { @@ -279,6 +282,7 @@ * required to perform the DHE exchange. * * @return void + * @throws CryptographyException */ private static function handleDheExchange(ClientRequest $clientRequest): void { @@ -412,6 +416,9 @@ * @param ClientRequest $clientRequest The RPC client request containing headers, body, and session information. * * @return void + * @throws CryptographyException + * @throws DatabaseOperationException + * @throws ResolutionException */ private static function handleRpc(ClientRequest $clientRequest): void { @@ -484,8 +491,7 @@ // Synchronize the peer try { - $client = self::getExternalSession($clientRequest->getIdentifyAs()->getDomain()); - RegisteredPeerManager::synchronizeExternalPeer($client->resolvePeer($clientRequest->getIdentifyAs())); + self::synchronizeExternalPeer($clientRequest->getIdentifyAs()); } catch (DatabaseOperationException $e) { @@ -701,6 +707,27 @@ } } + /** + * Synchronizes an external peer by resolving and integrating its information into the system. + * + * @param PeerAddress|string $externalPeer The external peer to synchronize, provided as a PeerAddress instance or a string. + * @return void + * @throws CryptographyException If there is an error in the cryptography + * @throws DatabaseOperationException If there is an error while processing the peer against the database + * @throws ResolutionException If the synchronization process fails due to unresolved peer information or other errors. + * @throws RpcException If there is an RPC exception while connecting to the remote server + */ + public static function synchronizeExternalPeer(PeerAddress|string $externalPeer): void + { + if($externalPeer instanceof PeerAddress) + { + $externalPeer = $externalPeer->getAddress(); + } + + $client = self::getExternalSession($externalPeer->getDomain()); + RegisteredPeerManager::synchronizeExternalPeer($client->resolvePeer($externalPeer)); + } + /** * Retrieves the server information by assembling data from the configuration settings. * From 92c70559f449d29e3fd896532e8ec2e27b5a741f Mon Sep 17 00:00:00 2001 From: netkas Date: Sat, 11 Jan 2025 19:59:16 -0500 Subject: [PATCH 172/420] Fix session expiration logic in SessionRecord --- src/Socialbox/Objects/Database/SessionRecord.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Socialbox/Objects/Database/SessionRecord.php b/src/Socialbox/Objects/Database/SessionRecord.php index 515dd88..1315f62 100644 --- a/src/Socialbox/Objects/Database/SessionRecord.php +++ b/src/Socialbox/Objects/Database/SessionRecord.php @@ -168,7 +168,7 @@ public function getState(): SessionState { $expires = time() + Configuration::getPoliciesConfiguration()->getSessionInactivityExpires(); - if($this->lastRequest !== null && $this->lastRequest->getTimestamp() < $expires) + if($this->lastRequest !== null && $this->lastRequest->getTimestamp() > $expires) { return SessionState::EXPIRED; } From 9dd60330d2c8143cbe80fc0ddffa5e33899b6b34 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 14 Jan 2025 14:26:41 -0500 Subject: [PATCH 173/420] Add ContactManager for managing contact-related operations --- src/Socialbox/Managers/ContactManager.php | 279 ++++++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 src/Socialbox/Managers/ContactManager.php diff --git a/src/Socialbox/Managers/ContactManager.php b/src/Socialbox/Managers/ContactManager.php new file mode 100644 index 0000000..660e289 --- /dev/null +++ b/src/Socialbox/Managers/ContactManager.php @@ -0,0 +1,279 @@ +getAddress(); + } + + try + { + // Check if the contact is already in the database + $statement = Database::getConnection()->prepare('SELECT COUNT(*) FROM contacts WHERE peer_uuid=:peer AND contact_peer_address=:address'); + $statement->bindParam(':peer', $peerUuid); + $statement->bindParam(':address', $contactAddress); + $statement->execute(); + + return $statement->fetchColumn() > 0; + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to check if a contact exists in the database', $e); + } + } + + /** + * Creates a new contact associated with the given peer UUID and contact address + * in the database, with a specified relationship type. + * + * @param string $peerUuid The unique identifier of the peer. + * @param string|PeerAddress $contactAddress The contact's address, either as a string or a PeerAddress instance. + * @param ContactRelationshipType $relationship The type of relationship between the peer and the contact. Defaults to ContactRelationshipType::MUTUAL. + * @return string The UUID of the newly created contact. + * @throws DatabaseOperationException If the operation fails. + */ + public static function createContact(string $peerUuid, string|PeerAddress $contactAddress, ContactRelationshipType $relationship=ContactRelationshipType::MUTUAL): string + { + if($contactAddress instanceof PeerAddress) + { + $contactAddress = $contactAddress->getAddress(); + } + + $uuid = UuidV4::v4()->toRfc4122(); + + try + { + // Insert the contact into the database + $statement = Database::getConnection()->prepare('INSERT INTO contacts (uuid, peer_uuid, contact_peer_address, relationship) VALUES (:uuid, :peer, :address, :relationship)'); + $statement->bindParam(':uuid', $uuid); + $statement->bindParam(':peer', $peerUuid); + $statement->bindParam(':address', $contactAddress); + $relationship = $relationship->value; + $statement->bindParam(':relationship', $relationship); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to create a new contact in the database', $e); + } + + return $uuid; + } + + /** + * Retrieves the total number of contacts associated with a specific peer. + * + * @param string $peerUuid The unique identifier for the peer whose contact count is to be retrieved. + * @return int The total number of contacts for the given peer. + * @throws DatabaseOperationException If the database query fails. + */ + public static function getContactCount(string $peerUuid): int + { + try + { + // Get the contact count from the database + $statement = Database::getConnection()->prepare('SELECT COUNT(*) FROM contacts WHERE peer_uuid=:peer'); + $statement->bindParam(':peer', $peerUuid); + $statement->execute(); + return $statement->fetchColumn(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to get the contact count from the database', $e); + } + } + + /** + * Retrieves a specific contact associated with a peer based on the contact's address. + * + * @param string $peerUuid The unique identifier for the peer whose contact is to be retrieved. + * @param string|PeerAddress $contactAddress The address of the contact, either as a string or a PeerAddress object. + * @return ContactRecord|null The retrieved ContactRecord instance if found, or null if no matching contact exists. + * @throws DatabaseOperationException If the database query fails. + */ + public static function getContact(string $peerUuid, string|PeerAddress $contactAddress): ?ContactRecord + { + if($contactAddress instanceof PeerAddress) + { + $contactAddress = $contactAddress->getAddress(); + } + + try + { + // Get the contact from the database + $statement = Database::getConnection()->prepare('SELECT * FROM contacts WHERE peer_uuid=:peer AND contact_peer_address=:address LIMIT 1'); + $statement->bindParam(':peer', $peerUuid); + $statement->bindParam(':address', $contactAddress); + $statement->execute(); + $result = $statement->fetch(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to get a contact from the database', $e); + } + + if($result === false) + { + return null; + } + + return ContactRecord::fromArray($result); + } + + /** + * Deletes a specific contact associated with a given peer. + * + * @param string $peerUuid The unique identifier for the peer whose contact is to be deleted. + * @param string|PeerAddress $contactAddress The address of the contact to be deleted. Can be provided as a string or a PeerAddress instance. + * @return void + * @throws DatabaseOperationException If the database query fails. + */ + public static function deleteContact(string $peerUuid, string|PeerAddress $contactAddress): void + { + if($contactAddress instanceof PeerAddress) + { + $contactAddress = $contactAddress->getAddress(); + } + + try + { + $statement = Database::getConnection()->prepare('DELETE FROM contacts WHERE peer_uuid=:peer AND contact_peer_address=:address'); + $statement->bindParam(':peer', $peerUuid); + $statement->bindParam(':address', $contactAddress); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to delete a contact from the database', $e); + } + } + + /** + * Updates the relationship type of a contact associated with a specific peer. + * + * @param string $peerUuid The unique identifier for the peer whose contact relationship is to be updated. + * @param string|PeerAddress $contactAddress The address of the contact to update. Can be provided as a string or an instance of PeerAddress. + * @param ContactRelationshipType $relationship The new relationship type to assign to the contact. + * @return void + * @throws DatabaseOperationException If the database query fails. + */ + public static function updateContactRelationship(string $peerUuid, string|PeerAddress $contactAddress, ContactRelationshipType $relationship): void + { + if($contactAddress instanceof PeerAddress) + { + $contactAddress = $contactAddress->getAddress(); + } + + try + { + $statement = Database::getConnection()->prepare('UPDATE contacts SET relationship=:relationship WHERE peer_uuid=:peer AND contact_peer_address=:address'); + $relationship = $relationship->value; + $statement->bindParam(':relationship', $relationship); + $statement->bindParam(':peer', $peerUuid); + $statement->bindParam(':address', $contactAddress); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to update the relationship for a contact in the database', $e); + } + } + + /** + * Retrieves a contact by its unique identifier. + * + * @param string $uuid The unique identifier of the contact to retrieve. + * @return ContactRecord|null A ContactRecord instance if the contact is found, or null if no contact exists with the provided UUID. + * @throws DatabaseOperationException If the database query fails. + */ + public static function getContactByUuid(string $uuid): ?ContactRecord + { + try + { + // Get the contact from the database + $statement = Database::getConnection()->prepare('SELECT * FROM contacts WHERE uuid=:uuid LIMIT 1'); + $statement->bindParam(':uuid', $uuid); + $statement->execute(); + $result = $statement->fetch(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to get a contact from the database', $e); + } + + if($result === false) + { + return null; + } + + return ContactRecord::fromArray($result); + } + + /** + * Retrieves a list of contacts associated with a specific peer. + * + * @param string $peerUuid The unique identifier for the peer whose contacts are to be retrieved. + * @param int $limit The maximum number of contacts to retrieve per page. Defaults to 100. + * @param int $page The page number to retrieve. Defaults to 1. + * @return array An array of ContactRecord instances representing the contacts for the given peer. + * @throws DatabaseOperationException If the database query fails. + */ + public static function getContacts(string $peerUuid, int $limit = 100, int $page = 1): array + { + if ($page < 1) + { + $page = 1; + } + + if ($limit < 1) + { + $limit = 1; + } + + $contacts = []; + try + { + $statement = Database::getConnection()->prepare("SELECT * FROM contacts WHERE peer_uuid = :peer ORDER BY created DESC LIMIT :limit OFFSET :offset"); + $offset = ($page - 1) * $limit; + $statement->bindParam(':peer', $peerUuid); + $statement->bindParam(':limit', $limit, PDO::PARAM_INT); + $statement->bindParam(':offset', $offset, PDO::PARAM_INT); + $statement->execute(); + + // Fetch results + $results = $statement->fetchAll(PDO::FETCH_ASSOC); + + // Convert results to ContactRecord instances + foreach ($results as $result) + { + $contacts[] = ContactRecord::fromArray($result); + } + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to get contacts from the database', $e); + } + return $contacts; + } + } \ No newline at end of file From b46423db145ea55255b95351e4957562ee1aae76 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 14 Jan 2025 14:51:37 -0500 Subject: [PATCH 174/420] Add ContactRecord class for handling contact data --- .../Objects/Database/ContactRecord.php | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 src/Socialbox/Objects/Database/ContactRecord.php diff --git a/src/Socialbox/Objects/Database/ContactRecord.php b/src/Socialbox/Objects/Database/ContactRecord.php new file mode 100644 index 0000000..3900920 --- /dev/null +++ b/src/Socialbox/Objects/Database/ContactRecord.php @@ -0,0 +1,138 @@ +uuid = $data['uuid']; + $this->peerUuid = $data['peer_uuid']; + $this->contactPeerAddress = $data['contact_peer_address']; + + if(is_string($data['relationship'])) + { + $this->relationship = ContactRelationshipType::from($data['relationship']); + } + elseif($data['relationship'] instanceof ContactRelationshipType) + { + $this->relationship = $data['relationship']; + } + else + { + throw new InvalidArgumentException('Invalid relationship type'); + } + + if(is_int($data['created'])) + { + $this->created = (new DateTime())->setTimestamp($data['created']); + } + elseif(is_string($data['created'])) + { + $this->created = new DateTime($data['created']); + } + elseif($data['created'] instanceof DateTime) + { + $this->created = $data['created']; + } + else + { + throw new InvalidArgumentException('Invalid created date'); + } + } + + /** + * + * @return string Returns the UUID as a string. + */ + public function getUuid(): string + { + return $this->uuid; + } + + /** + * Retrieves the UUID of the peer. + * + * @return string The UUID of the peer. + */ + public function getPeerUuid(): string + { + return $this->peerUuid; + } + + /** + * Retrieves the contact peer address. + * + * @return string The contact peer address. + */ + public function getContactPeerAddress(): string + { + return $this->contactPeerAddress; + } + + /** + * Retrieves the relationship type of the contact. + * + * @return ContactRelationshipType The relationship type of the contact. + */ + public function getRelationship(): ContactRelationshipType + { + return $this->relationship; + } + + /** + * Retrieves the created date and time. + * + * @return DateTime The DateTime object representing when the entity was created. + */ + public function getCreated(): DateTime + { + return $this->created; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): ContactRecord + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'uuid' => $this->uuid, + 'peer_uuid' => $this->peerUuid, + 'contact_peer_address' => $this->contactPeerAddress, + 'relationship' => $this->relationship->value, + 'created' => $this->created->format('Y-m-d H:i:s') + ]; + } + } \ No newline at end of file From 14ad8588b0eacaab8e652899ed5af7e178ca52b3 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 14 Jan 2025 14:52:40 -0500 Subject: [PATCH 175/420] Add ContactRelationshipType enum --- src/Socialbox/Enums/Types/ContactRelationshipType.php | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/Socialbox/Enums/Types/ContactRelationshipType.php diff --git a/src/Socialbox/Enums/Types/ContactRelationshipType.php b/src/Socialbox/Enums/Types/ContactRelationshipType.php new file mode 100644 index 0000000..9585bd4 --- /dev/null +++ b/src/Socialbox/Enums/Types/ContactRelationshipType.php @@ -0,0 +1,10 @@ + Date: Tue, 14 Jan 2025 14:52:59 -0500 Subject: [PATCH 176/420] Refactor file structure for better Docker organization --- Dockerfile | 22 ++++++++++++++++----- docker-compose.test.yml | 4 ++-- docker-compose.yml | 2 +- docker/docker.conf | 2 ++ entrypoint.sh => docker/entrypoint.sh | 0 nginx.conf => docker/nginx.conf | 0 redis.conf => docker/redis.conf | 0 supervisord.conf => docker/supervisord.conf | 0 docker/zz-docker.conf | 10 ++++++++++ 9 files changed, 32 insertions(+), 8 deletions(-) create mode 100644 docker/docker.conf rename entrypoint.sh => docker/entrypoint.sh (100%) rename nginx.conf => docker/nginx.conf (100%) rename redis.conf => docker/redis.conf (100%) rename supervisord.conf => docker/supervisord.conf (100%) create mode 100644 docker/zz-docker.conf diff --git a/Dockerfile b/Dockerfile index 22628b7..774fb36 100644 --- a/Dockerfile +++ b/Dockerfile @@ -83,11 +83,12 @@ RUN ncc build --config release --build-source --log-level debug && \ RUN rm -rf /tmp/build && rm -rf /var/www/html/* # Copy over the required files -COPY nginx.conf /etc/nginx/nginx.conf +COPY docker/nginx.conf /etc/nginx/nginx.conf COPY public/index.php /var/www/html/index.php RUN chown -R www-data:www-data /var/www/html && chmod -R 755 /var/www/html -# ----------------------------- Cron Configuration --------------------------- +# ----------------------------- Environment Configuration --------------------------- +# Configure Cron RUN echo "*/1 * * * * root for i in {1..12}; do /usr/bin/socialbox process-outgoing; sleep 5; done" > /etc/cron.d/socialbox-process-outgoing && \ echo "*/1 * * * * root /usr/bin/socialbox session-cleanup" > /etc/cron.d/socialbox-session-cleanup && \ echo "*/5 * * * * root /usr/bin/socialbox peer-cleanup" > /etc/cron.d/socialbox-peer-cleanup && \ @@ -100,9 +101,20 @@ RUN echo "*/1 * * * * root for i in {1..12}; do /usr/bin/socialbox process-outgo crontab /etc/cron.d/socialbox-session-cleanup && \ crontab /etc/cron.d/socialbox-peer-cleanup -# ----------------------------- Supervisor Configuration --------------------- # Copy Supervisor configuration -COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf +COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf +# Copy docker.conf & zz-docker.conf for PHP-FPM +COPY docker/docker.conf /usr/local/etc/php-fpm.d/docker.conf +COPY docker/zz-docker.conf /usr/local/etc/php-fpm.d/zz-docker.conf + +# Configure php.ini and enable error and log it to /var/log rather than stdout +RUN cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini && \ + sed -i 's/^;error_log = php_errors.log/error_log = \/var\/log\/php_errors.log/' /usr/local/etc/php/php.ini && \ + sed -i 's/^;log_errors = On/log_errors = On/' /usr/local/etc/php/php.ini && \ + sed -i 's/^;error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT/error_reporting = E_ALL/' /usr/local/etc/php/php.ini && \ + sed -i 's/^;display_errors = Off/display_errors = On/' /usr/local/etc/php/php.ini && \ + sed -i 's/^;date.timezone =/date.timezone = UTC/' /usr/local/etc/php/php.ini + # ----------------------------- Cleanup --------------------- WORKDIR / @@ -112,7 +124,7 @@ EXPOSE 8085 # ----------------------------- Container Startup ---------------------------- # Copy over entrypoint script and set it as executable -COPY entrypoint.sh /usr/local/bin/entrypoint.sh +COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/entrypoint.sh # Set the entrypoint diff --git a/docker-compose.test.yml b/docker-compose.test.yml index b8d5fa2..47c225b 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -95,7 +95,7 @@ services: command: redis-server /usr/local/etc/redis/redis.conf --appendonly yes volumes: - coffee_redis_data:/data - - ./redis.conf:/usr/local/etc/redis/redis.conf + - ./docker/redis.conf:/usr/local/etc/redis/redis.conf networks: - coffee_network expose: @@ -198,7 +198,7 @@ services: command: redis-server /usr/local/etc/redis/redis.conf --appendonly yes volumes: - teapot_redis_data:/data - - ./redis.conf:/usr/local/etc/redis/redis.conf + - ./docker/redis.conf:/usr/local/etc/redis/redis.conf networks: - teapot_network expose: diff --git a/docker-compose.yml b/docker-compose.yml index d4d0f75..e409af1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -85,7 +85,7 @@ services: command: redis-server /usr/local/etc/redis/redis.conf --appendonly yes volumes: - redis_data:/data - - ./redis.conf:/usr/local/etc/redis/redis.conf + - ./docker/redis.conf:/usr/local/etc/redis/redis.conf networks: - internal_network expose: diff --git a/docker/docker.conf b/docker/docker.conf new file mode 100644 index 0000000..40d2614 --- /dev/null +++ b/docker/docker.conf @@ -0,0 +1,2 @@ +[www] +clear_env = no \ No newline at end of file diff --git a/entrypoint.sh b/docker/entrypoint.sh similarity index 100% rename from entrypoint.sh rename to docker/entrypoint.sh diff --git a/nginx.conf b/docker/nginx.conf similarity index 100% rename from nginx.conf rename to docker/nginx.conf diff --git a/redis.conf b/docker/redis.conf similarity index 100% rename from redis.conf rename to docker/redis.conf diff --git a/supervisord.conf b/docker/supervisord.conf similarity index 100% rename from supervisord.conf rename to docker/supervisord.conf diff --git a/docker/zz-docker.conf b/docker/zz-docker.conf new file mode 100644 index 0000000..c8e8e2b --- /dev/null +++ b/docker/zz-docker.conf @@ -0,0 +1,10 @@ +[global] +daemonize=no +error_log = /var/log/php_fpm_error.log +log_limit = 1048576 +log_level = notice + +[www] +listen = 9000 +catch_workers_output = yes +decorate_workers_output = no \ No newline at end of file From e7516083be6921fe2b3126de631b6e10af621d9c Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 14 Jan 2025 14:53:10 -0500 Subject: [PATCH 177/420] "Ensure consistent file formatting across config files --- coffee_socialbox/config/socialbox.conf | 2 +- teapot_socialbox/config/socialbox.conf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/coffee_socialbox/config/socialbox.conf b/coffee_socialbox/config/socialbox.conf index 63c3be4..7374a3f 100755 --- a/coffee_socialbox/config/socialbox.conf +++ b/coffee_socialbox/config/socialbox.conf @@ -100,4 +100,4 @@ "user_display_images_path": "user_profiles", "user_display_images_max_size": 3145728 } -} +} \ No newline at end of file diff --git a/teapot_socialbox/config/socialbox.conf b/teapot_socialbox/config/socialbox.conf index d3d3c9d..d04b1ee 100755 --- a/teapot_socialbox/config/socialbox.conf +++ b/teapot_socialbox/config/socialbox.conf @@ -100,4 +100,4 @@ "user_display_images_path": "user_profiles", "user_display_images_max_size": 3145728 } -} +} \ No newline at end of file From edf6677256ac5814dbae2a8aad5b81a89364df5e Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 14 Jan 2025 14:53:21 -0500 Subject: [PATCH 178/420] Add error handling and update parameter in RPC request --- src/Socialbox/SocialClient.php | 2 +- src/Socialbox/Socialbox.php | 40 ++++++++++++++++++++-------------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/Socialbox/SocialClient.php b/src/Socialbox/SocialClient.php index c3a6a7e..3ff5a11 100644 --- a/src/Socialbox/SocialClient.php +++ b/src/Socialbox/SocialClient.php @@ -494,7 +494,7 @@ { return (bool)$this->sendRequest( new RpcRequest(StandardMethods::SETTINGS_SET_DISPLAY_NAME->value, Utilities::randomCrc32(), [ - 'display_name' => $displayName + 'name' => $displayName ]) )->getResponse()->getResult(); } diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index c00e18f..703e05a 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -56,27 +56,35 @@ // Handle the request type, only `init` and `dhe` are not encrypted using the session's encrypted key // RPC Requests must be encrypted and signed by the client, vice versa for server responses. - switch($clientRequest->getRequestType()) + try { - case RequestType::INFO: - self::handleInformationRequest(); - break; + switch($clientRequest->getRequestType()) + { + case RequestType::INFO: + self::handleInformationRequest(); + break; - case RequestType::INITIATE_SESSION: - self::handleInitiateSession($clientRequest); - break; + case RequestType::INITIATE_SESSION: + self::handleInitiateSession($clientRequest); + break; - case RequestType::DHE_EXCHANGE: - self::handleDheExchange($clientRequest); - break; + case RequestType::DHE_EXCHANGE: + self::handleDheExchange($clientRequest); + break; - case RequestType::RPC: - self::handleRpc($clientRequest); - break; + case RequestType::RPC: + self::handleRpc($clientRequest); + break; - default: - self::returnError(400, StandardError::BAD_REQUEST, 'Invalid Request-Type header'); + default: + self::returnError(400, StandardError::BAD_REQUEST, 'Invalid Request-Type header'); + } } + catch(Exception $e) + { + self::returnError(500, StandardError::INTERNAL_SERVER_ERROR, 'An internal error occurred while processing the request', $e); + } + } /** @@ -607,7 +615,7 @@ privateKey: Configuration::getCryptographyConfiguration()->getHostPrivateKey() ); } - catch (Exceptions\CryptographyException $e) + catch (Exception $e) { self::returnError(500, StandardError::INTERNAL_SERVER_ERROR, 'Failed to encrypt the server response', $e); return; From a4602bab91490deb2406080f99de4f39c08c8acd Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 14 Jan 2025 14:53:29 -0500 Subject: [PATCH 179/420] Add option to display internal exceptions in produceError --- src/Socialbox/Exceptions/StandardException.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Socialbox/Exceptions/StandardException.php b/src/Socialbox/Exceptions/StandardException.php index f9f639c..8028440 100644 --- a/src/Socialbox/Exceptions/StandardException.php +++ b/src/Socialbox/Exceptions/StandardException.php @@ -3,6 +3,8 @@ namespace Socialbox\Exceptions; use Exception; + use Socialbox\Classes\Configuration; + use Socialbox\Classes\Utilities; use Socialbox\Enums\StandardError; use Socialbox\Objects\RpcError; use Socialbox\Objects\RpcRequest; @@ -29,6 +31,11 @@ public function produceError(RpcRequest $request): ?RpcError { + if(Configuration::getSecurityConfiguration()->isDisplayInternalExceptions()) + { + return $request->produceError(StandardError::from($this->code), Utilities::throwableToString($this)); + } + return $request->produceError(StandardError::from($this->code), $this->message); } } \ No newline at end of file From 9d986595412aab6240f760c7f30ec5f195397b6b Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 14 Jan 2025 14:53:36 -0500 Subject: [PATCH 180/420] Add docker directory to web resources configuration --- .idea/webResources.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/.idea/webResources.xml b/.idea/webResources.xml index 7990450..10ead0c 100644 --- a/.idea/webResources.xml +++ b/.idea/webResources.xml @@ -8,6 +8,7 @@ + From cd12c1b987714bbbff00bb97e7f7e085bd6a72b0 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 14 Jan 2025 14:53:47 -0500 Subject: [PATCH 181/420] Refactor SessionFlags handling and improve test coverage --- src/Socialbox/Enums/Flags/SessionFlags.php | 67 ++++--- src/Socialbox/Managers/SessionManager.php | 21 +- .../Enums/Flags/SessionFlagsTest.php | 180 ++++++++++++++++++ tests/Socialbox/SocialClientTest.php | 61 ++++++ 4 files changed, 293 insertions(+), 36 deletions(-) create mode 100644 tests/Socialbox/Enums/Flags/SessionFlagsTest.php create mode 100644 tests/Socialbox/SocialClientTest.php diff --git a/src/Socialbox/Enums/Flags/SessionFlags.php b/src/Socialbox/Enums/Flags/SessionFlags.php index 01c37bf..ae47c2a 100644 --- a/src/Socialbox/Enums/Flags/SessionFlags.php +++ b/src/Socialbox/Enums/Flags/SessionFlags.php @@ -2,6 +2,8 @@ namespace Socialbox\Enums\Flags; + use Socialbox\Classes\Logger; + enum SessionFlags : string { // Session states @@ -45,20 +47,20 @@ public static function getRegistrationFlags(): array { return [ - self::SET_PASSWORD->value, - self::SET_OTP->value, - self::SET_DISPLAY_NAME->value, - self::SET_DISPLAY_PICTURE->value, - self::SET_PHONE->value, - self::SET_BIRTHDAY->value, - self::SET_EMAIL->value, - self::VER_PRIVACY_POLICY->value, - self::VER_TERMS_OF_SERVICE->value, - self::VER_COMMUNITY_GUIDELINES->value, - self::VER_EMAIL->value, - self::VER_SMS->value, - self::VER_PHONE_CALL->value, - self::VER_IMAGE_CAPTCHA->value + self::SET_PASSWORD, + self::SET_OTP, + self::SET_DISPLAY_NAME, + self::SET_DISPLAY_PICTURE, + self::SET_PHONE, + self::SET_BIRTHDAY, + self::SET_EMAIL, + self::VER_PRIVACY_POLICY, + self::VER_TERMS_OF_SERVICE, + self::VER_COMMUNITY_GUIDELINES, + self::VER_EMAIL, + self::VER_SMS, + self::VER_PHONE_CALL, + self::VER_IMAGE_CAPTCHA ]; } @@ -70,10 +72,10 @@ public static function getAuthenticationFlags(): array { return [ - self::VER_IMAGE_CAPTCHA->value, - self::VER_PASSWORD->value, - self::VER_OTP->value, - self::VER_AUTHENTICATION->value + self::VER_IMAGE_CAPTCHA, + self::VER_PASSWORD, + self::VER_OTP, + self::VER_AUTHENTICATION ]; } @@ -112,20 +114,35 @@ */ public static function isComplete(array $flags): bool { - $flags = array_map(function ($flag) {return is_string($flag) ? SessionFlags::from($flag) : $flag;}, $flags); - $flags = array_map(fn(SessionFlags $flag) => $flag->value, $flags); + // Map provided flags to their scalar values if they are enums + $flagValues = array_map(fn($flag) => $flag instanceof SessionFlags ? $flag->value : $flag, $flags); - if (in_array(SessionFlags::REGISTRATION_REQUIRED->value, $flags)) + if (in_array(SessionFlags::REGISTRATION_REQUIRED, $flags, true)) { - return !array_intersect(self::getRegistrationFlags(), $flags); // Check if the intersection is empty + Logger::getLogger()->info('Checking registration flags'); + // Compare values instead of objects + return empty(array_intersect(self::getScalarValues(self::getRegistrationFlags()), $flagValues)); } - if (in_array(SessionFlags::AUTHENTICATION_REQUIRED->value, $flags)) + if (in_array(SessionFlags::AUTHENTICATION_REQUIRED, $flags, true)) { - return !array_intersect(self::getAuthenticationFlags(), $flags); // Check if the intersection is empty - + Logger::getLogger()->info('Checking authentication flags'); + // Compare values instead of objects + return empty(array_intersect(self::getScalarValues(self::getAuthenticationFlags()), $flagValues)); } + Logger::getLogger()->info('Neither registration nor authentication flags found'); return true; } + + /** + * Helper method: Converts an array of SessionFlags enums to their scalar values (strings) + * + * @param array $flagEnums Array of SessionFlags objects + * @return array Array of scalar values corresponding to the flags + */ + private static function getScalarValues(array $flagEnums): array + { + return array_map(fn(SessionFlags $flag) => $flag->value, $flagEnums); + } } diff --git a/src/Socialbox/Managers/SessionManager.php b/src/Socialbox/Managers/SessionManager.php index 2c2e848..6bf5071 100644 --- a/src/Socialbox/Managers/SessionManager.php +++ b/src/Socialbox/Managers/SessionManager.php @@ -11,7 +11,6 @@ use Socialbox\Classes\Cryptography; use Socialbox\Classes\Database; use Socialbox\Classes\Logger; - use Socialbox\Classes\Utilities; use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\SessionState; use Socialbox\Enums\StandardError; @@ -33,9 +32,7 @@ * @param string $clientPublicSigningKey The client's public signing key, which must be a valid Ed25519 key. * @param string $clientPublicEncryptionKey The client's public encryption key, which must be a valid X25519 key. * @param KeyPair $serverEncryptionKeyPair The server's key pair for encryption, including both public and private keys. - * * @return string The UUID of the newly created session. - * * @throws InvalidArgumentException If the provided public signing key or encryption key is invalid. * @throws DatabaseOperationException If there is an error during the session creation in the database. */ @@ -310,7 +307,7 @@ * Retrieves the flags associated with a specific session. * * @param string $uuid The UUID of the session to retrieve flags for. - * @return array An array of flags associated with the specified session. + * @return SessionFlags[] An array of flags associated with the specified session. * @throws StandardException If the specified session does not exist. * @throws DatabaseOperationException If there */ @@ -359,7 +356,7 @@ try { $statement = Database::getConnection()->prepare("UPDATE sessions SET flags=? WHERE uuid=?"); - $statement->bindValue(1, Utilities::serializeList($flags)); + $statement->bindValue(1, SessionFlags::toString($flags)); $statement->bindParam(2, $uuid); $statement->execute(); } @@ -381,15 +378,16 @@ { Logger::getLogger()->verbose(sprintf("Removing flags from session %s", $uuid)); - $existingFlags = self::getFlags($uuid); - $flagsToRemove = array_map(fn($flag) => $flag->value, $flags); - $updatedFlags = array_filter($existingFlags, fn($flag) => !in_array($flag->value, $flagsToRemove)); - $flags = SessionFlags::toString($updatedFlags); + $existingFlags = array_map(fn(SessionFlags $flag) => $flag->value, self::getFlags($uuid)); + $flagsToRemove = array_map(fn(SessionFlags $flag) => $flag->value, $flags); + $updatedFlags = array_diff($existingFlags, $flagsToRemove); + $flagString = SessionFlags::toString(array_map(fn(string $value) => SessionFlags::from($value), $updatedFlags)); try { + // Update the session flags in the database $statement = Database::getConnection()->prepare("UPDATE sessions SET flags=? WHERE uuid=?"); - $statement->bindValue(1, $flags); // Directly use the toString() result + $statement->bindValue(1, $flagString); // Use the stringified updated flags $statement->bindParam(2, $uuid); $statement->execute(); } @@ -436,7 +434,7 @@ public static function updateFlow(SessionRecord $session, array $flagsToRemove=[]): void { // Don't do anything if the session is already authenticated - if(!in_array(SessionFlags::REGISTRATION_REQUIRED, $session->getFlags()) || !in_array(SessionFlags::AUTHENTICATION_REQUIRED, $session->getFlags())) + if (!$session->flagExists(SessionFlags::AUTHENTICATION_REQUIRED) && !$session->flagExists(SessionFlags::REGISTRATION_REQUIRED)) { return; } @@ -456,6 +454,7 @@ { SessionManager::removeFlags($session->getUuid(), [SessionFlags::REGISTRATION_REQUIRED, SessionFlags::AUTHENTICATION_REQUIRED]); // Remove the registration/authentication flags SessionManager::setAuthenticated($session->getUuid(), true); // Mark the session as authenticated + RegisteredPeerManager::enablePeer($session->getPeerUuid()); // Enable the peer } } } \ No newline at end of file diff --git a/tests/Socialbox/Enums/Flags/SessionFlagsTest.php b/tests/Socialbox/Enums/Flags/SessionFlagsTest.php new file mode 100644 index 0000000..46f4974 --- /dev/null +++ b/tests/Socialbox/Enums/Flags/SessionFlagsTest.php @@ -0,0 +1,180 @@ +assertEquals($expectedFlags, $result); + } + + /** + * Test that fromString handles an empty string correctly by returning an empty array. + */ + public function testFromStringWithEmptyString() + { + $flagString = ''; + $result = SessionFlags::fromString($flagString); + + $this->assertEquals([], $result); + } + + /** + * Test that fromString correctly trims whitespace from flag values. + */ + public function testFromStringWithWhitespace() + { + $flagString = ' SET_PASSWORD , SET_EMAIL , VER_SMS '; + $expectedFlags = [ + SessionFlags::SET_PASSWORD, + SessionFlags::SET_EMAIL, + SessionFlags::VER_SMS, + ]; + + $result = SessionFlags::fromString($flagString); + + $this->assertEquals($expectedFlags, $result); + } + + /** + * Test that fromString throws an error for invalid values. + */ + public function testFromStringWithInvalidValues() + { + $this->expectException(\ValueError::class); + + $flagString = 'INVALID_FLAG'; + SessionFlags::fromString($flagString); + } + + /** + * Test that fromString works for a single valid flag. + */ + public function testFromStringWithSingleValue() + { + $flagString = 'SET_PASSWORD'; + $expectedFlags = [SessionFlags::SET_PASSWORD]; + + $result = SessionFlags::fromString($flagString); + + $this->assertEquals($expectedFlags, $result); + } + + /** + * Test that fromString correctly handles duplicate flag values in the input string. + */ + public function testFromStringWithDuplicateValues() + { + $flagString = 'SET_EMAIL,SET_EMAIL,VER_SMS'; + $expectedFlags = [ + SessionFlags::SET_EMAIL, + SessionFlags::SET_EMAIL, + SessionFlags::VER_SMS, + ]; + + $result = SessionFlags::fromString($flagString); + + $this->assertEquals($expectedFlags, $result); + } + + /** + * Test that isComplete returns true for an empty array of flags. + */ + public function testIsCompleteWithEmptyFlags() + { + $flags = []; + + $result = SessionFlags::isComplete($flags); + + $this->assertTrue($result); + } + + /** + * Test that isComplete returns false when registration flags are incomplete. + */ + public function testIsCompleteWithIncompleteRegistrationFlags() + { + $flags = [ + SessionFlags::REGISTRATION_REQUIRED, + SessionFlags::SET_PASSWORD, + ]; + + $result = SessionFlags::isComplete($flags); + + $this->assertFalse($result); + } + + /** + * Test that isComplete returns false when authentication flags are incomplete. + */ + public function testIsCompleteWithIncompleteAuthenticationFlags() + { + $flags = [ + SessionFlags::AUTHENTICATION_REQUIRED, + SessionFlags::VER_PASSWORD, + ]; + + $result = SessionFlags::isComplete($flags); + + $this->assertFalse($result); + } + + /** + * Test that isComplete returns true when registration flags are complete. + */ + public function testIsCompleteWithCompleteRegistrationFlags() + { + $flags = [ + SessionFlags::REGISTRATION_REQUIRED, + ]; + + $result = SessionFlags::isComplete($flags); + + $this->assertTrue($result); + } + + /** + * Test that isComplete returns true when authentication flags are complete. + */ + public function testIsCompleteWithCompleteAuthenticationFlags() + { + $flags = [ + SessionFlags::AUTHENTICATION_REQUIRED, + ]; + + $result = SessionFlags::isComplete($flags); + + $this->assertTrue($result); + } + + /** + * Test that isComplete ignores non-relevant flags while processing. + */ + public function testIsCompleteIgnoringNonRelevantFlags() + { + $flags = [ + SessionFlags::RATE_LIMITED, + SessionFlags::AUTHENTICATION_REQUIRED, + ]; + + $result = SessionFlags::isComplete($flags); + + $this->assertTrue($result); + } + } diff --git a/tests/Socialbox/SocialClientTest.php b/tests/Socialbox/SocialClientTest.php new file mode 100644 index 0000000..0089677 --- /dev/null +++ b/tests/Socialbox/SocialClientTest.php @@ -0,0 +1,61 @@ +@'. + */ + private static function generateUsername(string $domain): string + { + $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + $charactersLength = strlen($characters); + $randomString = ''; + + for ($i = 0; $i < 16; $i++) + { + $randomString .= $characters[rand(0, $charactersLength - 1)]; + } + + return 'user' . $randomString . '@' . $domain; + } + + public function testConnection() :void + { + $coffeeClient = new SocialClient(self::generateUsername('intvo.id')); + + // Check initial session state + $this->assertFalse($coffeeClient->getSessionState()->isAuthenticated()); + $this->assertTrue($coffeeClient->getSessionState()->containsFlag(SessionFlags::REGISTRATION_REQUIRED)); + $this->assertTrue($coffeeClient->getSessionState()->containsFlag(SessionFlags::SET_PASSWORD)); + $this->assertTrue($coffeeClient->getSessionState()->containsFlag(SessionFlags::SET_DISPLAY_NAME)); + + // Check progressive session state + $this->assertTrue($coffeeClient->settingsSetPassword('coffeePassword')); + $this->assertFalse($coffeeClient->getSessionState()->containsFlag(SessionFlags::SET_PASSWORD)); + $this->assertTrue($coffeeClient->settingsSetDisplayName('Coffee User')); + $this->assertFalse($coffeeClient->getSessionState()->containsFlag(SessionFlags::SET_DISPLAY_NAME)); + + $this->assertFalse($coffeeClient->getSessionState()->containsFlag(SessionFlags::REGISTRATION_REQUIRED)); + $this->assertTrue($coffeeClient->getSessionState()->isAuthenticated()); + } + } From 34b31c58a8e95e19d09aa4e26a2addc0a07de0f9 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 14 Jan 2025 15:44:54 -0500 Subject: [PATCH 182/420] Update RPC endpoints and add shared network configuration --- .env | 8 ++++---- coffee_socialbox/config/socialbox.conf | 6 +++--- docker-compose.test.yml | 22 +++++++++++++--------- teapot_socialbox/config/socialbox.conf | 6 +++--- tests/Socialbox/SocialClientTest.php | 22 ++++++++++++++++++++-- 5 files changed, 43 insertions(+), 21 deletions(-) diff --git a/.env b/.env index 94b5d60..d86c46b 100644 --- a/.env +++ b/.env @@ -28,9 +28,9 @@ REDIS_PASSWORD=root # Test Configuration, can be ignored. Used for docker-compose-test.yml SB_COFFEE_NAME=coffee SB_COFFEE_DOMAIN=coffee.com -SB_COFFEE_RPC_ENDPOINT=http://127.0.0.0:8086/ -SB_INSTANCE_DNS_MOCK_COFFEE="coffee.com v=socialbox;sb-rpc=http://127.0.0.0:8086/;sb-key=sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc;sb-exp=0" +SB_COFFEE_RPC_ENDPOINT=http://coffee_socialbox:8085/ +SB_INSTANCE_DNS_MOCK_COFFEE="coffee.com v=socialbox;sb-rpc=http://coffee_socialbox:8085/;sb-key=sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc;sb-exp=0" SB_TEAPOT_DOMAIN=teapot.com -SB_TEAPOT_RPC_ENDPOINT=http://127.0.0.0:8087/ -SB_INSTANCE_DNS_MOCK_TEAPOT="teapot.com v=socialbox;sb-rpc=http://127.0.0.0:8087/;sb-key=sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY;sb-exp=0" \ No newline at end of file +SB_TEAPOT_RPC_ENDPOINT=http://teapot_socialbox:8085/ +SB_INSTANCE_DNS_MOCK_TEAPOT="teapot.com v=socialbox;sb-rpc=http://teapot_socialbox:8085/;sb-key=sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY;sb-exp=0" \ No newline at end of file diff --git a/coffee_socialbox/config/socialbox.conf b/coffee_socialbox/config/socialbox.conf index 7374a3f..1483880 100755 --- a/coffee_socialbox/config/socialbox.conf +++ b/coffee_socialbox/config/socialbox.conf @@ -3,10 +3,10 @@ "enabled": true, "name": "coffee", "domain": "coffee.com", - "rpc_endpoint": "http://127.0.0.0:8086/", + "rpc_endpoint": "http://coffee_socialbox:8085/", "dns_mocks": { - "teapot.com": "v=socialbox;sb-rpc=http://127.0.0.0:8087/;sb-key=sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY;sb-exp=0", - "coffee.com": "v=socialbox;sb-rpc=http://127.0.0.0:8086/;sb-key=sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc;sb-exp=0" + "teapot.com": "v=socialbox;sb-rpc=http://teapot_socialbox:8085/;sb-key=sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY;sb-exp=0", + "coffee.com": "v=socialbox;sb-rpc=http://coffee_socialbox:8085/;sb-key=sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc;sb-exp=0" } }, "security": { diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 47c225b..9c3d393 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -19,6 +19,7 @@ services: condition: service_healthy networks: - coffee_network + - shared_network restart: unless-stopped volumes: - ./coffee_socialbox/config:/etc/config @@ -34,7 +35,7 @@ services: # Change these values to match your environment or update the .env file SB_INSTANCE_NAME: ${SB_COFFEE_NAME:-coffee} # Instance name SB_COFFEE_NAME SB_INSTANCE_DOMAIN: ${SB_COFFEE_DOMAIN:-coffee.com} # Instance domain SB_COFFEE_DOMAIN - SB_INSTANCE_RPC_ENDPOINT: ${SB_COFFEE_RPC_ENDPOINT:-http://127.0.0.0:8086/} # Instance RPC endpoint SB_COFFEE_RPC_ENDPOINT + SB_INSTANCE_RPC_ENDPOINT: ${SB_COFFEE_RPC_ENDPOINT:-http://coffee_socialbox:8085/} # Instance RPC endpoint SB_COFFEE_RPC_ENDPOINT SB_LOGGING_CONSOLE_ENABLED: ${SB_LOGGING_CONSOLE_ENABLED:-true} SB_LOGGING_CONSOLE_LEVEL: ${SB_LOGGING_CONSOLE_LEVEL:-debug} SB_LOGGING_FILE_ENABLED: ${SB_LOGGING_FILE_ENABLED:-true} @@ -59,10 +60,10 @@ services: # Usage: SB_INSTANCE_DNS_MOCK_: # Environment Variable name is ignored, only the value is used with the prefix being used to detect # the instance name and the suffix being used to detect the TXT record - SB_INSTANCE_DNS_MOCK_COFFEE: ${SB_INSTANCE_DNS_MOCK_COFFEE:-"coffee.com v=socialbox;sb-rpc=http://127.0.0.0:8086/;sb-key=sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc;sb-exp=0"} - SB_INSTANCE_DNS_MOCK_TEAPOT: ${SB_INSTANCE_DNS_MOCK_TEAPOT:-"teapot.com v=socialbox;sb-rpc=http://127.0.0.0:8087/;sb-key=sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY;sb-exp=0"} + SB_INSTANCE_DNS_MOCK_COFFEE: ${SB_INSTANCE_DNS_MOCK_COFFEE:-"coffee.com v=socialbox;sb-rpc=http://coffee_socialbox:8085/;sb-key=sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc;sb-exp=0"} + SB_INSTANCE_DNS_MOCK_TEAPOT: ${SB_INSTANCE_DNS_MOCK_TEAPOT:-"teapot.com v=socialbox;sb-rpc=http://teapot_socialbox:8085/;sb-key=sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY;sb-exp=0"} healthcheck: - test: ["CMD", "curl", "-f", "-H", "Request-Type: ping", "${SB_INSTANCE_RPC_ENDPOINT-http://127.0.0.0:8086/}"] + test: ["CMD", "curl", "-f", "-H", "Request-Type: ping", "${SB_INSTANCE_RPC_ENDPOINT-http://coffee_socialbox:8085/}"] interval: 30s timeout: 10s retries: 3 @@ -122,6 +123,7 @@ services: condition: service_healthy networks: - teapot_network + - shared_network restart: unless-stopped volumes: - ./teapot_socialbox/config:/etc/config @@ -137,7 +139,7 @@ services: # Change these values to match your environment or update the .env file SB_INSTANCE_NAME: ${SB_TEAPOT_NAME:-teapot} # Instance name SB_TEAPOT_NAME SB_INSTANCE_DOMAIN: ${SB_TEAPOT_DOMAIN:-teapot.com} # Instance domain SB_TEAPOT_DOMAIN - SB_INSTANCE_RPC_ENDPOINT: ${SB_TEAPOT_RPC_ENDPOINT:-http://127.0.0.0:8087/} # Instance RPC endpoint SB_TEAPOT_RPC_ENDPOINT + SB_INSTANCE_RPC_ENDPOINT: ${SB_TEAPOT_RPC_ENDPOINT:-http://teapot_socialbox:8085/} # Instance RPC endpoint SB_TEAPOT_RPC_ENDPOINT SB_LOGGING_CONSOLE_ENABLED: ${SB_LOGGING_CONSOLE_ENABLED:-true} SB_LOGGING_CONSOLE_LEVEL: ${SB_LOGGING_CONSOLE_LEVEL:-debug} SB_LOGGING_FILE_ENABLED: ${SB_LOGGING_FILE_ENABLED:-true} @@ -162,10 +164,10 @@ services: # Usage: SB_INSTANCE_DNS_MOCK_: # Environment Variable name is ignored, only the value is used with the prefix being used to detect # the instance name and the suffix being used to detect the TXT record - SB_INSTANCE_DNS_MOCK_COFFEE: ${SB_INSTANCE_DNS_MOCK_COFFEE:-"coffee.com v=socialbox;sb-rpc=http://127.0.0.0:8086/;sb-key=sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc;sb-exp=0"} - SB_INSTANCE_DNS_MOCK_TEAPOT: ${SB_INSTANCE_DNS_MOCK_TEAPOT:-"teapot.com v=socialbox;sb-rpc=http://127.0.0.0:8087/;sb-key=sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY;sb-exp=0"} + SB_INSTANCE_DNS_MOCK_COFFEE: ${SB_INSTANCE_DNS_MOCK_COFFEE:-"coffee.com v=socialbox;sb-rpc=http://coffee_socialbox:8085/;sb-key=sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc;sb-exp=0"} + SB_INSTANCE_DNS_MOCK_TEAPOT: ${SB_INSTANCE_DNS_MOCK_TEAPOT:-"teapot.com v=socialbox;sb-rpc=http://teapot_socialbox:8085/;sb-key=sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY;sb-exp=0"} healthcheck: - test: ["CMD", "curl", "-f", "-H", "Request-Type: ping", "${SB_INSTANCE_RPC_ENDPOINT-http://127.0.0.0:8087/}"] + test: ["CMD", "curl", "-f", "-H", "Request-Type: ping", "${SB_INSTANCE_RPC_ENDPOINT-http://teapot_socialbox:8085/}"] interval: 30s timeout: 10s retries: 3 @@ -226,4 +228,6 @@ networks: name: teapot_network coffee_network: driver: bridge - name: coffee_network \ No newline at end of file + name: coffee_network + shared_network: + driver: bridge \ No newline at end of file diff --git a/teapot_socialbox/config/socialbox.conf b/teapot_socialbox/config/socialbox.conf index d04b1ee..a6e14ed 100755 --- a/teapot_socialbox/config/socialbox.conf +++ b/teapot_socialbox/config/socialbox.conf @@ -3,10 +3,10 @@ "enabled": true, "name": "teapot", "domain": "teapot.com", - "rpc_endpoint": "http://127.0.0.0:8087/", + "rpc_endpoint": "http://teapot_socialbox:8085/", "dns_mocks": { - "teapot.com": "v=socialbox;sb-rpc=http://127.0.0.0:8087/;sb-key=sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY;sb-exp=0", - "coffee.com": "v=socialbox;sb-rpc=http://127.0.0.0:8086/;sb-key=sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc;sb-exp=0" + "teapot.com": "v=socialbox;sb-rpc=http://teapot_socialbox:8085/;sb-key=sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY;sb-exp=0", + "coffee.com": "v=socialbox;sb-rpc=http://coffee_socialbox:8085/;sb-key=sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc;sb-exp=0" } }, "security": { diff --git a/tests/Socialbox/SocialClientTest.php b/tests/Socialbox/SocialClientTest.php index 0089677..e0a0691 100644 --- a/tests/Socialbox/SocialClientTest.php +++ b/tests/Socialbox/SocialClientTest.php @@ -39,9 +39,17 @@ return 'user' . $randomString . '@' . $domain; } - public function testConnection() :void + private static function registerUser(string $domain): SocialClient { - $coffeeClient = new SocialClient(self::generateUsername('intvo.id')); + $client = new SocialClient(self::generateUsername($domain)); + $client->settingsSetPassword("password"); + $client->settingsSetDisplayName("Example User"); + return $client; + } + + public function testRegistration(): void + { + $coffeeClient = new SocialClient(self::generateUsername(self::COFFEE_DOMAIN)); // Check initial session state $this->assertFalse($coffeeClient->getSessionState()->isAuthenticated()); @@ -58,4 +66,14 @@ $this->assertFalse($coffeeClient->getSessionState()->containsFlag(SessionFlags::REGISTRATION_REQUIRED)); $this->assertTrue($coffeeClient->getSessionState()->isAuthenticated()); } + + public function testResolveDecentralizedPeer(): void + { + $coffeeUser = self::registerUser(self::COFFEE_DOMAIN); + $this->assertTrue($coffeeUser->getSessionState()->isAuthenticated()); + $teapotUser = self::registerUser(self::TEAPOT_DOMAIN); + $this->assertTrue($teapotUser->getSessionState()->isAuthenticated()); + + $coffeePeer = $coffeeUser->resolvePeer($teapotUser->getIdentifiedAs()); + } } From 1cab09b6752cfcf07bbc1b2943863977b8287841 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 14 Jan 2025 15:45:02 -0500 Subject: [PATCH 183/420] Add isExternal method to PeerAddress class --- src/Socialbox/Objects/PeerAddress.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Socialbox/Objects/PeerAddress.php b/src/Socialbox/Objects/PeerAddress.php index b603399..a2640fe 100644 --- a/src/Socialbox/Objects/PeerAddress.php +++ b/src/Socialbox/Objects/PeerAddress.php @@ -71,6 +71,21 @@ return $this->username === ReservedUsernames::HOST->value && $this->domain === Configuration::getInstanceConfiguration()->getDomain(); } + /** + * Determines if the peer is external. + * + * @return bool True if the peer is external, false otherwise. + */ + public function isExternal(): bool + { + if($this->isHost()) + { + return false; + } + + return $this->username === ReservedUsernames::HOST->value; + } + /** * Returns whether the peer requires authentication, for example, the anonymous user does not require authentication * From 2fa4fdebee8dd410e164d4fdbb51c643077e3743 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 14 Jan 2025 15:45:10 -0500 Subject: [PATCH 184/420] Add getter methods to RpcClient for instance properties --- src/Socialbox/Classes/RpcClient.php | 110 ++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/src/Socialbox/Classes/RpcClient.php b/src/Socialbox/Classes/RpcClient.php index b8e21f8..1371cb5 100644 --- a/src/Socialbox/Classes/RpcClient.php +++ b/src/Socialbox/Classes/RpcClient.php @@ -575,6 +575,116 @@ return $responses; } + /** + * Retrieves the peer address identified for the instance. + * + * @return PeerAddress The identified peer address. + */ + public function getIdentifiedAs(): PeerAddress + { + return $this->identifiedAs; + } + + /** + * Retrieves the server's public signing key. + * + * @return string The server's public signing key. + */ + public function getServerPublicSigningKey(): string + { + return $this->serverPublicSigningKey; + } + + /** + * Retrieves the server's public encryption key. + * + * @return string The public encryption key of the server. + */ + public function getServerPublicEncryptionKey(): string + { + return $this->serverPublicEncryptionKey; + } + + /** + * Retrieves the client's signing key pair. + * + * @return KeyPair The client's signing key pair. + */ + public function getClientSigningKeyPair(): KeyPair + { + return $this->clientSigningKeyPair; + } + + /** + * Retrieves the client encryption key pair configured for the instance. + * + * @return KeyPair The client encryption key pair. + */ + public function getClientEncryptionKeyPair(): KeyPair + { + return $this->clientEncryptionKeyPair; + } + + /** + * Retrieves the private shared secret configured for the instance. + * + * @return string The private shared secret value. + */ + public function getPrivateSharedSecret(): string + { + return $this->privateSharedSecret; + } + + /** + * Retrieves the client transport encryption key configured for the instance. + * + * @return string The client transport encryption key value. + */ + public function getClientTransportEncryptionKey(): string + { + return $this->clientTransportEncryptionKey; + } + + /** + * Retrieves the server transport encryption key. + * + * @return string The server transport encryption key. + */ + public function getServerTransportEncryptionKey(): string + { + return $this->serverTransportEncryptionKey; + } + + /** + * Retrieves the RPC endpoint configured for the instance. + * + * @return string The RPC endpoint value. + */ + public function getRpcEndpoint(): string + { + return $this->rpcEndpoint; + } + + /** + * Retrieves the remote server address configured for the instance. + * + * @return string The remote server address. + */ + public function getRemoteServer(): string + { + return $this->remoteServer; + } + + /** + * Retrieves the session UUID associated with the instance. + * + * @return string The session UUID value. + */ + public function getSessionUuid(): string + { + return $this->sessionUuid; + } + /** * Exports the current session details into an ExportedSession object. * From 5cb8e72ca5ab1ce3cf09bbb32ea36a93d8ef58ad Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 14 Jan 2025 15:45:17 -0500 Subject: [PATCH 185/420] Refactor header validation logic. --- src/Socialbox/Socialbox.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index 703e05a..52ae74f 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -122,12 +122,6 @@ return false; } - if(!$clientRequest->headerExists(StandardHeaders::SIGNING_PUBLIC_KEY)) - { - self::returnError(400, StandardError::BAD_REQUEST, 'Missing required header: ' . StandardHeaders::SIGNING_PUBLIC_KEY->value); - return false; - } - if(!$clientRequest->headerExists(StandardHeaders::ENCRYPTION_PUBLIC_KEY)) { self::returnError(400, StandardError::BAD_REQUEST, 'Missing required header: ' . StandardHeaders::ENCRYPTION_PUBLIC_KEY->value); @@ -146,6 +140,12 @@ return false; } + if(!$clientRequest->getIdentifyAs()->isExternal() && !$clientRequest->headerExists(StandardHeaders::SIGNING_PUBLIC_KEY)) + { + self::returnError(400, StandardError::BAD_REQUEST, 'Missing required header: ' . StandardHeaders::SIGNING_PUBLIC_KEY->value); + return false; + } + return true; } From c6bc83ad87574ef4c418ea97e3e5b42bdc2cad56 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 14 Jan 2025 15:45:25 -0500 Subject: [PATCH 186/420] Fix key name in RPC request payload --- src/Socialbox/SocialClient.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Socialbox/SocialClient.php b/src/Socialbox/SocialClient.php index 3ff5a11..829728e 100644 --- a/src/Socialbox/SocialClient.php +++ b/src/Socialbox/SocialClient.php @@ -644,7 +644,7 @@ return Peer::fromArray($this->sendRequest( new RpcRequest(StandardMethods::RESOLVE_PEER->value, Utilities::randomCrc32(), [ - 'peer_address' => $peerAddress + 'peer' => $peerAddress ]) )->getResponse()->getResult()); } From 5e78afc43e550ddfdaada26787f661659e31c745 Mon Sep 17 00:00:00 2001 From: mykola2312 <49044616+mykola2312@users.noreply.github.com> Date: Wed, 15 Jan 2025 00:54:59 +0200 Subject: [PATCH 187/420] Make supervisord watch php logs --- docker/supervisord.conf | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docker/supervisord.conf b/docker/supervisord.conf index b46bbce..f723f44 100644 --- a/docker/supervisord.conf +++ b/docker/supervisord.conf @@ -17,11 +17,16 @@ autorestart=true priority=20 stdout_logfile=/var/log/fpm.log stderr_logfile=/var/log/fpm_error.log -stdout_logfile_maxbytes=20MB +stdout_logfile_maxbytes=0 stdout_logfile_backups=5 -stderr_logfile_maxbytes=20MB +stderr_logfile_maxbytes=0 stderr_logfile_backups=5 +[program:php-fpm-log] +command=tail -f /var/log/fpm.log /var/log/fpm_error.log +stdout_events_enabled=true +stderr_events_enabled=true + [program:nginx] command=/usr/sbin/nginx -g "daemon off;" -c /etc/nginx/nginx.conf autostart=true From e5c6bed02e317121c06edd9f2b7e1afedeb106f4 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 14 Jan 2025 19:26:20 -0500 Subject: [PATCH 188/420] Add server_public_signing_key to session response payload --- src/Socialbox/Managers/ExternalSessionManager.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Socialbox/Managers/ExternalSessionManager.php b/src/Socialbox/Managers/ExternalSessionManager.php index 2fc5f3d..8ad0364 100644 --- a/src/Socialbox/Managers/ExternalSessionManager.php +++ b/src/Socialbox/Managers/ExternalSessionManager.php @@ -111,6 +111,7 @@ 'session_uuid' => $result['session_uuid'], 'transport_encryption_algorithm' => $result['transport_encryption_algorithm'], 'server_keypair_expires' => $result['server_keypair_expires'], + 'server_public_signing_key' => $result['server_public_signing_key'], 'server_public_encryption_key' => $result['server_public_encryption_key'], 'client_public_signing_key' => Configuration::getCryptographyConfiguration()->getHostPublicKey(), 'client_private_signing_key' => Configuration::getCryptographyConfiguration()->getHostPrivateKey(), From 56acd4cf81c02b1be8cf289d5e0c7a12ba88a717 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 14 Jan 2025 19:26:31 -0500 Subject: [PATCH 189/420] Enhance RpcException message with encryption algorithm details --- src/Socialbox/Classes/RpcClient.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Socialbox/Classes/RpcClient.php b/src/Socialbox/Classes/RpcClient.php index 1371cb5..a370ab5 100644 --- a/src/Socialbox/Classes/RpcClient.php +++ b/src/Socialbox/Classes/RpcClient.php @@ -84,7 +84,7 @@ // Check if the transport encryption algorithm has changed if($this->serverInformation->getTransportEncryptionAlgorithm() !== $exportedSession->getTransportEncryptionAlgorithm()) { - throw new RpcException('The server has changed its transport encryption algorithm, a new session must be created'); + throw new RpcException('The server has changed its transport encryption algorithm, a new session must be created, old algorithm: ' . $exportedSession->getTransportEncryptionAlgorithm() . ', new algorithm: ' . $this->serverInformation->getTransportEncryptionAlgorithm()); } return; From a7f5d268de352cd37d2e20422abcb57a7488d055 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 15 Jan 2025 13:55:33 -0500 Subject: [PATCH 190/420] Add support for handling PING requests in Socialbox --- src/Socialbox/Socialbox.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index 52ae74f..c903f0a 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -60,6 +60,10 @@ { switch($clientRequest->getRequestType()) { + case RequestType::PING: + self::handlePingRequest(); + break; + case RequestType::INFO: self::handleInformationRequest(); break; @@ -87,6 +91,18 @@ } + /** + * Handles an incoming ping request by sending a successful HTTP response. + * + * @return void + */ + private static function handlePingRequest(): void + { + http_response_code(200); + header('Content-Type: text/plain'); + print('OK'); + } + /** * Handles an information request by setting the appropriate HTTP response code, * content type headers, and printing the server information in JSON format. From 3adb62cfc750ac610b1e94bc903aab8fd32a0de4 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 15 Jan 2025 13:55:43 -0500 Subject: [PATCH 191/420] Add PING request type to RequestType enum --- src/Socialbox/Enums/Types/RequestType.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Socialbox/Enums/Types/RequestType.php b/src/Socialbox/Enums/Types/RequestType.php index f908aea..e447884 100644 --- a/src/Socialbox/Enums/Types/RequestType.php +++ b/src/Socialbox/Enums/Types/RequestType.php @@ -4,6 +4,11 @@ enum RequestType : string { + /** + * Represents the action of sending a ping request. + */ + case PING = 'ping'; + /** * Represents the action of getting server information (Non-RPC Request) */ From 4a143a8a978b450b9221001392d064b92f061c8a Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 15 Jan 2025 13:55:52 -0500 Subject: [PATCH 192/420] Refactor database operations for resolved DNS records --- .../database/resolved_dns_records.sql | 4 +- .../Managers/ResolvedDnsRecordsManager.php | 50 +++++++++---------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/Socialbox/Classes/Resources/database/resolved_dns_records.sql b/src/Socialbox/Classes/Resources/database/resolved_dns_records.sql index 5f4c03f..57f95bf 100644 --- a/src/Socialbox/Classes/Resources/database/resolved_dns_records.sql +++ b/src/Socialbox/Classes/Resources/database/resolved_dns_records.sql @@ -1,7 +1,7 @@ create table resolved_dns_records ( - domain varchar(512) not null comment 'The domain name' - primary key comment 'Unique Index for the server domain', + domain varchar(512) not null comment 'Unique Index for the server domain' + primary key, rpc_endpoint text not null comment 'The endpoint of the RPC server', public_key text not null comment 'The Public Key of the server', expires bigint not null comment 'The Unix Timestamp for when the server''s keypair expires', diff --git a/src/Socialbox/Managers/ResolvedDnsRecordsManager.php b/src/Socialbox/Managers/ResolvedDnsRecordsManager.php index b277462..0c8c5f1 100644 --- a/src/Socialbox/Managers/ResolvedDnsRecordsManager.php +++ b/src/Socialbox/Managers/ResolvedDnsRecordsManager.php @@ -139,21 +139,28 @@ $endpoint = $dnsRecord->getRpcEndpoint(); $publicKey = $dnsRecord->getPublicSigningKey(); + if($domain === null || $endpoint === null || $publicKey === null) + { + throw new DatabaseOperationException('Failed to add a resolved server to the database: Invalid parameters'); + } + if(self::resolvedServerExists($domain)) { - $statement = Database::getConnection()->prepare("UPDATE resolved_dns_records SET rpc_endpoint=?, public_key=?, expires=?, updated=? WHERE domain=?"); - $statement->bindParam(1, $endpoint); - $statement->bindParam(2, $publicKey); - $expires = (new DateTime())->setTimestamp($dnsRecord->getExpires()); - $statement->bindParam(3, $expires); - $updated = new DateTime(); - $statement->bindParam(4, $updated); - $statement->bindParam(5, $domain); - $statement->execute(); - - if($statement->rowCount() === 0) + try { - throw new DatabaseOperationException('Failed to update a resolved server in the database'); + $statement = Database::getConnection()->prepare("UPDATE resolved_dns_records SET rpc_endpoint=:rpc_endpoint, public_key=:public_key, expires=:expires, updated=:updated WHERE domain=:domain"); + $statement->bindParam(':rpc_endpoint', $endpoint); + $statement->bindParam(':public_key', $publicKey); + $expires = (new DateTime())->setTimestamp($dnsRecord->getExpires())->format('Y-m-d H:i:s'); + $statement->bindParam(':expires', $expires); + $updated = (new DateTime())->format('Y-m-d H:i:s'); + $statement->bindParam(':updated', $updated); + $statement->bindParam(':domain', $domain); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to update a resolved server in the database', $e); } return; @@ -161,20 +168,13 @@ try { - $statement = Database::getConnection()->prepare("INSERT INTO resolved_dns_records (domain, rpc_endpoint, public_key, expires, updated) VALUES (?, ?, ?, ?, ?)"); - $statement->bindParam(1, $domain); - $statement->bindParam(2, $endpoint); - $statement->bindParam(3, $publicKey); - $expires = (new DateTime())->setTimestamp($dnsRecord->getExpires()); - $statement->bindParam(4, $expires); - $updated = new DateTime(); - $statement->bindParam(5, $updated); + $statement = Database::getConnection()->prepare("INSERT INTO resolved_dns_records (domain, rpc_endpoint, public_key, expires) VALUES (:domain, :rpc_endpoint, :public_key, :expires)"); + $statement->bindParam(':domain', $domain); + $statement->bindParam(':rpc_endpoint', $endpoint); + $statement->bindParam(':public_key', $publicKey); + $expires = (new DateTime())->setTimestamp($dnsRecord->getExpires())->format('Y-m-d H:i:s'); + $statement->bindParam(':expires', $expires); $statement->execute(); - - if($statement->rowCount() === 0) - { - throw new DatabaseOperationException('Failed to add a resolved server to the database'); - } } catch(PDOException $e) { From a2bbb6e57b5d8fc0ee34f9abc67bd343ee925569 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 15 Jan 2025 14:22:46 -0500 Subject: [PATCH 193/420] Add transport encryption and configuration updates --- .idea/dataSources.xml | 20 +++++++++++++++++++ docker-compose.test.yml | 6 ++++-- .../Managers/ExternalSessionManager.php | 4 +++- src/Socialbox/Objects/Standard/Peer.php | 1 + 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml index eb6fe11..222744a 100644 --- a/.idea/dataSources.xml +++ b/.idea/dataSources.xml @@ -8,5 +8,25 @@ jdbc:mariadb://127.0.0.1:3306/socialbox $ProjectFileDir$ + + mariadb + true + org.mariadb.jdbc.Driver + jdbc:mariadb://127.0.0.1:3308/socialbox + + + + $ProjectFileDir$ + + + mariadb + true + org.mariadb.jdbc.Driver + jdbc:mariadb://127.0.0.1:3307/socialbox + + + + $ProjectFileDir$ + \ No newline at end of file diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 9c3d393..af06faa 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -23,7 +23,6 @@ services: restart: unless-stopped volumes: - ./coffee_socialbox/config:/etc/config - - ./coffee_socialbox/logs:/var/log - ./coffee_socialbox/data:/etc/socialbox environment: # No need to change these values @@ -81,6 +80,8 @@ services: - coffee_mariadb_data:/var/lib/mysql networks: - coffee_network + ports: + - "3308:3306" expose: - "3306" healthcheck: @@ -127,7 +128,6 @@ services: restart: unless-stopped volumes: - ./teapot_socialbox/config:/etc/config - - ./teapot_socialbox/logs:/var/log - ./teapot_socialbox/data:/etc/socialbox environment: # No need to change these values @@ -185,6 +185,8 @@ services: - teapot_mariadb_data:/var/lib/mysql networks: - teapot_network + ports: + - "3307:3306" # Unique port for Teapot instance expose: - "3306" healthcheck: diff --git a/src/Socialbox/Managers/ExternalSessionManager.php b/src/Socialbox/Managers/ExternalSessionManager.php index 8ad0364..648a5f1 100644 --- a/src/Socialbox/Managers/ExternalSessionManager.php +++ b/src/Socialbox/Managers/ExternalSessionManager.php @@ -46,13 +46,15 @@ { try { - $stmt = Database::getConnection()->prepare("INSERT INTO external_sessions (domain, rpc_endpoint, session_uuid, server_keypair_expires, server_public_signing_key, server_public_encryption_key, host_public_encryption_key, host_private_encryption_key, private_shared_secret, host_transport_encryption_key, server_transport_encryption_key) VALUES (:domain, :rpc_endpoint, :session_uuid, :server_keypair_expires, :server_public_signing_key, :server_public_encryption_key, :host_public_encryption_key, :host_private_encryption_key, :private_shared_secret, :host_transport_encryption_key, :server_transport_encryption_key)"); + $stmt = Database::getConnection()->prepare("INSERT INTO external_sessions (domain, rpc_endpoint, session_uuid, transport_encryption_algorithm, server_keypair_expires, server_public_signing_key, server_public_encryption_key, host_public_encryption_key, host_private_encryption_key, private_shared_secret, host_transport_encryption_key, server_transport_encryption_key) VALUES (:domain, :rpc_endpoint, :session_uuid, :transport_encryption_algorithm, :server_keypair_expires, :server_public_signing_key, :server_public_encryption_key, :host_public_encryption_key, :host_private_encryption_key, :private_shared_secret, :host_transport_encryption_key, :server_transport_encryption_key)"); $domain = $exportedSession->getRemoteServer(); $stmt->bindParam(':domain', $domain); $rpcEndpoint = $exportedSession->getRpcEndpoint(); $stmt->bindParam(':rpc_endpoint', $rpcEndpoint); $sessionUuid = $exportedSession->getSessionUuid(); $stmt->bindParam(':session_uuid', $sessionUuid); + $transportEncryptionAlgorithm = $exportedSession->getTransportEncryptionAlgorithm(); + $stmt->bindParam(':transport_encryption_algorithm', $transportEncryptionAlgorithm); $serverKeypairExpires = $exportedSession->getServerKeypairExpires(); $stmt->bindParam(':server_keypair_expires', $serverKeypairExpires); $serverPublicSigningKey = $exportedSession->getServerPublicSigningKey(); diff --git a/src/Socialbox/Objects/Standard/Peer.php b/src/Socialbox/Objects/Standard/Peer.php index 16b3c02..86ec249 100644 --- a/src/Socialbox/Objects/Standard/Peer.php +++ b/src/Socialbox/Objects/Standard/Peer.php @@ -27,6 +27,7 @@ */ public function __construct(array $data) { + // TODO: Bug: PHP message: PHP Warning: Undefined array key "address" in /var/ncc/packages/net.nosial.socialbox=1.0.0/bin/src/Socialbox/Objects/Standard/Peer.php on line 28 if(is_string($data['address'])) { $this->address = PeerAddress::fromAddress($data['address']); From 4064dc999127ec30f8ccd521961a3fb53315f11a Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 15 Jan 2025 14:56:09 -0500 Subject: [PATCH 194/420] Enable internal exception display in configuration --- teapot_socialbox/config/socialbox.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/teapot_socialbox/config/socialbox.conf b/teapot_socialbox/config/socialbox.conf index a6e14ed..30cba3d 100755 --- a/teapot_socialbox/config/socialbox.conf +++ b/teapot_socialbox/config/socialbox.conf @@ -10,7 +10,7 @@ } }, "security": { - "display_internal_exceptions": false, + "display_internal_exceptions": true, "resolved_servers_ttl": 600, "captcha_ttl": 200, "otp_secret_key_length": 32, @@ -100,4 +100,4 @@ "user_display_images_path": "user_profiles", "user_display_images_max_size": 3145728 } -} \ No newline at end of file +} From f0f7f887835c2a97689bd3adf2e7b25ce139c89e Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 15 Jan 2025 14:56:12 -0500 Subject: [PATCH 195/420] Enable internal exception display in configuration --- coffee_socialbox/config/socialbox.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coffee_socialbox/config/socialbox.conf b/coffee_socialbox/config/socialbox.conf index 1483880..0cb9e6f 100755 --- a/coffee_socialbox/config/socialbox.conf +++ b/coffee_socialbox/config/socialbox.conf @@ -10,7 +10,7 @@ } }, "security": { - "display_internal_exceptions": false, + "display_internal_exceptions": true, "resolved_servers_ttl": 600, "captcha_ttl": 200, "otp_secret_key_length": 32, @@ -100,4 +100,4 @@ "user_display_images_path": "user_profiles", "user_display_images_max_size": 3145728 } -} \ No newline at end of file +} From 4b69a7559bc0126e380f9a515ac692be3fc8ce6e Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 15 Jan 2025 15:04:44 -0500 Subject: [PATCH 196/420] Set internal exceptions display to true in test config --- docker-compose.test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.test.yml b/docker-compose.test.yml index af06faa..53b0297 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -39,7 +39,7 @@ services: SB_LOGGING_CONSOLE_LEVEL: ${SB_LOGGING_CONSOLE_LEVEL:-debug} SB_LOGGING_FILE_ENABLED: ${SB_LOGGING_FILE_ENABLED:-true} SB_LOGGING_FILE_LEVEL: ${SB_LOGGING_FILE_LEVEL:-debug} - SB_SECURITY_DISPLAY_INTERNAL_EXCEPTIONS: ${SB_SECURITY_DISPLAY_INTERNAL_EXCEPTIONS:-true} + SB_SECURITY_DISPLAY_INTERNAL_EXCEPTIONS: true SB_CRYPTO_KEYPAIR_EXPIRES: ${SB_CRYPTO_KEYPAIR_EXPIRES} SB_CRYPTO_ENCRYPTION_KEYS_COUNT: ${SB_CRYPTO_ENCRYPTION_KEYS_COUNT:-10} SB_CRYPTO_ENCRYPTION_KEYS_ALGORITHM: ${SB_CRYPTO_ENCRYPTION_KEYS_ALGORITHM:-xchacha20} @@ -144,7 +144,7 @@ services: SB_LOGGING_CONSOLE_LEVEL: ${SB_LOGGING_CONSOLE_LEVEL:-debug} SB_LOGGING_FILE_ENABLED: ${SB_LOGGING_FILE_ENABLED:-true} SB_LOGGING_FILE_LEVEL: ${SB_LOGGING_FILE_LEVEL:-debug} - SB_SECURITY_DISPLAY_INTERNAL_EXCEPTIONS: ${SB_SECURITY_DISPLAY_INTERNAL_EXCEPTIONS:-true} + SB_SECURITY_DISPLAY_INTERNAL_EXCEPTIONS: true SB_CRYPTO_KEYPAIR_EXPIRES: ${SB_CRYPTO_KEYPAIR_EXPIRES} SB_CRYPTO_ENCRYPTION_KEYS_COUNT: ${SB_CRYPTO_ENCRYPTION_KEYS_COUNT:-10} SB_CRYPTO_ENCRYPTION_KEYS_ALGORITHM: ${SB_CRYPTO_ENCRYPTION_KEYS_ALGORITHM:-xchacha20} From eda41155aa3478dc626c1583bb933ccfa9a2d63a Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 15 Jan 2025 15:04:53 -0500 Subject: [PATCH 197/420] Update exception message for invalid address type --- src/Socialbox/Objects/Standard/Peer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Socialbox/Objects/Standard/Peer.php b/src/Socialbox/Objects/Standard/Peer.php index 86ec249..e8d839c 100644 --- a/src/Socialbox/Objects/Standard/Peer.php +++ b/src/Socialbox/Objects/Standard/Peer.php @@ -38,7 +38,7 @@ } else { - throw new InvalidArgumentException('Invalid address value'); + throw new InvalidArgumentException('Invalid address value type, got type ' . gettype($data['address'])); } $this->displayName = $data['display_name']; From 2fcdb7cd5696fe75e322b0f1cb704995ad599cf5 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 15 Jan 2025 15:14:44 -0500 Subject: [PATCH 198/420] Minor change (automated) --- coffee_socialbox/config/socialbox.conf | 2 +- teapot_socialbox/config/socialbox.conf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/coffee_socialbox/config/socialbox.conf b/coffee_socialbox/config/socialbox.conf index 0cb9e6f..e23c2f6 100755 --- a/coffee_socialbox/config/socialbox.conf +++ b/coffee_socialbox/config/socialbox.conf @@ -100,4 +100,4 @@ "user_display_images_path": "user_profiles", "user_display_images_max_size": 3145728 } -} +} \ No newline at end of file diff --git a/teapot_socialbox/config/socialbox.conf b/teapot_socialbox/config/socialbox.conf index 30cba3d..e81d5b6 100755 --- a/teapot_socialbox/config/socialbox.conf +++ b/teapot_socialbox/config/socialbox.conf @@ -100,4 +100,4 @@ "user_display_images_path": "user_profiles", "user_display_images_max_size": 3145728 } -} +} \ No newline at end of file From 8cad8f97fec5a056eed0fd53452210fd39d01eb2 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 15 Jan 2025 15:15:06 -0500 Subject: [PATCH 199/420] Add __toString method to PeerAddress class --- src/Socialbox/Objects/PeerAddress.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Socialbox/Objects/PeerAddress.php b/src/Socialbox/Objects/PeerAddress.php index a2640fe..4372e11 100644 --- a/src/Socialbox/Objects/PeerAddress.php +++ b/src/Socialbox/Objects/PeerAddress.php @@ -109,4 +109,14 @@ { return sprintf("%s@%s", $this->username, $this->domain); } + + /** + * Returns the string representation of the object + * + * @return string The string representation of the object + */ + public function __toString(): string + { + return $this->getAddress(); + } } \ No newline at end of file From f9a268058f58c3015dff6118046e44951d11e2a8 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 15 Jan 2025 15:15:21 -0500 Subject: [PATCH 200/420] Refactor toStandardPeer to improve data construction clarity --- src/Socialbox/Objects/Database/RegisteredPeerRecord.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Socialbox/Objects/Database/RegisteredPeerRecord.php b/src/Socialbox/Objects/Database/RegisteredPeerRecord.php index a3ff8bd..aa69195 100644 --- a/src/Socialbox/Objects/Database/RegisteredPeerRecord.php +++ b/src/Socialbox/Objects/Database/RegisteredPeerRecord.php @@ -292,7 +292,12 @@ */ public function toStandardPeer(): Peer { - return Peer::fromArray($this->toArray()); + return Peer::fromArray([ + 'address' => $this->getAddress(), + 'display_name' => $this->displayName, + 'flags' => array_map(fn(PeerFlags $flag) => $flag->value, $this->flags), + 'registered' => $this->created->getTimestamp() + ]); } /** From ab03439e04f67483a13f58449c11bfb738cba337 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 15 Jan 2025 15:21:44 -0500 Subject: [PATCH 201/420] Refactor peer synchronization and resolution logic. --- .../Classes/StandardMethods/ResolvePeer.php | 14 ++------- src/Socialbox/Socialbox.php | 29 +++++++++++++++++-- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/ResolvePeer.php b/src/Socialbox/Classes/StandardMethods/ResolvePeer.php index 73ce94f..a83ee69 100644 --- a/src/Socialbox/Classes/StandardMethods/ResolvePeer.php +++ b/src/Socialbox/Classes/StandardMethods/ResolvePeer.php @@ -66,21 +66,11 @@ // Otherwise, resolve the peer from the remote server try { - $client = Socialbox::getExternalSession($peerAddress->getDomain()); + return $rpcRequest->produceResponse(Socialbox::resolveExternalPeer($peerAddress)); } catch(Exception $e) { - throw new StandardException(sprintf('There was an error while trying to connect to %s: %s', $peerAddress->getDomain(), $e->getMessage()), StandardError::RESOLUTION_FAILED, $e); - } - - // Return the result/error of the resolution - try - { - return $rpcRequest->produceResponse($client->resolvePeer($peerAddress)); - } - catch(RpcException $e) - { - throw new StandardException($e->getMessage(), StandardError::tryFrom($e->getCode()) ?? StandardError::UNKNOWN, $e); + throw new StandardException(sprintf('There was an error while trying to resolve the peer %s: %s', $peerAddress, $e->getMessage()), StandardError::RESOLUTION_FAILED, $e); } } } \ No newline at end of file diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index c903f0a..27d0d04 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -28,6 +28,7 @@ use Socialbox\Managers\SessionManager; use Socialbox\Objects\ClientRequest; use Socialbox\Objects\PeerAddress; + use Socialbox\Objects\Standard\Peer; use Socialbox\Objects\Standard\ServerInformation; use Throwable; @@ -734,15 +735,21 @@ /** * Synchronizes an external peer by resolving and integrating its information into the system. * - * @param PeerAddress|string $externalPeer The external peer to synchronize, provided as a PeerAddress instance or a string. + * @param PeerAddress|Peer|string $externalPeer The external peer to synchronize, provided as a PeerAddress instance or a string. * @return void * @throws CryptographyException If there is an error in the cryptography * @throws DatabaseOperationException If there is an error while processing the peer against the database * @throws ResolutionException If the synchronization process fails due to unresolved peer information or other errors. * @throws RpcException If there is an RPC exception while connecting to the remote server */ - public static function synchronizeExternalPeer(PeerAddress|string $externalPeer): void + public static function synchronizeExternalPeer(PeerAddress|Peer|string $externalPeer): void { + if($externalPeer instanceof Peer) + { + RegisteredPeerManager::synchronizeExternalPeer($externalPeer); + return; + } + if($externalPeer instanceof PeerAddress) { $externalPeer = $externalPeer->getAddress(); @@ -752,6 +759,24 @@ RegisteredPeerManager::synchronizeExternalPeer($client->resolvePeer($externalPeer)); } + /** + * Resolves an external peer based on the given peer address or string identifier. + * + * @param PeerAddress|string $externalPeer The external peer address or string identifier to be resolved. + * @return Peer The resolved external peer after synchronization. + */ + public static function resolveExternalPeer(PeerAddress|string $externalPeer): Peer + { + if($externalPeer instanceof PeerAddress) + { + $externalPeer = $externalPeer->getAddress(); + } + + $resolvedPeer = self::getExternalSession($externalPeer->getDomain())->resolvePeer($externalPeer); + self::synchronizeExternalPeer($resolvedPeer); + return $resolvedPeer; + } + /** * Retrieves the server information by assembling data from the configuration settings. * From 9dd8c39ce14315a9bbc1ad97f34bf6b5ac168f59 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 15 Jan 2025 15:49:53 -0500 Subject: [PATCH 202/420] Refactor peer resolution logic and add sync interval setting --- src/Socialbox/Classes/Configuration.php | 4 + .../Configuration/PoliciesConfiguration.php | 12 +++ .../Classes/StandardMethods/ResolvePeer.php | 26 +------ src/Socialbox/Socialbox.php | 77 +++++++++++++++++-- 4 files changed, 88 insertions(+), 31 deletions(-) diff --git a/src/Socialbox/Classes/Configuration.php b/src/Socialbox/Classes/Configuration.php index dd07b14..8ec6a5e 100644 --- a/src/Socialbox/Classes/Configuration.php +++ b/src/Socialbox/Classes/Configuration.php @@ -153,6 +153,10 @@ // answer within the time-frame that the captcha was generated // If expired; client is expected to request for a new captcha which will generate a new random answer. $config->setDefault('policies.image_captcha_expires', 300); + // The amount of time in seconds it takes before a peer's external address is resolved again + // When a peer's external address is resolved, it is cached for this amount of time before resolving again. + // This reduces the amount of times a resolution request is made to the external server. + $config->setDefault('policies.peer_sync_interval', 3600); // Storage configuration $config->setDefault('storage.path', '/etc/socialbox'); // The main path for file storage diff --git a/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php b/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php index bf7de98..8216907 100644 --- a/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php +++ b/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php @@ -7,12 +7,14 @@ private int $maxSigningKeys; private int $sessionInactivityExpires; private int $imageCaptchaExpires; + private int $peerSyncInterval; public function __construct(array $data) { $this->maxSigningKeys = $data['max_signing_keys']; $this->sessionInactivityExpires = $data['session_inactivity_expires']; $this->imageCaptchaExpires = $data['image_captcha_expires']; + $this->peerSyncInterval = $data['peer_sync_interval']; } /** @@ -46,4 +48,14 @@ { return $this->imageCaptchaExpires; } + + /** + * Returns the maximum amount of seconds before the external peer resolve cache is considered expired + * + * @return int + */ + public function getPeerSyncInterval(): int + { + return $this->peerSyncInterval; + } } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/ResolvePeer.php b/src/Socialbox/Classes/StandardMethods/ResolvePeer.php index a83ee69..0cdc6bf 100644 --- a/src/Socialbox/Classes/StandardMethods/ResolvePeer.php +++ b/src/Socialbox/Classes/StandardMethods/ResolvePeer.php @@ -41,32 +41,10 @@ throw new StandardException('Peer Address Error: ' . $e->getMessage(), StandardError::RPC_INVALID_ARGUMENTS, $e); } - // If the requested peer resides in the server, resolve the peer internally. - if($peerAddress->getDomain() === Configuration::getInstanceConfiguration()->getDomain()) - { - try - { - $registeredPeer = RegisteredPeerManager::getPeerByAddress($peerAddress); - } - catch (DatabaseOperationException $e) - { - throw new StandardException('There was an unexpected error while trying to resolve the peer internally', StandardError::INTERNAL_SERVER_ERROR, $e); - } - - // Return not found if the returned record is null or if the registered peer isn't enabled - if($registeredPeer === null || !$registeredPeer->isEnabled()) - { - return $rpcRequest->produceError(StandardError::PEER_NOT_FOUND, sprintf('Peer %s not found', $peerAddress->getAddress())); - } - - // Return standard peer representation - return $rpcRequest->produceResponse($registeredPeer->toStandardPeer()); - } - - // Otherwise, resolve the peer from the remote server + // Resolve the peer using the server's peer resolver, this will resolve both internal peers and external peers try { - return $rpcRequest->produceResponse(Socialbox::resolveExternalPeer($peerAddress)); + return $rpcRequest->produceResponse(Socialbox::resolvePeer($peerAddress)); } catch(Exception $e) { diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index 27d0d04..c86e89e 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -762,19 +762,82 @@ /** * Resolves an external peer based on the given peer address or string identifier. * - * @param PeerAddress|string $externalPeer The external peer address or string identifier to be resolved. + * @param PeerAddress|string $peer The external peer address or string identifier to be resolved. * @return Peer The resolved external peer after synchronization. */ - public static function resolveExternalPeer(PeerAddress|string $externalPeer): Peer + public static function resolvePeer(PeerAddress|string $peer): Peer { - if($externalPeer instanceof PeerAddress) + if($peer instanceof PeerAddress) { - $externalPeer = $externalPeer->getAddress(); + $peer = $peer->getAddress(); } - $resolvedPeer = self::getExternalSession($externalPeer->getDomain())->resolvePeer($externalPeer); - self::synchronizeExternalPeer($resolvedPeer); - return $resolvedPeer; + try + { + $registeredPeer = RegisteredPeerManager::getPeerByAddress($peer); + } + catch (DatabaseOperationException $e) + { + throw new StandardException('There was an unexpected error while trying to resolve the peer internally', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + // Return not found if the peer was found but is disabled + if($registeredPeer !== null && !$registeredPeer->isEnabled()) + { + throw new StandardException('The requested peer is disabled', StandardError::PEER_NOT_FOUND); + } + + // If the peer was not found but the peer resides in an external server, resolve it + try + { + if($registeredPeer === null && $peer->getDomain() !== Configuration::getInstanceConfiguration()->getDomain()) + { + try + { + $registeredPeer = self::getExternalSession($peer->getDomain())->resolvePeer($peer); + } + catch (DatabaseOperationException $e) + { + throw new StandardException('There was an unexpected error while trying to resolve the peer externally: ' . $e->getMessage(), StandardError::RESOLUTION_FAILED, $e); + } + + // Synchronize the peer for future use + self::synchronizeExternalPeer($registeredPeer); + } + // If the peer was found and the peer does reside in an external server, re-resolve it if necessary + elseif($registeredPeer !== null && $peer->getDomain() !== Configuration::getInstanceConfiguration()->getDomain()) + { + if($registeredPeer->getUpdated()->getTimestamp() < time() - Configuration::getPoliciesConfiguration()->getPeerSyncInterval()) + { + try + { + $registeredPeer = self::getExternalSession($peer->getDomain())->resolvePeer($peer); + } + catch (DatabaseOperationException $e) + { + throw new StandardException('There was an unexpected error while trying to resolve the peer externally: ' . $e->getMessage(), StandardError::RESOLUTION_FAILED, $e); + } + + // Synchronize the peer for future use + self::synchronizeExternalPeer($registeredPeer); + } + } + } + catch(StandardException $e) + { + throw $e; + } + catch(Exception $e) + { + throw new StandardException('Failed to resolve the peer: ' . $e->getMessage(), StandardError::RESOLUTION_FAILED, $e); + } + + if($registeredPeer === null) + { + throw new StandardException('The requested peer was not found', StandardError::PEER_NOT_FOUND); + } + + return $registeredPeer; } /** From 97757bec5f6235f0ea75cae3c4fdd4e7f9d61c66 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 15 Jan 2025 19:21:20 -0500 Subject: [PATCH 203/420] Enable internal exception display in .env configuration --- .env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env b/.env index d86c46b..b6e15c4 100644 --- a/.env +++ b/.env @@ -6,7 +6,7 @@ SB_INSTANCE_NAME=Socialbox SB_RPC_ENDPOINT=http://127.0.0.0:8085/ SB_LOGGING_CONSOLE_ENABLED=true SB_LOGGING_CONSOLE_LEVEL=info -SB_SECURITY_DISPLAY_INTERNAL_EXCEPTIONS=false +SB_SECURITY_DISPLAY_INTERNAL_EXCEPTIONS=true SB_CRYPTO_KEYPAIR_EXPIRES= SB_CRYPTO_ENCRYPTION_KEYS_ALGORITHM=xchacha20 SB_CRYPTO_TRANSPORT_ENCRYPTION_ALGORITHM=chacha20 From 5afe4429edd51cf3f7d3bc6db5e0b42b5949116c Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 15 Jan 2025 19:21:37 -0500 Subject: [PATCH 204/420] Add `peer_sync_interval` configuration to socialbox policies --- coffee_socialbox/config/socialbox.conf | 3 ++- teapot_socialbox/config/socialbox.conf | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/coffee_socialbox/config/socialbox.conf b/coffee_socialbox/config/socialbox.conf index e23c2f6..e2baf17 100755 --- a/coffee_socialbox/config/socialbox.conf +++ b/coffee_socialbox/config/socialbox.conf @@ -93,7 +93,8 @@ "policies": { "max_signing_keys": 20, "session_inactivity_expires": 43200, - "image_captcha_expires": 300 + "image_captcha_expires": 300, + "peer_sync_interval": 3600 }, "storage": { "path": "/etc/socialbox", diff --git a/teapot_socialbox/config/socialbox.conf b/teapot_socialbox/config/socialbox.conf index e81d5b6..44c4cdb 100755 --- a/teapot_socialbox/config/socialbox.conf +++ b/teapot_socialbox/config/socialbox.conf @@ -93,7 +93,8 @@ "policies": { "max_signing_keys": 20, "session_inactivity_expires": 43200, - "image_captcha_expires": 300 + "image_captcha_expires": 300, + "peer_sync_interval": 3600 }, "storage": { "path": "/etc/socialbox", From a3456bdeed637a6b1962d12aa5f181cfcf7a1792 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 22 Jan 2025 13:18:55 -0500 Subject: [PATCH 205/420] Added LogLib2 support --- .idea/php.xml | 1 + Dockerfile | 1 + docker-compose.test.yml | 7 ++ project.json | 4 +- .../Classes/CliCommands/InitializeCommand.php | 113 +++++++++--------- src/Socialbox/Classes/Logger.php | 37 +++--- src/Socialbox/Program.php | 19 +++ 7 files changed, 102 insertions(+), 80 deletions(-) diff --git a/.idea/php.xml b/.idea/php.xml index acc3054..93636e7 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -17,6 +17,7 @@ + diff --git a/Dockerfile b/Dockerfile index 774fb36..18bc6ee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,6 +45,7 @@ RUN docker-php-ext-install -j$(nproc) \ curl \ opcache \ zip \ + sockets \ pcntl && \ pecl install redis memcached && \ docker-php-ext-enable redis memcached diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 53b0297..90e1203 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -61,6 +61,10 @@ services: # the instance name and the suffix being used to detect the TXT record SB_INSTANCE_DNS_MOCK_COFFEE: ${SB_INSTANCE_DNS_MOCK_COFFEE:-"coffee.com v=socialbox;sb-rpc=http://coffee_socialbox:8085/;sb-key=sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc;sb-exp=0"} SB_INSTANCE_DNS_MOCK_TEAPOT: ${SB_INSTANCE_DNS_MOCK_TEAPOT:-"teapot.com v=socialbox;sb-rpc=http://teapot_socialbox:8085/;sb-key=sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY;sb-exp=0"} + + LOGLIB_UDP_ENABLED: true + LOGLIB_UDP_HOST: 10.0.0.109 + LOGLIB_UDP_PORT: 5131 healthcheck: test: ["CMD", "curl", "-f", "-H", "Request-Type: ping", "${SB_INSTANCE_RPC_ENDPOINT-http://coffee_socialbox:8085/}"] interval: 30s @@ -166,6 +170,9 @@ services: # the instance name and the suffix being used to detect the TXT record SB_INSTANCE_DNS_MOCK_COFFEE: ${SB_INSTANCE_DNS_MOCK_COFFEE:-"coffee.com v=socialbox;sb-rpc=http://coffee_socialbox:8085/;sb-key=sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc;sb-exp=0"} SB_INSTANCE_DNS_MOCK_TEAPOT: ${SB_INSTANCE_DNS_MOCK_TEAPOT:-"teapot.com v=socialbox;sb-rpc=http://teapot_socialbox:8085/;sb-key=sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY;sb-exp=0"} + LOGLIB_UDP_ENABLED: true + LOGLIB_UDP_HOST: 10.0.0.109 + LOGLIB_UDP_PORT: 5131 healthcheck: test: ["CMD", "curl", "-f", "-H", "Request-Type: ping", "${SB_INSTANCE_RPC_ENDPOINT-http://teapot_socialbox:8085/}"] interval: 30s diff --git a/project.json b/project.json index 19c0bbc..06b6fd0 100644 --- a/project.json +++ b/project.json @@ -36,9 +36,9 @@ "source": "nosial/libs.config=latest@n64" }, { - "name": "net.nosial.loglib", + "name": "net.nosial.loglib2", "version": "latest", - "source": "nosial/libs.log=latest@n64" + "source": "nosial/loglib2=latest@github" }, { "name": "net.nosial.optslib", diff --git a/src/Socialbox/Classes/CliCommands/InitializeCommand.php b/src/Socialbox/Classes/CliCommands/InitializeCommand.php index 5cd7d31..89de809 100644 --- a/src/Socialbox/Classes/CliCommands/InitializeCommand.php +++ b/src/Socialbox/Classes/CliCommands/InitializeCommand.php @@ -3,18 +3,19 @@ namespace Socialbox\Classes\CliCommands; use Exception; + use ncc\CLI\Main; use ncc\ThirdParty\Symfony\Process\Exception\InvalidArgumentException; use PDOException; use Socialbox\Abstracts\CacheLayer; use Socialbox\Classes\Configuration; use Socialbox\Classes\Cryptography; use Socialbox\Classes\Database; - use Socialbox\Classes\DnsHelper; use Socialbox\Classes\Logger; use Socialbox\Classes\Resources; use Socialbox\Enums\DatabaseObjects; use Socialbox\Exceptions\CryptographyException; use Socialbox\Interfaces\CliCommandInterface; + use Socialbox\Program; use Socialbox\Socialbox; class InitializeCommand implements CliCommandInterface @@ -31,30 +32,30 @@ 'instance.enabled', 'instance.domain', 'registration.*' ]; - Logger::getLogger()->error('Socialbox is disabled. Use --force to initialize the instance or set `instance.enabled` to True in the configuration'); - Logger::getLogger()->info('The reason you are required to do this is to allow you to configure the instance before enabling it'); - Logger::getLogger()->info('The following configurations are required to be set before enabling the instance:'); + Program::getLogger()->error('Socialbox is disabled. Use --force to initialize the instance or set `instance.enabled` to True in the configuration'); + Program::getLogger()->info('The reason you are required to do this is to allow you to configure the instance before enabling it'); + Program::getLogger()->info('The following configurations are required to be set before enabling the instance:'); foreach($required_configurations as $config) { - Logger::getLogger()->info(sprintf(' - %s', $config)); + Program::getLogger()->info(sprintf(' - %s', $config)); } - Logger::getLogger()->info('instance.private_key & instance.public_key are automatically generated if not set'); - Logger::getLogger()->info('instance.domain is required to be set to the domain name of the instance'); - Logger::getLogger()->info('instance.rpc_endpoint is required to be set to the publicly accessible http rpc endpoint of this server'); - Logger::getLogger()->info('registration.* are required to be set to allow users to register to the instance'); - Logger::getLogger()->info('You will be given a DNS TXT record to set for the public key after the initialization process'); - Logger::getLogger()->info('The configuration file can be edited using ConfigLib:'); - Logger::getLogger()->info(' configlib --conf socialbox -e nano'); - Logger::getLogger()->info('Or manually at:'); - Logger::getLogger()->info(sprintf(' %s', Configuration::getConfigurationLib()->getPath())); + Program::getLogger()->info('instance.private_key & instance.public_key are automatically generated if not set'); + Program::getLogger()->info('instance.domain is required to be set to the domain name of the instance'); + Program::getLogger()->info('instance.rpc_endpoint is required to be set to the publicly accessible http rpc endpoint of this server'); + Program::getLogger()->info('registration.* are required to be set to allow users to register to the instance'); + Program::getLogger()->info('You will be given a DNS TXT record to set for the public key after the initialization process'); + Program::getLogger()->info('The configuration file can be edited using ConfigLib:'); + Program::getLogger()->info(' configlib --conf socialbox -e nano'); + Program::getLogger()->info('Or manually at:'); + Program::getLogger()->info(sprintf(' %s', Configuration::getConfigurationLib()->getPath())); if(getenv('SB_MODE') === 'automated') { // Wait & Reload the configuration while(!Configuration::getInstanceConfiguration()->isEnabled()) { - Logger::getLogger()->info('Waiting for configuration, retrying in 5 seconds...'); + Program::getLogger()->info('Waiting for configuration, retrying in 5 seconds...'); sleep(5); Configuration::reload(); } @@ -71,32 +72,32 @@ // This is useful for CI/CD pipelines & Docker if(getenv('SB_MODE') === 'automated') { - Logger::getLogger()->info('Automated Setup Procedure is detected'); + Program::getLogger()->info('Automated Setup Procedure is detected'); self::applyEnvironmentVariables(); } if(Configuration::getInstanceConfiguration()->getDomain() === null) { - Logger::getLogger()->error('instance.domain is required but was not set'); + Program::getLogger()->error('instance.domain is required but was not set'); return 1; } if(Configuration::getInstanceConfiguration()->getRpcEndpoint() === null) { - Logger::getLogger()->error('instance.rpc_endpoint is required but was not set'); + Program::getLogger()->error('instance.rpc_endpoint is required but was not set'); return 1; } - Logger::getLogger()->info('Initializing Socialbox...'); + Program::getLogger()->info('Initializing Socialbox...'); if(Configuration::getCacheConfiguration()->isEnabled()) { - Logger::getLogger()->verbose('Clearing cache layer...'); + Program::getLogger()->verbose('Clearing cache layer...'); CacheLayer::getInstance()->clear(); } foreach(DatabaseObjects::casesOrdered() as $object) { - Logger::getLogger()->verbose("Initializing database object {$object->value}"); + Program::getLogger()->verbose("Initializing database object {$object->value}"); try { @@ -107,18 +108,18 @@ // Check if the error code is for "table already exists" if ($e->getCode() === '42S01') { - Logger::getLogger()->warning("Database object {$object->value} already exists, skipping..."); + Program::getLogger()->warning("Database object {$object->value} already exists, skipping..."); continue; } else { - Logger::getLogger()->error("Failed to initialize database object {$object->value}: {$e->getMessage()}", $e); + Program::getLogger()->error("Failed to initialize database object {$object->value}: {$e->getMessage()}", $e); return 1; } } catch(Exception $e) { - Logger::getLogger()->error("Failed to initialize database object {$object->value}: {$e->getMessage()}", $e); + Program::getLogger()->error("Failed to initialize database object {$object->value}: {$e->getMessage()}", $e); return 1; } } @@ -133,12 +134,12 @@ try { - Logger::getLogger()->info('Generating new key pair (expires ' . date('Y-m-d H:i:s', $expires) . ')...'); + Program::getLogger()->info('Generating new key pair (expires ' . date('Y-m-d H:i:s', $expires) . ')...'); $signingKeyPair = Cryptography::generateSigningKeyPair(); } catch (CryptographyException $e) { - Logger::getLogger()->error('Failed to generate cryptography values', $e); + Program::getLogger()->error('Failed to generate cryptography values', $e); return 1; } @@ -153,7 +154,7 @@ Configuration::getCryptographyConfiguration()->getInternalEncryptionKeys() === null || count(Configuration::getCryptographyConfiguration()->getInternalEncryptionKeys()) < Configuration::getCryptographyConfiguration()->getEncryptionKeysCount()) { - Logger::getLogger()->info('Generating internal encryption keys...'); + Program::getLogger()->info('Generating internal encryption keys...'); $encryptionKeys = Configuration::getCryptographyConfiguration()->getInternalEncryptionKeys() ?? []; while(count($encryptionKeys) < Configuration::getCryptographyConfiguration()->getEncryptionKeysCount()) { @@ -163,13 +164,13 @@ Configuration::getConfigurationLib()->set('cryptography.internal_encryption_keys', $encryptionKeys); } - Logger::getLogger()->info('Updating configuration...'); + Program::getLogger()->info('Updating configuration...'); Configuration::getConfigurationLib()->save(); Configuration::reload(); - Logger::getLogger()->info('Socialbox has been initialized successfully'); - Logger::getLogger()->info(sprintf('Set the DNS TXT record for the domain %s to the following value:', Configuration::getInstanceConfiguration()->getDomain())); - Logger::getLogger()->info(Socialbox::getDnsRecord()); + Program::getLogger()->info('Socialbox has been initialized successfully'); + Program::getLogger()->info(sprintf('Set the DNS TXT record for the domain %s to the following value:', Configuration::getInstanceConfiguration()->getDomain())); + Program::getLogger()->info(Socialbox::getDnsRecord()); return 0; } @@ -227,7 +228,7 @@ foreach($configurationMap as $env => $config) { $variable = getenv($env); - Logger::getLogger()->info(sprintf('Checking environment variable %s...', $env)); + Program::getLogger()->info(sprintf('Checking environment variable %s...', $env)); switch($env) { @@ -245,91 +246,91 @@ if($variable !== false) { Configuration::getConfigurationLib()->set($config, $variable); - Logger::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); + Program::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); } break; case 'SB_INSTANCE_DOMAIN': if($variable === false && Configuration::getInstanceConfiguration()->getDomain() === null) { - Logger::getLogger()->warning(sprintf('%s is not set, expected %s environment variable', $config, $env)); + Program::getLogger()->warning(sprintf('%s is not set, expected %s environment variable', $config, $env)); } else { Configuration::getConfigurationLib()->set($config, $variable); - Logger::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); + Program::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); } break; case 'SB_DATABASE_HOST': if($variable === false && Configuration::getDatabaseConfiguration()->getHost() === null) { - Logger::getLogger()->warning(sprintf('%s is not set, expected %s environment variable', $config, $env)); + Program::getLogger()->warning(sprintf('%s is not set, expected %s environment variable', $config, $env)); } else { Configuration::getConfigurationLib()->set($config, $variable); - Logger::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); + Program::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); } break; case 'SB_DATABASE_PORT': if($variable === false && Configuration::getDatabaseConfiguration()->getPort() === null) { - Logger::getLogger()->warning(sprintf('%s is not set, expected %s environment variable', $config, $env)); + Program::getLogger()->warning(sprintf('%s is not set, expected %s environment variable', $config, $env)); } else { Configuration::getConfigurationLib()->set($config, (int) $variable); - Logger::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); + Program::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); } break; case 'SB_DATABASE_USERNAME': if($variable === false && Configuration::getDatabaseConfiguration()->getUsername() === null) { - Logger::getLogger()->warning(sprintf('%s is not set, expected %s environment variable', $config, $env)); + Program::getLogger()->warning(sprintf('%s is not set, expected %s environment variable', $config, $env)); } else { Configuration::getConfigurationLib()->set($config, $variable); - Logger::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); + Program::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); } break; case 'SB_DATABASE_PASSWORD': if($variable === false && Configuration::getDatabaseConfiguration()->getPassword() === null) { - Logger::getLogger()->warning(sprintf('%s is not set, expected %s environment variable', $config, $env)); + Program::getLogger()->warning(sprintf('%s is not set, expected %s environment variable', $config, $env)); } else { Configuration::getConfigurationLib()->set($config, $variable); - Logger::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); + Program::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); } break; case 'SB_DATABASE_NAME': if($variable === false && Configuration::getDatabaseConfiguration()->getName() === null) { - Logger::getLogger()->warning(sprintf('%s is not set, expected %s environment variable', $config, $env)); + Program::getLogger()->warning(sprintf('%s is not set, expected %s environment variable', $config, $env)); } else { Configuration::getConfigurationLib()->set($config, $variable); - Logger::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); + Program::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); } break; case 'SB_INSTANCE_RPC_ENDPOINT': if($variable === false && Configuration::getInstanceConfiguration()->getRpcEndpoint() === null) { - Logger::getLogger()->warning(sprintf('%s is not set, expected %s environment variable', $config, $env)); + Program::getLogger()->warning(sprintf('%s is not set, expected %s environment variable', $config, $env)); } else { Configuration::getConfigurationLib()->set($config, $variable); - Logger::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); + Program::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); } break; @@ -340,7 +341,7 @@ if($variable !== false) { Configuration::getConfigurationLib()->set($config, filter_var($variable, FILTER_VALIDATE_BOOLEAN)); - Logger::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); + Program::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); } break; @@ -350,12 +351,12 @@ if($variable !== false) { Configuration::getConfigurationLib()->set($config, (int) $variable); - Logger::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); + Program::getLogger()->info(sprintf('Set %s to %s', $config, $variable)); } break; default: - Logger::getLogger()->warning("Environment variable $env is not supported"); + Program::getLogger()->warning("Environment variable $env is not supported"); break; } } @@ -367,7 +368,7 @@ $mockServer = explode(' ', $mockServer); if(count($mockServer) !== 2) { - Logger::getLogger()->warning(sprintf('Invalid DNS Mock Server format: %s', implode(' ', $mockServer))); + Program::getLogger()->warning(sprintf('Invalid DNS Mock Server format: %s', implode(' ', $mockServer))); continue; } @@ -375,30 +376,30 @@ $txt = $mockServer[1] ?? null; if($domain === null || $txt === null) { - Logger::getLogger()->warning(sprintf('Invalid DNS Mock Server format, domain or txt missing: %s', implode(' ', $mockServer))); + Program::getLogger()->warning(sprintf('Invalid DNS Mock Server format, domain or txt missing: %s', implode(' ', $mockServer))); continue; } try { $mockServers[$domain] = $txt; - Logger::getLogger()->info(sprintf('Added Mock Server %s: %s', $domain, $txt)); + Program::getLogger()->info(sprintf('Added Mock Server %s: %s', $domain, $txt)); } catch(InvalidArgumentException $e) { - Logger::getLogger()->error(sprintf('Invalid TXT record format for %s', $domain), $e); + Program::getLogger()->error(sprintf('Invalid TXT record format for %s', $domain), $e); continue; } } if(count($mockServers) > 0) { - Logger::getLogger()->info('Setting Mock Servers...'); + Program::getLogger()->info('Setting Mock Servers...'); Configuration::getConfigurationLib()->set('instance.dns_mocks', $mockServers); } // Apply changes & reload the configuration - Logger::getLogger()->info('Updating configuration...'); + Program::getLogger()->info('Updating configuration...'); Configuration::getConfigurationLib()->save(); // Save Configuration::reload(); // Reload } diff --git a/src/Socialbox/Classes/Logger.php b/src/Socialbox/Classes/Logger.php index 90e4ab2..9fe20b1 100644 --- a/src/Socialbox/Classes/Logger.php +++ b/src/Socialbox/Classes/Logger.php @@ -1,30 +1,23 @@ setConsoleLoggingEnabled(Configuration::getLoggingConfiguration()->isConsoleLoggingEnabled()); - self::$logger->setConsoleLoggingLevel(Configuration::getLoggingConfiguration()->getConsoleLoggingLevel()); - self::$logger->setFileLoggingEnabled(Configuration::getLoggingConfiguration()->isFileLoggingEnabled()); - self::$logger->setFileLoggingLevel(Configuration::getLoggingConfiguration()->getFileLoggingLevel()); + if(self::$logger === null) + { + self::$logger = new \LogLib2\Logger(Configuration::getInstanceConfiguration()->getName()); + \LogLib2\Logger::registerHandlers(); + } - Log::registerExceptionHandler(); - Log::register(self::$logger); + return self::$logger; } - - return self::$logger; - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/src/Socialbox/Program.php b/src/Socialbox/Program.php index 198c953..af22cb3 100644 --- a/src/Socialbox/Program.php +++ b/src/Socialbox/Program.php @@ -3,10 +3,13 @@ namespace Socialbox; use OptsLib\Parse; + use Socialbox\Classes\Logger; use Socialbox\Enums\CliCommands; class Program { + private static ?\LogLib2\Logger $logger; + /** * Socialbox main entry point * @@ -73,4 +76,20 @@ return 0; } + + /** + * Retrieves the logger instance for the Socialbox program. + * + * @return \LogLib2\Logger Returns the logger instance. + */ + public static function getLogger(): \LogLib2\Logger + { + if(self::$logger === null) + { + self::$logger = new \LogLib2\Logger('net.nosial.socialbox'); + \LogLib2\Logger::registerHandlers(); + } + + return self::$logger; + } } \ No newline at end of file From e41a7867b42b32a1e3997ca8ddb139a3d4d4e484 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 22 Jan 2025 14:22:28 -0500 Subject: [PATCH 206/420] Added CLI logger --- src/Socialbox/Program.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Socialbox/Program.php b/src/Socialbox/Program.php index af22cb3..a73490d 100644 --- a/src/Socialbox/Program.php +++ b/src/Socialbox/Program.php @@ -2,13 +2,13 @@ namespace Socialbox; + use LogLib2\Logger; use OptsLib\Parse; - use Socialbox\Classes\Logger; use Socialbox\Enums\CliCommands; class Program { - private static ?\LogLib2\Logger $logger; + private static ?Logger $logger=null; /** * Socialbox main entry point @@ -80,14 +80,14 @@ /** * Retrieves the logger instance for the Socialbox program. * - * @return \LogLib2\Logger Returns the logger instance. + * @return Logger Returns the logger instance. */ - public static function getLogger(): \LogLib2\Logger + public static function getLogger(): Logger { if(self::$logger === null) { - self::$logger = new \LogLib2\Logger('net.nosial.socialbox'); - \LogLib2\Logger::registerHandlers(); + self::$logger = new Logger('net.nosial.socialbox'); + Logger::registerHandlers(); } return self::$logger; From 9bf702534488d479cf6e780ffcc8601b7d93c354 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 22 Jan 2025 14:22:51 -0500 Subject: [PATCH 207/420] Updated documentation for UDP logging & Changed host to shared docker host --- docker-compose.test.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 90e1203..3f339e8 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -61,9 +61,10 @@ services: # the instance name and the suffix being used to detect the TXT record SB_INSTANCE_DNS_MOCK_COFFEE: ${SB_INSTANCE_DNS_MOCK_COFFEE:-"coffee.com v=socialbox;sb-rpc=http://coffee_socialbox:8085/;sb-key=sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc;sb-exp=0"} SB_INSTANCE_DNS_MOCK_TEAPOT: ${SB_INSTANCE_DNS_MOCK_TEAPOT:-"teapot.com v=socialbox;sb-rpc=http://teapot_socialbox:8085/;sb-key=sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY;sb-exp=0"} - + # UDP Logging, won't cause issues if the server is not available + # See https://github.com/nosial/LogLib2/blob/master/server.py for more information LOGLIB_UDP_ENABLED: true - LOGLIB_UDP_HOST: 10.0.0.109 + LOGLIB_UDP_HOST: 172.17.0.1 LOGLIB_UDP_PORT: 5131 healthcheck: test: ["CMD", "curl", "-f", "-H", "Request-Type: ping", "${SB_INSTANCE_RPC_ENDPOINT-http://coffee_socialbox:8085/}"] @@ -170,8 +171,10 @@ services: # the instance name and the suffix being used to detect the TXT record SB_INSTANCE_DNS_MOCK_COFFEE: ${SB_INSTANCE_DNS_MOCK_COFFEE:-"coffee.com v=socialbox;sb-rpc=http://coffee_socialbox:8085/;sb-key=sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc;sb-exp=0"} SB_INSTANCE_DNS_MOCK_TEAPOT: ${SB_INSTANCE_DNS_MOCK_TEAPOT:-"teapot.com v=socialbox;sb-rpc=http://teapot_socialbox:8085/;sb-key=sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY;sb-exp=0"} + # UDP Logging, won't cause issues if the server is not available + # See https://github.com/nosial/LogLib2/blob/master/server.py for more information LOGLIB_UDP_ENABLED: true - LOGLIB_UDP_HOST: 10.0.0.109 + LOGLIB_UDP_HOST: 172.17.0.1 LOGLIB_UDP_PORT: 5131 healthcheck: test: ["CMD", "curl", "-f", "-H", "Request-Type: ping", "${SB_INSTANCE_RPC_ENDPOINT-http://teapot_socialbox:8085/}"] From ad1e8190947a7126b5f96fa3422a0019bb601b68 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 22 Jan 2025 15:04:41 -0500 Subject: [PATCH 208/420] Type error fixes --- src/Socialbox/Socialbox.php | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index c86e89e..3d68ab5 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -27,6 +27,7 @@ use Socialbox\Managers\RegisteredPeerManager; use Socialbox\Managers\SessionManager; use Socialbox\Objects\ClientRequest; + use Socialbox\Objects\Database\RegisteredPeerRecord; use Socialbox\Objects\PeerAddress; use Socialbox\Objects\Standard\Peer; use Socialbox\Objects\Standard\ServerInformation; @@ -762,19 +763,19 @@ /** * Resolves an external peer based on the given peer address or string identifier. * - * @param PeerAddress|string $peer The external peer address or string identifier to be resolved. + * @param PeerAddress|string $peerAddress The external peer address or string identifier to be resolved. * @return Peer The resolved external peer after synchronization. */ - public static function resolvePeer(PeerAddress|string $peer): Peer + public static function resolvePeer(PeerAddress|string $peerAddress): Peer { - if($peer instanceof PeerAddress) + if(is_string($peerAddress)) { - $peer = $peer->getAddress(); + $peerAddress = PeerAddress::fromAddress($peerAddress); } try { - $registeredPeer = RegisteredPeerManager::getPeerByAddress($peer); + $registeredPeer = RegisteredPeerManager::getPeerByAddress($peerAddress); } catch (DatabaseOperationException $e) { @@ -790,11 +791,11 @@ // If the peer was not found but the peer resides in an external server, resolve it try { - if($registeredPeer === null && $peer->getDomain() !== Configuration::getInstanceConfiguration()->getDomain()) + if($registeredPeer === null && $peerAddress->getDomain() !== Configuration::getInstanceConfiguration()->getDomain()) { try { - $registeredPeer = self::getExternalSession($peer->getDomain())->resolvePeer($peer); + $registeredPeer = self::getExternalSession($peerAddress->getDomain())->resolvePeer($peerAddress); } catch (DatabaseOperationException $e) { @@ -805,13 +806,13 @@ self::synchronizeExternalPeer($registeredPeer); } // If the peer was found and the peer does reside in an external server, re-resolve it if necessary - elseif($registeredPeer !== null && $peer->getDomain() !== Configuration::getInstanceConfiguration()->getDomain()) + elseif($registeredPeer !== null && $peerAddress->getDomain() !== Configuration::getInstanceConfiguration()->getDomain()) { if($registeredPeer->getUpdated()->getTimestamp() < time() - Configuration::getPoliciesConfiguration()->getPeerSyncInterval()) { try { - $registeredPeer = self::getExternalSession($peer->getDomain())->resolvePeer($peer); + $registeredPeer = self::getExternalSession($peerAddress->getDomain())->resolvePeer($peerAddress); } catch (DatabaseOperationException $e) { @@ -837,6 +838,11 @@ throw new StandardException('The requested peer was not found', StandardError::PEER_NOT_FOUND); } + if($registeredPeer instanceof RegisteredPeerRecord) + { + $registeredPeer = $registeredPeer->toStandardPeer(); + } + return $registeredPeer; } From bb3e0a5ffaca1837204cff00f3cbf3800d9e074b Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 22 Jan 2025 15:35:35 -0500 Subject: [PATCH 209/420] Updated PhpDoc, added method ADDRESS_BOOK_ADD_CONTACT, minor correction in getRegistrationMethods where $methods was not initialized --- .../StandardMethods/AddressBookAddContact.php | 75 +++++++++++++++++++ src/Socialbox/Enums/StandardMethods.php | 14 +++- 2 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 src/Socialbox/Classes/StandardMethods/AddressBookAddContact.php diff --git a/src/Socialbox/Classes/StandardMethods/AddressBookAddContact.php b/src/Socialbox/Classes/StandardMethods/AddressBookAddContact.php new file mode 100644 index 0000000..39b70d6 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/AddressBookAddContact.php @@ -0,0 +1,75 @@ +containsParameter('peer')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing required \'peer\' parameter'); + } + + try + { + $address = PeerAddress::fromAddress($rpcRequest->getParameter('peer')); + } + catch(InvalidArgumentException $e) + { + throw new StandardException('Invalid peer address', StandardError::RPC_INVALID_ARGUMENTS, $e); + } + + if($rpcRequest->containsParameter('relationship')) + { + $relationship = ContactRelationshipType::tryFrom(strtoupper($rpcRequest->getParameter('relationship'))); + if($relationship === null) + { + throw new StandardException('Invalid relationship type', StandardError::RPC_INVALID_ARGUMENTS); + } + } + else + { + $relationship = ContactRelationshipType::MUTUAL; + } + + try + { + // Resolve the peer, this would throw a StandardException if something goes wrong + Socialbox::resolvePeer($address); + + // Check if the contact already exists + $peer = $request->getPeer(); + if(ContactManager::isContact($peer, $address)) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'Contact already exists'); + } + + // Create the contact + ContactManager::createContact($peer, $address, $relationship); + } + catch (DatabaseOperationException $e) + { + throw new StandardException('Failed to add contact', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + // Return success + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index c4e8cf9..403fd03 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -6,6 +6,7 @@ use Socialbox\Classes\StandardMethods\AcceptCommunityGuidelines; use Socialbox\Classes\StandardMethods\AcceptPrivacyPolicy; use Socialbox\Classes\StandardMethods\AcceptTermsOfService; + use Socialbox\Classes\StandardMethods\AddressBookAddContact; use Socialbox\Classes\StandardMethods\Authenticate; use Socialbox\Classes\StandardMethods\GetAllowedMethods; use Socialbox\Classes\StandardMethods\GetCommunityGuidelines; @@ -96,6 +97,8 @@ case SETTINGS_ADD_SIGNING_KEY = 'settingsAddSigningKey'; case SETTINGS_GET_SIGNING_KEYS = 'settingsGetSigningKeys'; + case ADDRESS_BOOK_ADD_CONTACT = 'addressBookAddContact'; + case AUTHENTICATE = 'authenticate'; case RESOLVE_PEER = 'resolvePeer'; @@ -145,6 +148,8 @@ self::SETTINGS_ADD_SIGNING_KEY => SettingsAddSigningKey::execute($request, $rpcRequest), self::SETTINGS_GET_SIGNING_KEYS => SettingsGetSigningKeys::execute($request, $rpcRequest), + self::ADDRESS_BOOK_ADD_CONTACT => AddressBookAddContact::execute($request, $rpcRequest), + self::AUTHENTICATE => Authenticate::execute($request, $rpcRequest), self::RESOLVE_PEER => ResolvePeer::execute($request, $rpcRequest), @@ -234,7 +239,8 @@ /** * Retrieves a list of external methods based on the client's session state. * - * @param ClientRequest + * @param ClientRequest $clientRequest The client request object containing all the request parameters + * @return array Returns an array methods that are available for external sessions */ private static function getExternalMethods(ClientRequest $clientRequest): array { @@ -273,7 +279,9 @@ self::SETTINGS_SET_EMAIL, self::SETTINGS_SET_PHONE, self::SETTINGS_SET_BIRTHDAY, - self::RESOLVE_PEER + self::RESOLVE_PEER, + + self::ADDRESS_BOOK_ADD_CONTACT ]; // Prevent the user from deleting their display name if it is required @@ -335,6 +343,8 @@ return []; } + $methods = []; + // If the flag `VER_PRIVACY_POLICY` is set, then the user can accept the privacy policy if($session->flagExists(SessionFlags::VER_PRIVACY_POLICY)) { From 3bb5f2788eb3bf9d68e62031c0d2989dd4500600 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 22 Jan 2025 15:36:01 -0500 Subject: [PATCH 210/420] Updated PhpDocs --- src/Socialbox/Socialbox.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index 3d68ab5..133665d 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -41,9 +41,6 @@ * missing or invalid request types. * * @return void - * @throws CryptographyException - * @throws DatabaseOperationException - * @throws ResolutionException */ public static function handleRequest(): void { @@ -765,6 +762,7 @@ * * @param PeerAddress|string $peerAddress The external peer address or string identifier to be resolved. * @return Peer The resolved external peer after synchronization. + * @throws StandardException Thrown if there was an error with the resolution process */ public static function resolvePeer(PeerAddress|string $peerAddress): Peer { From 2933b5435f5300276e2fb7a969d7e13abf5af25e Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 22 Jan 2025 15:36:12 -0500 Subject: [PATCH 211/420] Updated Formatting --- src/Socialbox/Managers/ContactManager.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Socialbox/Managers/ContactManager.php b/src/Socialbox/Managers/ContactManager.php index 660e289..dc00f46 100644 --- a/src/Socialbox/Managers/ContactManager.php +++ b/src/Socialbox/Managers/ContactManager.php @@ -239,7 +239,7 @@ * @return array An array of ContactRecord instances representing the contacts for the given peer. * @throws DatabaseOperationException If the database query fails. */ - public static function getContacts(string $peerUuid, int $limit = 100, int $page = 1): array + public static function getContacts(string $peerUuid, int $limit=100, int $page=1): array { if ($page < 1) { @@ -252,9 +252,10 @@ } $contacts = []; + try { - $statement = Database::getConnection()->prepare("SELECT * FROM contacts WHERE peer_uuid = :peer ORDER BY created DESC LIMIT :limit OFFSET :offset"); + $statement = Database::getConnection()->prepare("SELECT * FROM contacts WHERE peer_uuid=:peer ORDER BY created DESC LIMIT :limit OFFSET :offset"); $offset = ($page - 1) * $limit; $statement->bindParam(':peer', $peerUuid); $statement->bindParam(':limit', $limit, PDO::PARAM_INT); From 0d09e77d59afe82aebcb61cd5172484495a62f9e Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 22 Jan 2025 15:36:19 -0500 Subject: [PATCH 212/420] Updated PhpDocs --- src/Socialbox/Objects/ClientRequest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Socialbox/Objects/ClientRequest.php b/src/Socialbox/Objects/ClientRequest.php index d822d35..d9f957a 100644 --- a/src/Socialbox/Objects/ClientRequest.php +++ b/src/Socialbox/Objects/ClientRequest.php @@ -6,6 +6,7 @@ use Socialbox\Enums\StandardHeaders; use Socialbox\Enums\Types\RequestType; use Socialbox\Exceptions\CryptographyException; + use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\RequestException; use Socialbox\Managers\RegisteredPeerManager; use Socialbox\Managers\SessionManager; @@ -182,6 +183,7 @@ * Retrieves the associated peer for the current session. * * @return RegisteredPeerRecord|null Returns the associated RegisteredPeerRecord if available, or null if no session exists. + * @throws DatabaseOperationException Thrown if an error occurs while retrieving the peer. */ public function getPeer(): ?RegisteredPeerRecord { From aa445c7bdd4a31d698e62f3c4b0fe5749ee12f39 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 22 Jan 2025 15:40:50 -0500 Subject: [PATCH 213/420] Added method ADDRESS_BOOK_DELETE_CONTACT --- .../AddressBookDeleteContact.php | 59 +++++++++++++++++++ src/Socialbox/Enums/StandardMethods.php | 6 +- 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 src/Socialbox/Classes/StandardMethods/AddressBookDeleteContact.php diff --git a/src/Socialbox/Classes/StandardMethods/AddressBookDeleteContact.php b/src/Socialbox/Classes/StandardMethods/AddressBookDeleteContact.php new file mode 100644 index 0000000..9ec51a9 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/AddressBookDeleteContact.php @@ -0,0 +1,59 @@ +containsParameter('peer')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing required \'peer\' parameter'); + } + + try + { + $address = PeerAddress::fromAddress($rpcRequest->getParameter('peer')); + } + catch(InvalidArgumentException $e) + { + throw new StandardException('Invalid peer address', StandardError::RPC_INVALID_ARGUMENTS, $e); + } + + try + { + // Check if the contact already exists + $peer = $request->getPeer(); + if(!ContactManager::isContact($peer, $address)) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'Contact does not exist'); + } + + // Create the contact + ContactManager::deleteContact($peer, $address); + } + catch (DatabaseOperationException $e) + { + throw new StandardException('Failed to remove contact', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + // Return success + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index 403fd03..e54e3e8 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -7,6 +7,7 @@ use Socialbox\Classes\StandardMethods\AcceptPrivacyPolicy; use Socialbox\Classes\StandardMethods\AcceptTermsOfService; use Socialbox\Classes\StandardMethods\AddressBookAddContact; + use Socialbox\Classes\StandardMethods\AddressBookDeleteContact; use Socialbox\Classes\StandardMethods\Authenticate; use Socialbox\Classes\StandardMethods\GetAllowedMethods; use Socialbox\Classes\StandardMethods\GetCommunityGuidelines; @@ -98,6 +99,7 @@ case SETTINGS_GET_SIGNING_KEYS = 'settingsGetSigningKeys'; case ADDRESS_BOOK_ADD_CONTACT = 'addressBookAddContact'; + case ADDRESS_BOOK_DELETE_CONTACT = 'addressBookDeleteContact'; case AUTHENTICATE = 'authenticate'; case RESOLVE_PEER = 'resolvePeer'; @@ -149,6 +151,7 @@ self::SETTINGS_GET_SIGNING_KEYS => SettingsGetSigningKeys::execute($request, $rpcRequest), self::ADDRESS_BOOK_ADD_CONTACT => AddressBookAddContact::execute($request, $rpcRequest), + self::ADDRESS_BOOK_DELETE_CONTACT => AddressBookDeleteContact::execute($request, $rpcRequest), self::AUTHENTICATE => Authenticate::execute($request, $rpcRequest), self::RESOLVE_PEER => ResolvePeer::execute($request, $rpcRequest), @@ -281,7 +284,8 @@ self::SETTINGS_SET_BIRTHDAY, self::RESOLVE_PEER, - self::ADDRESS_BOOK_ADD_CONTACT + self::ADDRESS_BOOK_ADD_CONTACT, + self::ADDRESS_BOOK_DELETE_CONTACT, ]; // Prevent the user from deleting their display name if it is required From 9e02f0c29bae9b5fb9b6dcab6744477a9d4af14e Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 22 Jan 2025 15:43:27 -0500 Subject: [PATCH 214/420] Simplified RpcRequest constructor to accept StandardMethods & string as a input for the method parameter --- src/Socialbox/Objects/RpcRequest.php | 10 +++- src/Socialbox/SocialClient.php | 78 ++++++++++++++-------------- 2 files changed, 47 insertions(+), 41 deletions(-) diff --git a/src/Socialbox/Objects/RpcRequest.php b/src/Socialbox/Objects/RpcRequest.php index 6bc1d94..a60354d 100644 --- a/src/Socialbox/Objects/RpcRequest.php +++ b/src/Socialbox/Objects/RpcRequest.php @@ -5,6 +5,7 @@ use InvalidArgumentException; use Socialbox\Classes\Logger; use Socialbox\Enums\StandardError; + use Socialbox\Enums\StandardMethods; use Socialbox\Exceptions\StandardException; use Socialbox\Interfaces\SerializableInterface; @@ -17,12 +18,17 @@ /** * Constructs the object from an array of data. * - * @param string $method The method of the request. + * @param string|StandardMethods $method The method of the request. * @param string|null $id The ID of the request. * @param array|null $parameters The parameters of the request. */ - public function __construct(string $method, ?string $id, ?array $parameters=null) + public function __construct(string|StandardMethods $method, ?string $id, ?array $parameters=null) { + if($method instanceof StandardMethods) + { + $method = $method->value; + } + $this->method = $method; $this->parameters = $parameters; $this->id = $id; diff --git a/src/Socialbox/SocialClient.php b/src/Socialbox/SocialClient.php index 829728e..90e11f1 100644 --- a/src/Socialbox/SocialClient.php +++ b/src/Socialbox/SocialClient.php @@ -46,7 +46,7 @@ public function ping(): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::PING->value, Utilities::randomCrc32()) + new RpcRequest(StandardMethods::PING, Utilities::randomCrc32()) )->getResponse()->getResult(); } @@ -59,7 +59,7 @@ public function getSessionState(): SessionState { return SessionState::fromArray($this->sendRequest( - new RpcRequest(StandardMethods::GET_SESSION_STATE->value, Utilities::randomCrc32()) + new RpcRequest(StandardMethods::GET_SESSION_STATE, Utilities::randomCrc32()) )->getResponse()->getResult()); } @@ -72,7 +72,7 @@ public function getAllowedMethods(): array { return $this->sendRequest( - new RpcRequest(StandardMethods::GET_ALLOWED_METHODS->value, Utilities::randomCrc32()) + new RpcRequest(StandardMethods::GET_ALLOWED_METHODS, Utilities::randomCrc32()) )->getResponse()->getResult(); } @@ -85,7 +85,7 @@ public function getPrivacyPolicy(): ServerDocument { return ServerDocument::fromArray($this->sendRequest( - new RpcRequest(StandardMethods::GET_PRIVACY_POLICY->value, Utilities::randomCrc32()) + new RpcRequest(StandardMethods::GET_PRIVACY_POLICY, Utilities::randomCrc32()) )->getResponse()->getResult()); } @@ -98,7 +98,7 @@ public function acceptPrivacyPolicy(): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::ACCEPT_PRIVACY_POLICY->value, Utilities::randomCrc32()) + new RpcRequest(StandardMethods::ACCEPT_PRIVACY_POLICY, Utilities::randomCrc32()) )->getResponse()->getResult(); } @@ -111,7 +111,7 @@ public function getTermsOfService(): ServerDocument { return ServerDocument::fromArray($this->sendRequest( - new RpcRequest(StandardMethods::GET_TERMS_OF_SERVICE->value, Utilities::randomCrc32()) + new RpcRequest(StandardMethods::GET_TERMS_OF_SERVICE, Utilities::randomCrc32()) )->getResponse()->getResult()); } @@ -124,7 +124,7 @@ public function acceptTermsOfService(): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::ACCEPT_TERMS_OF_SERVICE->value, Utilities::randomCrc32()) + new RpcRequest(StandardMethods::ACCEPT_TERMS_OF_SERVICE, Utilities::randomCrc32()) )->getResponse()->getResult(); } @@ -137,7 +137,7 @@ public function getCommunityGuidelines(): ServerDocument { return ServerDocument::fromArray($this->sendRequest( - new RpcRequest(StandardMethods::GET_COMMUNITY_GUIDELINES->value, Utilities::randomCrc32()) + new RpcRequest(StandardMethods::GET_COMMUNITY_GUIDELINES, Utilities::randomCrc32()) )->getResponse()->getResult()); } @@ -150,7 +150,7 @@ public function acceptCommunityGuidelines(): true { return $this->sendRequest( - new RpcRequest(StandardMethods::ACCEPT_COMMUNITY_GUIDELINES->value, Utilities::randomCrc32()) + new RpcRequest(StandardMethods::ACCEPT_COMMUNITY_GUIDELINES, Utilities::randomCrc32()) )->getResponse()->getResult(); } @@ -164,7 +164,7 @@ public function verificationEmail(string $emailAddress): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_EMAIL->value, Utilities::randomCrc32(), [ + new RpcRequest(StandardMethods::VERIFICATION_EMAIL, Utilities::randomCrc32(), [ 'email_address' => $emailAddress ]) )->getResponse()->getResult(); @@ -180,7 +180,7 @@ public function verificationAnswerEmail(string $verificationCode): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_ANSWER_EMAIL->value, Utilities::randomCrc32(), [ + new RpcRequest(StandardMethods::VERIFICATION_ANSWER_EMAIL, Utilities::randomCrc32(), [ 'verification_code' => $verificationCode ]) )->getResponse()->getResult(); @@ -196,7 +196,7 @@ public function verificationSms(string $phoneNumber): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_SMS->value, Utilities::randomCrc32(), [ + new RpcRequest(StandardMethods::VERIFICATION_SMS, Utilities::randomCrc32(), [ 'phone_number' => $phoneNumber ]) )->getResponse()->getResult(); @@ -212,7 +212,7 @@ public function verificationAnswerSms(string $verificationCode): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_ANSWER_SMS->value, Utilities::randomCrc32(), [ + new RpcRequest(StandardMethods::VERIFICATION_ANSWER_SMS, Utilities::randomCrc32(), [ 'verification_code' => $verificationCode ]) )->getResponse()->getResult(); @@ -228,7 +228,7 @@ public function verificationPhone(string $phoneNumber): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_PHONE_CALL->value, Utilities::randomCrc32(), [ + new RpcRequest(StandardMethods::VERIFICATION_PHONE_CALL, Utilities::randomCrc32(), [ 'phone_number' => $phoneNumber ]) )->getResponse()->getResult(); @@ -244,7 +244,7 @@ public function verificationAnswerPhone(string $verificationCode): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_ANSWER_PHONE_CALL->value, Utilities::randomCrc32(), [ + new RpcRequest(StandardMethods::VERIFICATION_ANSWER_PHONE_CALL, Utilities::randomCrc32(), [ 'verification_code' => $verificationCode ]) )->getResponse()->getResult(); @@ -259,7 +259,7 @@ public function verificationGetImageCaptcha(): string { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_GET_IMAGE_CAPTCHA->value, Utilities::randomCrc32()) + new RpcRequest(StandardMethods::VERIFICATION_GET_IMAGE_CAPTCHA, Utilities::randomCrc32()) )->getResponse()->getResult(); } @@ -273,7 +273,7 @@ public function verificationAnswerImageCaptcha(string $verificationCode): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_ANSWER_IMAGE_CAPTCHA->value, Utilities::randomCrc32(), [ + new RpcRequest(StandardMethods::VERIFICATION_ANSWER_IMAGE_CAPTCHA, Utilities::randomCrc32(), [ 'verification_code' => $verificationCode ]) )->getResponse()->getResult(); @@ -288,7 +288,7 @@ public function verificationGetTextCaptcha(): string { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_GET_TEXT_CAPTCHA->value, Utilities::randomCrc32()) + new RpcRequest(StandardMethods::VERIFICATION_GET_TEXT_CAPTCHA, Utilities::randomCrc32()) )->getResponse()->getResult(); } @@ -302,7 +302,7 @@ public function verificationAnswerTextCaptcha(string $verificationCode): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_ANSWER_TEXT_CAPTCHA->value, Utilities::randomCrc32(), [ + new RpcRequest(StandardMethods::VERIFICATION_ANSWER_TEXT_CAPTCHA, Utilities::randomCrc32(), [ 'verification_code' => $verificationCode ]) )->getResponse()->getResult(); @@ -317,7 +317,7 @@ public function verificationGetExternalUrl(): string { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_GET_EXTERNAL_URL->value, Utilities::randomCrc32()) + new RpcRequest(StandardMethods::VERIFICATION_GET_EXTERNAL_URL, Utilities::randomCrc32()) )->getResponse()->getResult(); } @@ -331,7 +331,7 @@ public function verificationAnswerExternalUrl(string $verificationCode): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_ANSWER_EXTERNAL_URL->value, Utilities::randomCrc32(), [ + new RpcRequest(StandardMethods::VERIFICATION_ANSWER_EXTERNAL_URL, Utilities::randomCrc32(), [ 'verification_code' => $verificationCode ]) )->getResponse()->getResult(); @@ -358,7 +358,7 @@ } return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_PASSWORD_AUTHENTICATION->value, Utilities::randomCrc32(), [ + new RpcRequest(StandardMethods::VERIFICATION_PASSWORD_AUTHENTICATION, Utilities::randomCrc32(), [ 'password' => $password ]) )->getResponse()->getResult(); @@ -374,7 +374,7 @@ public function verificationOtpAuthentication(string $code): bool { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_OTP_AUTHENTICATION->value, Utilities::randomCrc32(), [ + new RpcRequest(StandardMethods::VERIFICATION_OTP_AUTHENTICATION, Utilities::randomCrc32(), [ 'code' => $code ]) )->getResponse()->getResult(); @@ -401,7 +401,7 @@ } return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_SET_PASSWORD->value, Utilities::randomCrc32(), [ + new RpcRequest(StandardMethods::SETTINGS_SET_PASSWORD, Utilities::randomCrc32(), [ 'password' => $password ]) )->getResponse()->getResult(); @@ -417,7 +417,7 @@ public function settingsDeletePassword(string $password): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_DELETE_PASSWORD->value, Utilities::randomCrc32(), [ + new RpcRequest(StandardMethods::SETTINGS_DELETE_PASSWORD, Utilities::randomCrc32(), [ 'password' => $password ]) )->getResponse()->getResult(); @@ -434,7 +434,7 @@ public function settingsUpdatePassword(string $password, string $existingPassword): bool { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_UPDATE_PASSWORD->value, Utilities::randomCrc32(), [ + new RpcRequest(StandardMethods::SETTINGS_UPDATE_PASSWORD, Utilities::randomCrc32(), [ 'password' => $password, 'existing_password' => $existingPassword ]) @@ -450,7 +450,7 @@ public function settingsSetOtp(string $otp, bool $hash=true): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_SET_OTP->value, Utilities::randomCrc32(), [ + new RpcRequest(StandardMethods::SETTINGS_SET_OTP, Utilities::randomCrc32(), [ 'otp' => $hash ? hash('sha512', $otp) : $otp ]) )->getResponse()->getResult(); @@ -477,7 +477,7 @@ } return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_DELETE_OTP->value, Utilities::randomCrc32(), [ + new RpcRequest(StandardMethods::SETTINGS_DELETE_OTP, Utilities::randomCrc32(), [ 'password' => $password ]) )->getResponse()->getResult(); @@ -493,7 +493,7 @@ public function settingsSetDisplayName(string $displayName): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_SET_DISPLAY_NAME->value, Utilities::randomCrc32(), [ + new RpcRequest(StandardMethods::SETTINGS_SET_DISPLAY_NAME, Utilities::randomCrc32(), [ 'name' => $displayName ]) )->getResponse()->getResult(); @@ -505,7 +505,7 @@ public function settingsDeleteDisplayName(): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_DELETE_DISPLAY_NAME->value, Utilities::randomCrc32()) + new RpcRequest(StandardMethods::SETTINGS_DELETE_DISPLAY_NAME, Utilities::randomCrc32()) )->getResponse()->getResult(); } @@ -519,7 +519,7 @@ public function settingsSetDisplayPicture(string $fileId): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_SET_DISPLAY_PICTURE->value, Utilities::randomCrc32(), [ + new RpcRequest(StandardMethods::SETTINGS_SET_DISPLAY_PICTURE, Utilities::randomCrc32(), [ 'file_id' => $fileId ]) )->getResponse()->getResult(); @@ -535,7 +535,7 @@ public function settingsSetEmail(string $emailAddress): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_SET_EMAIL->value, Utilities::randomCrc32(), [ + new RpcRequest(StandardMethods::SETTINGS_SET_EMAIL, Utilities::randomCrc32(), [ 'email_address' => $emailAddress ]) )->getResponse()->getResult(); @@ -550,7 +550,7 @@ public function settingsDeleteEmail(): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_DELETE_EMAIL->value, Utilities::randomCrc32()) + new RpcRequest(StandardMethods::SETTINGS_DELETE_EMAIL, Utilities::randomCrc32()) )->getResponse()->getResult(); } @@ -564,7 +564,7 @@ public function settingsSetPhone(string $phoneNumber): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_SET_DISPLAY_NAME->value, Utilities::randomCrc32(), [ + new RpcRequest(StandardMethods::SETTINGS_SET_DISPLAY_NAME, Utilities::randomCrc32(), [ 'phone_number' => $phoneNumber ]) )->getResponse()->getResult(); @@ -576,7 +576,7 @@ public function settingsDeletePhone(): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_DELETE_PHONE->value, Utilities::randomCrc32()) + new RpcRequest(StandardMethods::SETTINGS_DELETE_PHONE, Utilities::randomCrc32()) )->getResponse()->getResult(); } @@ -592,7 +592,7 @@ public function settingsSetBirthday(int $year, int $month, int $day): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_SET_BIRTHDAY->value, Utilities::randomCrc32(), [ + new RpcRequest(StandardMethods::SETTINGS_SET_BIRTHDAY, Utilities::randomCrc32(), [ 'year' => $year, 'month' => $month, 'day' => $day @@ -609,7 +609,7 @@ public function deleteBirthday(): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_DELETE_BIRTHDAY->value, Utilities::randomCrc32()) + new RpcRequest(StandardMethods::SETTINGS_DELETE_BIRTHDAY, Utilities::randomCrc32()) )->getResponse()->getResult(); } @@ -624,7 +624,7 @@ public function authenticate(): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::AUTHENTICATE->value, Utilities::randomCrc32()) + new RpcRequest(StandardMethods::AUTHENTICATE, Utilities::randomCrc32()) )->getResponse()->getResult(); } @@ -643,7 +643,7 @@ } return Peer::fromArray($this->sendRequest( - new RpcRequest(StandardMethods::RESOLVE_PEER->value, Utilities::randomCrc32(), [ + new RpcRequest(StandardMethods::RESOLVE_PEER, Utilities::randomCrc32(), [ 'peer' => $peerAddress ]) )->getResponse()->getResult()); From 83112f01375613c4caead7df95a97f3a7ffdaf63 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 22 Jan 2025 18:16:48 -0500 Subject: [PATCH 215/420] Added debug logging to RpcClient --- src/Socialbox/Classes/RpcClient.php | 72 ++++++++++++++++++----------- 1 file changed, 45 insertions(+), 27 deletions(-) diff --git a/src/Socialbox/Classes/RpcClient.php b/src/Socialbox/Classes/RpcClient.php index a370ab5..a2cb0e5 100644 --- a/src/Socialbox/Classes/RpcClient.php +++ b/src/Socialbox/Classes/RpcClient.php @@ -18,10 +18,11 @@ class RpcClient { + private \LogLib2\Logger $logger; + private const string CLIENT_NAME = 'Socialbox PHP'; private const string CLIENT_VERSION = '1.0'; - private bool $bypassSignatureVerification; private PeerAddress $identifiedAs; private string $serverPublicSigningKey; private string $serverPublicEncryptionKey; @@ -44,11 +45,11 @@ * @throws CryptographyException If there is an error in the cryptographic operations. * @throws ResolutionException If there is an error in the resolution process. * @throws RpcException If there is an error in the RPC request or if no response is received. - * @throws DatabaseOperationException + * @throws DatabaseOperationException If there is an error in the database operation. */ public function __construct(string|PeerAddress $identifiedAs, ?string $server=null, ?ExportedSession $exportedSession=null) { - $this->bypassSignatureVerification = false; + $this->logger = new \LogLib2\Logger('net.nosial.socialbox'); // If an exported session is provided, no need to re-connect. // Just use the session details provided. @@ -78,12 +79,14 @@ // Check if the active keypair has expired if($this->serverInformation->getServerKeypairExpires() > 0 && time() > $this->serverInformation->getServerKeypairExpires()) { + // TODO: Could be auto-resolved throw new RpcException('The server keypair has expired but the server has not provided a new keypair, contact the server administrator'); } // Check if the transport encryption algorithm has changed if($this->serverInformation->getTransportEncryptionAlgorithm() !== $exportedSession->getTransportEncryptionAlgorithm()) { + // TODO: Could be auto-resolved throw new RpcException('The server has changed its transport encryption algorithm, a new session must be created, old algorithm: ' . $exportedSession->getTransportEncryptionAlgorithm() . ', new algorithm: ' . $this->serverInformation->getTransportEncryptionAlgorithm()); } @@ -150,15 +153,13 @@ /** * Initiates a new session with the server and retrieves the session UUID. * - * @return string The session UUID provided by the server upon successful session creation. * @throws RpcException If the session cannot be created, if the server does not provide a valid response, * or critical headers like encryption public key are missing in the server's response. + * @return void */ private function createSession(): void { $ch = curl_init(); - - // Basic session details $headers = [ StandardHeaders::REQUEST_TYPE->value . ': ' . RequestType::INITIATE_SESSION->value, StandardHeaders::CLIENT_NAME->value . ': ' . self::CLIENT_NAME, @@ -193,6 +194,14 @@ return $len; }); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + + $this->logger->debug(sprintf('Creating session with %s', $this->rpcEndpoint)); + $this->logger->debug(sprintf('Headers: %s', json_encode($headers))); + $this->logger->debug(sprintf('Client Encryption Public Key: %s', $this->clientEncryptionKeyPair->getPublicKey())); + $this->logger->debug(sprintf('Client Signing Public Key: %s', $this->clientSigningKeyPair->getPublicKey())); + $this->logger->debug(sprintf('Identified As: %s', $this->identifiedAs->getAddress())); + $this->logger->debug(sprintf('Client Transport Encryption Key: %s', $this->clientTransportEncryptionKey)); + $response = curl_exec($ch); // If the response is false, the request failed @@ -233,6 +242,8 @@ throw new RpcException('Failed to create session at %s, the server did not return a public encryption key'); } + $this->logger->debug(sprintf('Server Encryption Public Key: %s', $serverPublicEncryptionKey)); + // Validate the server's encryption public key if(!Cryptography::validatePublicEncryptionKey($serverPublicEncryptionKey)) { @@ -249,6 +260,7 @@ } curl_close($ch); + $this->logger->debug(sprintf('Session UUID: %s', $response)); // Set the server's encryption key $this->serverPublicEncryptionKey = $serverPublicEncryptionKey; @@ -384,6 +396,11 @@ curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_POSTFIELDS, $encryptedData); + $this->logger->debug(sprintf('Sending RPC request to %s', $this->rpcEndpoint)); + $this->logger->debug(sprintf('Headers: %s', json_encode($headers))); + $this->logger->debug(sprintf('Encrypted Data Size: %d', strlen($encryptedData))); + $this->logger->debug(sprintf('Request Signature: %s', $signature)); + $response = curl_exec($ch); if ($response === false) @@ -419,6 +436,7 @@ } curl_close($ch); + $this->logger->debug(sprintf('Encrypted Response Size: %d', strlen($responseString))); try { @@ -427,36 +445,35 @@ encryptionKey: $this->clientTransportEncryptionKey, algorithm: $this->serverInformation->getTransportEncryptionAlgorithm() ); + $this->logger->debug(sprintf('Decrypted Response: %s', $decryptedResponse)); } catch (CryptographyException $e) { throw new RpcException('Failed to decrypt response: ' . $e->getMessage(), 0, $e); } - if (!$this->bypassSignatureVerification) + $signature = $returnHeaders[strtolower(StandardHeaders::SIGNATURE->value)][0] ?? null; + $this->logger->debug(sprintf('Response Signature: %s', $signature)); + if ($signature === null) { - $signature = $returnHeaders[strtolower(StandardHeaders::SIGNATURE->value)][0] ?? null; - if ($signature === null) - { - throw new RpcException('The server did not provide a signature for the response'); - } + throw new RpcException('The server did not provide a signature for the response'); + } - try + try + { + if(!Cryptography::verifyMessage( + message: $decryptedResponse, + signature: $signature, + publicKey: $this->serverPublicSigningKey, + )) { - if(!Cryptography::verifyMessage( - message: $decryptedResponse, - signature: $signature, - publicKey: $this->serverPublicSigningKey, - )) - { - throw new RpcException('Failed to verify the response signature'); - } - } - catch (CryptographyException $e) - { - throw new RpcException('Failed to verify the response signature: ' . $e->getMessage(), 0, $e); + throw new RpcException('Failed to verify the response signature'); } } + catch (CryptographyException $e) + { + throw new RpcException('Failed to verify the response signature: ' . $e->getMessage(), 0, $e); + } $decoded = json_decode($decryptedResponse, true); if(isset($decoded['id'])) @@ -483,8 +500,6 @@ public function getServerInformation(): ServerInformation { $ch = curl_init(); - - // Basic session details $headers = [ StandardHeaders::REQUEST_TYPE->value . ': ' . RequestType::INFO->value, StandardHeaders::CLIENT_NAME->value . ': ' . self::CLIENT_NAME, @@ -496,6 +511,8 @@ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + $this->logger->debug(sprintf('Getting server information from %s', $this->rpcEndpoint)); + $this->logger->debug(sprintf('Headers: %s', json_encode($headers))); $response = curl_exec($ch); if($response === false) @@ -516,6 +533,7 @@ } curl_close($ch); + $this->logger->debug(sprintf('Server information response: %s', $response)); return ServerInformation::fromArray(json_decode($response, true)); } From d56483119cb3de9b63dc05d381ac455f879cf35b Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 22 Jan 2025 18:20:00 -0500 Subject: [PATCH 216/420] Minor changes --- src/Socialbox/Classes/RpcClient.php | 2 -- tests/Socialbox/SocialClientTest.php | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Socialbox/Classes/RpcClient.php b/src/Socialbox/Classes/RpcClient.php index a2cb0e5..a7b320a 100644 --- a/src/Socialbox/Classes/RpcClient.php +++ b/src/Socialbox/Classes/RpcClient.php @@ -200,7 +200,6 @@ $this->logger->debug(sprintf('Client Encryption Public Key: %s', $this->clientEncryptionKeyPair->getPublicKey())); $this->logger->debug(sprintf('Client Signing Public Key: %s', $this->clientSigningKeyPair->getPublicKey())); $this->logger->debug(sprintf('Identified As: %s', $this->identifiedAs->getAddress())); - $this->logger->debug(sprintf('Client Transport Encryption Key: %s', $this->clientTransportEncryptionKey)); $response = curl_exec($ch); @@ -533,7 +532,6 @@ } curl_close($ch); - $this->logger->debug(sprintf('Server information response: %s', $response)); return ServerInformation::fromArray(json_decode($response, true)); } diff --git a/tests/Socialbox/SocialClientTest.php b/tests/Socialbox/SocialClientTest.php index e0a0691..43d6505 100644 --- a/tests/Socialbox/SocialClientTest.php +++ b/tests/Socialbox/SocialClientTest.php @@ -14,6 +14,8 @@ protected function setUp(): void { + putenv('LOG_LEVEL=debug'); + // Add mocked records for the test domains ServerResolver::addMock('coffee.com', 'v=socialbox;sb-rpc=http://127.0.0.0:8086/;sb-key=sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc;sb-exp=0'); ServerResolver::addMock('teapot.com', 'v=socialbox;sb-rpc=http://127.0.0.0:8087/;sb-key=sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY;sb-exp=0'); From 75de51c910e11e445b445e5c2e27943be0b47ef0 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 22 Jan 2025 22:01:07 -0500 Subject: [PATCH 217/420] Added method ADDRESS_BOOK_GET_CONTACTS --- src/Socialbox/Classes/Configuration.php | 4 + .../Configuration/PoliciesConfiguration.php | 12 +++ .../AddressBookGetContacts.php | 63 +++++++++++++++ src/Socialbox/Enums/StandardMethods.php | 4 + src/Socialbox/Managers/ContactManager.php | 18 ++--- ...ctRecord.php => ContactDatabaseRecord.php} | 19 ++++- .../Objects/Standard/ContactRecord.php | 76 +++++++++++++++++++ 7 files changed, 185 insertions(+), 11 deletions(-) create mode 100644 src/Socialbox/Classes/StandardMethods/AddressBookGetContacts.php rename src/Socialbox/Objects/Database/{ContactRecord.php => ContactDatabaseRecord.php} (86%) create mode 100644 src/Socialbox/Objects/Standard/ContactRecord.php diff --git a/src/Socialbox/Classes/Configuration.php b/src/Socialbox/Classes/Configuration.php index 8ec6a5e..e0823f9 100644 --- a/src/Socialbox/Classes/Configuration.php +++ b/src/Socialbox/Classes/Configuration.php @@ -157,6 +157,10 @@ // When a peer's external address is resolved, it is cached for this amount of time before resolving again. // This reduces the amount of times a resolution request is made to the external server. $config->setDefault('policies.peer_sync_interval', 3600); + // The maximum number of contacts a peer can retrieve from the server at once, if the client puts a + // value that exceeds this limit, the server will use this limit instead. + // recommendation: 100 + $config->setDefault('policies.get_contacts_limit', 100); // Storage configuration $config->setDefault('storage.path', '/etc/socialbox'); // The main path for file storage diff --git a/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php b/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php index 8216907..05abe8f 100644 --- a/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php +++ b/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php @@ -8,6 +8,7 @@ private int $sessionInactivityExpires; private int $imageCaptchaExpires; private int $peerSyncInterval; + private int $getContactsLimit; public function __construct(array $data) { @@ -15,6 +16,7 @@ $this->sessionInactivityExpires = $data['session_inactivity_expires']; $this->imageCaptchaExpires = $data['image_captcha_expires']; $this->peerSyncInterval = $data['peer_sync_interval']; + $this->getContactsLimit = $data['get_contacts_limit']; } /** @@ -58,4 +60,14 @@ { return $this->peerSyncInterval; } + + /** + * Returns the maximum amount of contacts that can be retrieved in a single request + * + * @return int + */ + public function getGetContactsLimit(): int + { + return $this->getContactsLimit; + } } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/AddressBookGetContacts.php b/src/Socialbox/Classes/StandardMethods/AddressBookGetContacts.php new file mode 100644 index 0000000..46122ab --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/AddressBookGetContacts.php @@ -0,0 +1,63 @@ +getGetContactsLimit(); + if($rpcRequest->containsParameter('limit')) + { + $limit = (int)$rpcRequest->getParameter('limit'); + if($limit < 0) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Invalid limit'); + } + + $limit = min($limit, Configuration::getPoliciesConfiguration()->getGetContactsLimit()); + } + + $page = 0; + if($rpcRequest->containsParameter('page')) + { + $page = (int)$rpcRequest->getParameter('page'); + if($page < 0) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Invalid page'); + } + + $page = max($page, 0); + } + + try + { + $contacts = ContactManager::getContacts($request->getPeer(), $limit, $page); + } + catch(DatabaseOperationException $e) + { + throw new StandardException('Failed to get contacts', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + $results = []; + foreach($contacts as $contact) + { + $results[] = $contact->toStandard(); + } + + return $rpcRequest->produceResponse($results); + } + } \ No newline at end of file diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index e54e3e8..7cf47fa 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -8,6 +8,7 @@ use Socialbox\Classes\StandardMethods\AcceptTermsOfService; use Socialbox\Classes\StandardMethods\AddressBookAddContact; use Socialbox\Classes\StandardMethods\AddressBookDeleteContact; + use Socialbox\Classes\StandardMethods\AddressBookGetContacts; use Socialbox\Classes\StandardMethods\Authenticate; use Socialbox\Classes\StandardMethods\GetAllowedMethods; use Socialbox\Classes\StandardMethods\GetCommunityGuidelines; @@ -100,6 +101,7 @@ case ADDRESS_BOOK_ADD_CONTACT = 'addressBookAddContact'; case ADDRESS_BOOK_DELETE_CONTACT = 'addressBookDeleteContact'; + case ADDRESS_BOOK_GET_CONTACTS = 'addressBookGetContacts'; case AUTHENTICATE = 'authenticate'; case RESOLVE_PEER = 'resolvePeer'; @@ -152,6 +154,7 @@ self::ADDRESS_BOOK_ADD_CONTACT => AddressBookAddContact::execute($request, $rpcRequest), self::ADDRESS_BOOK_DELETE_CONTACT => AddressBookDeleteContact::execute($request, $rpcRequest), + self::ADDRESS_BOOK_GET_CONTACTS => AddressBookGetContacts::execute($request, $rpcRequest), self::AUTHENTICATE => Authenticate::execute($request, $rpcRequest), self::RESOLVE_PEER => ResolvePeer::execute($request, $rpcRequest), @@ -286,6 +289,7 @@ self::ADDRESS_BOOK_ADD_CONTACT, self::ADDRESS_BOOK_DELETE_CONTACT, + self::ADDRESS_BOOK_GET_CONTACTS, ]; // Prevent the user from deleting their display name if it is required diff --git a/src/Socialbox/Managers/ContactManager.php b/src/Socialbox/Managers/ContactManager.php index dc00f46..39339dc 100644 --- a/src/Socialbox/Managers/ContactManager.php +++ b/src/Socialbox/Managers/ContactManager.php @@ -8,7 +8,7 @@ use Socialbox\Classes\Database; use Socialbox\Enums\Types\ContactRelationshipType; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Objects\Database\ContactRecord; + use Socialbox\Objects\Database\ContactDatabaseRecord; use Socialbox\Objects\PeerAddress; class ContactManager @@ -109,10 +109,10 @@ * * @param string $peerUuid The unique identifier for the peer whose contact is to be retrieved. * @param string|PeerAddress $contactAddress The address of the contact, either as a string or a PeerAddress object. - * @return ContactRecord|null The retrieved ContactRecord instance if found, or null if no matching contact exists. + * @return ContactDatabaseRecord|null The retrieved ContactRecord instance if found, or null if no matching contact exists. * @throws DatabaseOperationException If the database query fails. */ - public static function getContact(string $peerUuid, string|PeerAddress $contactAddress): ?ContactRecord + public static function getContact(string $peerUuid, string|PeerAddress $contactAddress): ?ContactDatabaseRecord { if($contactAddress instanceof PeerAddress) { @@ -138,7 +138,7 @@ return null; } - return ContactRecord::fromArray($result); + return ContactDatabaseRecord::fromArray($result); } /** @@ -204,10 +204,10 @@ * Retrieves a contact by its unique identifier. * * @param string $uuid The unique identifier of the contact to retrieve. - * @return ContactRecord|null A ContactRecord instance if the contact is found, or null if no contact exists with the provided UUID. + * @return ContactDatabaseRecord|null A ContactRecord instance if the contact is found, or null if no contact exists with the provided UUID. * @throws DatabaseOperationException If the database query fails. */ - public static function getContactByUuid(string $uuid): ?ContactRecord + public static function getContactByUuid(string $uuid): ?ContactDatabaseRecord { try { @@ -227,7 +227,7 @@ return null; } - return ContactRecord::fromArray($result); + return ContactDatabaseRecord::fromArray($result); } /** @@ -236,7 +236,7 @@ * @param string $peerUuid The unique identifier for the peer whose contacts are to be retrieved. * @param int $limit The maximum number of contacts to retrieve per page. Defaults to 100. * @param int $page The page number to retrieve. Defaults to 1. - * @return array An array of ContactRecord instances representing the contacts for the given peer. + * @return ContactDatabaseRecord[] An array of ContactRecord instances representing the contacts for the given peer. * @throws DatabaseOperationException If the database query fails. */ public static function getContacts(string $peerUuid, int $limit=100, int $page=1): array @@ -268,7 +268,7 @@ // Convert results to ContactRecord instances foreach ($results as $result) { - $contacts[] = ContactRecord::fromArray($result); + $contacts[] = ContactDatabaseRecord::fromArray($result); } } catch (PDOException $e) diff --git a/src/Socialbox/Objects/Database/ContactRecord.php b/src/Socialbox/Objects/Database/ContactDatabaseRecord.php similarity index 86% rename from src/Socialbox/Objects/Database/ContactRecord.php rename to src/Socialbox/Objects/Database/ContactDatabaseRecord.php index 3900920..47551a0 100644 --- a/src/Socialbox/Objects/Database/ContactRecord.php +++ b/src/Socialbox/Objects/Database/ContactDatabaseRecord.php @@ -7,8 +7,9 @@ use InvalidArgumentException; use Socialbox\Enums\Types\ContactRelationshipType; use Socialbox\Interfaces\SerializableInterface; + use Socialbox\Objects\Standard\ContactRecord; - class ContactRecord implements SerializableInterface + class ContactDatabaseRecord implements SerializableInterface { private string $uuid; private string $peerUuid; @@ -117,11 +118,25 @@ /** * @inheritDoc */ - public static function fromArray(array $data): ContactRecord + public static function fromArray(array $data): ContactDatabaseRecord { return new self($data); } + /** + * Converts the object to a standard contact record. + * + * @return ContactRecord The standard contact record. + */ + public function toStandard(): ContactRecord + { + return new ContactRecord([ + 'address' => $this->contactPeerAddress, + 'relationship' => $this->relationship, + 'added_timestamp' => $this->created->getTimestamp() + ]); + } + /** * @inheritDoc */ diff --git a/src/Socialbox/Objects/Standard/ContactRecord.php b/src/Socialbox/Objects/Standard/ContactRecord.php new file mode 100644 index 0000000..919efb0 --- /dev/null +++ b/src/Socialbox/Objects/Standard/ContactRecord.php @@ -0,0 +1,76 @@ +address = PeerAddress::fromAddress($data['address']); + $this->relationship = ContactRelationshipType::tryFrom($data['relationship']) ?? ContactRelationshipType::MUTUAL; + $this->addedTimestamp = $data['added_timestamp']; + } + + /** + * Retrieves the address of the contact. + * + * @return PeerAddress Returns the address of the contact. + */ + public function getAddress(): PeerAddress + { + return $this->address; + } + + /** + * Retrieves the relationship of the contact. + * + * @return ContactRelationshipType Returns the relationship of the contact. + */ + public function getRelationship(): ContactRelationshipType + { + return $this->relationship; + } + + /** + * Retrieves the timestamp when the contact was added. + * + * @return int Returns the timestamp when the contact was added. + */ + public function getAddedTimestamp(): int + { + return $this->addedTimestamp; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): object + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'address' => $this->address->getAddress(), + 'relationship' => $this->relationship->value, + 'added_timestamp' => $this->addedTimestamp + ]; + } + } \ No newline at end of file From f689e36378c55e8cf6805f7f37f4a7f015ca65bb Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 24 Jan 2025 15:10:20 -0500 Subject: [PATCH 218/420] Refactored Peer Information to use InformationFields rather than being hard-coded into the peer record --- .idea/sqldialects.xml | 4 +- src/Socialbox/Classes/Configuration.php | 14 + .../Configuration/PoliciesConfiguration.php | 112 ++++ .../RegistrationConfiguration.php | 48 ++ .../Resources/database/authentication_otp.sql | 2 +- .../Resources/database/captcha_images.sql | 2 +- .../Resources/database/peer_information.sql | 19 + .../Classes/Resources/database/peers.sql | 37 ++ .../Resources/database/registered_peers.sql | 39 -- .../Classes/Resources/database/sessions.sql | 2 +- .../SettingsAddInformationField.php | 147 +++++ .../SettingsDeleteBirthday.php | 38 -- .../SettingsDeleteDisplayName.php | 38 -- .../SettingsDeleteDisplayPicture.php | 38 -- .../SettingsDeleteEmailAddress.php | 38 -- .../SettingsDeleteInformationField.php | 119 ++++ .../SettingsDeletePhoneNumber.php | 38 -- .../SettingsGetInformationFields.php | 32 ++ .../StandardMethods/SettingsSetBirthday.php | 63 --- .../SettingsSetDisplayName.php | 48 -- .../SettingsSetDisplayPicture.php | 65 --- .../SettingsSetEmailAddress.php | 50 -- .../SettingsSetPhoneNumber.php | 49 -- .../SettingsUpdateInformationField.php | 62 +++ .../SettingsUpdateInformationPrivacy.php | 64 +++ src/Socialbox/Enums/DatabaseObjects.php | 6 +- src/Socialbox/Enums/Flags/SessionFlags.php | 18 +- src/Socialbox/Enums/PrivacyState.php | 11 + src/Socialbox/Enums/StandardMethods.php | 144 ++--- .../Enums/Types/InformationFieldName.php | 71 +++ src/Socialbox/Managers/CaptchaManager.php | 26 +- .../Managers/OneTimePasswordManager.php | 30 +- src/Socialbox/Managers/PasswordManager.php | 32 +- .../Managers/PeerInformationManager.php | 282 ++++++++++ .../Managers/RegisteredPeerManager.php | 508 ++---------------- src/Socialbox/Managers/SessionManager.php | 6 +- src/Socialbox/Objects/ClientRequest.php | 6 +- .../Database/PeerInformationFieldRecord.php | 105 ++++ ...egisteredPeerRecord.php => PeerRecord.php} | 89 +-- .../Objects/Standard/InformationField.php | 53 ++ .../Standard/InformationFieldState.php | 62 +++ src/Socialbox/Objects/Standard/SelfUser.php | 8 +- src/Socialbox/SocialClient.php | 125 ++--- src/Socialbox/Socialbox.php | 4 +- tests/Socialbox/SocialClientTest.php | 5 +- 45 files changed, 1422 insertions(+), 1337 deletions(-) create mode 100644 src/Socialbox/Classes/Resources/database/peer_information.sql create mode 100644 src/Socialbox/Classes/Resources/database/peers.sql delete mode 100644 src/Socialbox/Classes/Resources/database/registered_peers.sql create mode 100644 src/Socialbox/Classes/StandardMethods/SettingsAddInformationField.php delete mode 100644 src/Socialbox/Classes/StandardMethods/SettingsDeleteBirthday.php delete mode 100644 src/Socialbox/Classes/StandardMethods/SettingsDeleteDisplayName.php delete mode 100644 src/Socialbox/Classes/StandardMethods/SettingsDeleteDisplayPicture.php delete mode 100644 src/Socialbox/Classes/StandardMethods/SettingsDeleteEmailAddress.php create mode 100644 src/Socialbox/Classes/StandardMethods/SettingsDeleteInformationField.php delete mode 100644 src/Socialbox/Classes/StandardMethods/SettingsDeletePhoneNumber.php create mode 100644 src/Socialbox/Classes/StandardMethods/SettingsGetInformationFields.php delete mode 100644 src/Socialbox/Classes/StandardMethods/SettingsSetBirthday.php delete mode 100644 src/Socialbox/Classes/StandardMethods/SettingsSetDisplayName.php delete mode 100644 src/Socialbox/Classes/StandardMethods/SettingsSetDisplayPicture.php delete mode 100644 src/Socialbox/Classes/StandardMethods/SettingsSetEmailAddress.php delete mode 100644 src/Socialbox/Classes/StandardMethods/SettingsSetPhoneNumber.php create mode 100644 src/Socialbox/Classes/StandardMethods/SettingsUpdateInformationField.php create mode 100644 src/Socialbox/Classes/StandardMethods/SettingsUpdateInformationPrivacy.php create mode 100644 src/Socialbox/Enums/PrivacyState.php create mode 100644 src/Socialbox/Enums/Types/InformationFieldName.php create mode 100644 src/Socialbox/Managers/PeerInformationManager.php create mode 100644 src/Socialbox/Objects/Database/PeerInformationFieldRecord.php rename src/Socialbox/Objects/Database/{RegisteredPeerRecord.php => PeerRecord.php} (70%) create mode 100644 src/Socialbox/Objects/Standard/InformationField.php create mode 100644 src/Socialbox/Objects/Standard/InformationFieldState.php diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml index 2e3f2ff..f682500 100644 --- a/.idea/sqldialects.xml +++ b/.idea/sqldialects.xml @@ -4,7 +4,8 @@ - + + @@ -12,6 +13,7 @@ + diff --git a/src/Socialbox/Classes/Configuration.php b/src/Socialbox/Classes/Configuration.php index e0823f9..653ab2e 100644 --- a/src/Socialbox/Classes/Configuration.php +++ b/src/Socialbox/Classes/Configuration.php @@ -133,10 +133,14 @@ $config->setDefault('registration.password_required', true); $config->setDefault('registration.otp_required', false); $config->setDefault('registration.display_name_required', true); + $config->setDefault('registration.first_name_required', false); + $config->setDefault('registration.middle_name_required', false); + $config->setDefault('registration.last_name_required', false); $config->setDefault('registration.display_picture_required', false); $config->setDefault('registration.email_address_required', false); $config->setDefault('registration.phone_number_required', false); $config->setDefault('registration.birthday_required', false); + $config->setDefault('registration.url_required', false); $config->setDefault('registration.image_captcha_verification_required', true); // Authentication configuration @@ -162,6 +166,16 @@ // recommendation: 100 $config->setDefault('policies.get_contacts_limit', 100); + // Default privacy states for information fields associated with the peer + $config->setDefault('policies.default_display_picture_privacy', 'PUBLIC'); + $config->setDefault('policies.default_first_name_privacy', 'CONTACTS'); + $config->setDefault('policies.default_middle_name_privacy', 'PRIVATE'); + $config->setDefault('policies.default_last_name_privacy', 'PRIVATE'); + $config->setDefault('policies.default_email_address_privacy', 'CONTACTS'); + $config->setDefault('policies.default_phone_number_privacy', 'CONTACTS'); + $config->setDefault('policies.default_birthday_privacy', 'PRIVATE'); + $config->setDefault('policies.default_url_privacy', 'PUBLIC'); + // Storage configuration $config->setDefault('storage.path', '/etc/socialbox'); // The main path for file storage $config->setDefault('storage.user_display_images_path', 'user_profiles'); // eg; `/etc/socialbox/user_profiles` diff --git a/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php b/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php index 05abe8f..7a11247 100644 --- a/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php +++ b/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php @@ -2,6 +2,8 @@ namespace Socialbox\Classes\Configuration; + use Socialbox\Enums\PrivacyState; + class PoliciesConfiguration { private int $maxSigningKeys; @@ -9,7 +11,29 @@ private int $imageCaptchaExpires; private int $peerSyncInterval; private int $getContactsLimit; + private PrivacyState $defaultDisplayPicturePrivacy; + private PrivacyState $defaultFirstNamePrivacy; + private PrivacyState $defaultMiddleNamePrivacy; + private PrivacyState $defaultLastNamePrivacy; + private PrivacyState $defaultEmailAddressPrivacy; + private PrivacyState $defaultPhoneNumberPrivacy; + private PrivacyState $defaultBirthdayPrivacy; + private PrivacyState $defaultUrlPrivacy; + /** + * Constructor method for initializing the policies configuration + * + * @param array $data An associative array containing the following keys: + * 'max_signing_keys', 'session_inactivity_expires', + * 'image_captcha_expires', 'peer_sync_interval', + * 'get_contacts_limit', 'default_display_picture_privacy', + * 'default_first_name_privacy', 'default_middle_name_privacy', + * 'default_last_name_privacy', 'default_email_address_privacy', + * 'default_phone_number_privacy', 'default_birthday_privacy', + * 'default_url_privacy'. + * + * @return void + */ public function __construct(array $data) { $this->maxSigningKeys = $data['max_signing_keys']; @@ -17,6 +41,14 @@ $this->imageCaptchaExpires = $data['image_captcha_expires']; $this->peerSyncInterval = $data['peer_sync_interval']; $this->getContactsLimit = $data['get_contacts_limit']; + $this->defaultDisplayPicturePrivacy = PrivacyState::tryFrom($data['default_display_picture_privacy']) ?? PrivacyState::PRIVATE; + $this->defaultFirstNamePrivacy = PrivacyState::tryFrom($data['default_first_name_privacy']) ?? PrivacyState::PRIVATE; + $this->defaultMiddleNamePrivacy = PrivacyState::tryFrom($data['default_middle_name_privacy']) ?? PrivacyState::PRIVATE; + $this->defaultLastNamePrivacy = PrivacyState::tryFrom($data['default_last_name_privacy']) ?? PrivacyState::PRIVATE; + $this->defaultEmailAddressPrivacy = PrivacyState::tryFrom($data['default_email_address_privacy']) ?? PrivacyState::PRIVATE; + $this->defaultPhoneNumberPrivacy = PrivacyState::tryFrom($data['default_phone_number_privacy']) ?? PrivacyState::PRIVATE; + $this->defaultBirthdayPrivacy = PrivacyState::tryFrom($data['default_birthday_privacy']) ?? PrivacyState::PRIVATE; + $this->defaultUrlPrivacy = PrivacyState::tryFrom($data['default_url_privacy']) ?? PrivacyState::PRIVATE; } /** @@ -70,4 +102,84 @@ { return $this->getContactsLimit; } + + /** + * Returns the default privacy state for the display picture + * + * @return PrivacyState + */ + public function getDefaultDisplayPicturePrivacy(): PrivacyState + { + return $this->defaultDisplayPicturePrivacy; + } + + /** + * Returns the default privacy state for the first name + * + * @return PrivacyState + */ + public function getDefaultFirstNamePrivacy(): PrivacyState + { + return $this->defaultFirstNamePrivacy; + } + + /** + * Returns the default privacy state for the middle name + * + * @return PrivacyState + */ + public function getDefaultMiddleNamePrivacy(): PrivacyState + { + return $this->defaultMiddleNamePrivacy; + } + + /** + * Returns the default privacy state for the last name + * + * @return PrivacyState + */ + public function getDefaultLastNamePrivacy(): PrivacyState + { + return $this->defaultLastNamePrivacy; + } + + /** + * Returns the default privacy state for the email address + * + * @return PrivacyState + */ + public function getDefaultEmailAddressPrivacy(): PrivacyState + { + return $this->defaultEmailAddressPrivacy; + } + + /** + * Returns the default privacy state for the phone number + * + * @return PrivacyState + */ + public function getDefaultPhoneNumberPrivacy(): PrivacyState + { + return $this->defaultPhoneNumberPrivacy; + } + + /** + * Returns the default privacy state for the birthday + * + * @return PrivacyState + */ + public function getDefaultBirthdayPrivacy(): PrivacyState + { + return $this->defaultBirthdayPrivacy; + } + + /** + * Returns the default privacy state for the URL + * + * @return PrivacyState + */ + public function getDefaultUrlPrivacy(): PrivacyState + { + return $this->defaultUrlPrivacy; + } } \ No newline at end of file diff --git a/src/Socialbox/Classes/Configuration/RegistrationConfiguration.php b/src/Socialbox/Classes/Configuration/RegistrationConfiguration.php index 35fcaf0..6745109 100644 --- a/src/Socialbox/Classes/Configuration/RegistrationConfiguration.php +++ b/src/Socialbox/Classes/Configuration/RegistrationConfiguration.php @@ -17,10 +17,14 @@ private bool $passwordRequired; private bool $otpRequired; private bool $displayNameRequired; + private bool $firstNameRequired; + private bool $middleNameRequired; + private bool $lastNameRequired; private bool $displayPictureRequired; private bool $emailAddressRequired; private bool $phoneNumberRequired; private bool $birthdayRequired; + private bool $urlRequired; private bool $imageCaptchaVerificationRequired; /** @@ -49,10 +53,14 @@ $this->passwordRequired = (bool)$data['password_required']; $this->otpRequired = (bool)$data['otp_required']; $this->displayNameRequired = (bool)$data['display_name_required']; + $this->firstNameRequired = (bool)$data['first_name_required']; + $this->middleNameRequired = (bool)$data['middle_name_required']; + $this->lastNameRequired = (bool)$data['last_name_required']; $this->displayPictureRequired = (bool)$data['display_picture_required']; $this->emailAddressRequired = (bool)$data['email_address_required']; $this->phoneNumberRequired = (bool)$data['phone_number_required']; $this->birthdayRequired = (bool)$data['birthday_required']; + $this->urlRequired = (bool)$data['url_required']; $this->imageCaptchaVerificationRequired = (bool)$data['image_captcha_verification_required']; } @@ -186,6 +194,36 @@ return $this->displayNameRequired; } + /** + * Checks if a first name is required. + * + * @return bool Returns true if a first name is required, false otherwise. + */ + public function isFirstNameRequired(): bool + { + return $this->firstNameRequired; + } + + /** + * Checks if a middle name is required. + * + * @return bool Returns true if a middle name is required, false otherwise. + */ + public function isMiddleNameRequired(): bool + { + return $this->middleNameRequired; + } + + /** + * Checks if a last name is required. + * + * @return bool Returns true if a last name is required, false otherwise. + */ + public function isLastNameRequired(): bool + { + return $this->lastNameRequired; + } + /** * Checks if a display picture is required. * @@ -226,6 +264,16 @@ return $this->birthdayRequired; } + /** + * Determines if a URL is required. + * + * @return bool Returns true if a URL is required, false otherwise. + */ + public function isUrlRequired(): bool + { + return $this->urlRequired; + } + /** * Determines if image CAPTCHA verification is required. * diff --git a/src/Socialbox/Classes/Resources/database/authentication_otp.sql b/src/Socialbox/Classes/Resources/database/authentication_otp.sql index 788e100..4e32763 100644 --- a/src/Socialbox/Classes/Resources/database/authentication_otp.sql +++ b/src/Socialbox/Classes/Resources/database/authentication_otp.sql @@ -7,7 +7,7 @@ create table authentication_otp constraint authentication_otp_peer_uuid_uindex unique (peer_uuid) comment 'The Peer UUID unique Index', constraint authentication_otp_registered_peers_uuid_fk - foreign key (peer_uuid) references registered_peers (uuid) + foreign key (peer_uuid) references peers (uuid) on update cascade on delete cascade ) comment 'Table for housing encrypted OTP secrets for for verification'; diff --git a/src/Socialbox/Classes/Resources/database/captcha_images.sql b/src/Socialbox/Classes/Resources/database/captcha_images.sql index 03336da..1a3958b 100644 --- a/src/Socialbox/Classes/Resources/database/captcha_images.sql +++ b/src/Socialbox/Classes/Resources/database/captcha_images.sql @@ -10,7 +10,7 @@ create table captcha_images constraint captchas_peer_uuid_uindex unique (peer_uuid) comment 'The Primary Unique Index for the peer UUID', constraint captchas_registered_peers_uuid_fk - foreign key (peer_uuid) references registered_peers (uuid) + foreign key (peer_uuid) references peers (uuid) on update cascade on delete cascade ); diff --git a/src/Socialbox/Classes/Resources/database/peer_information.sql b/src/Socialbox/Classes/Resources/database/peer_information.sql new file mode 100644 index 0000000..f650ca7 --- /dev/null +++ b/src/Socialbox/Classes/Resources/database/peer_information.sql @@ -0,0 +1,19 @@ +create table peer_information +( + peer_uuid varchar(36) not null comment 'The Unique Universal Identifier for the peer', + property_name enum ('DISPLAY_NAME', 'DISPLAY_PICTURE', 'FIRST_NAME', 'MIDDLE_NAME', 'LAST_NAME', 'EMAIL_ADDRESS', 'PHONE_NUMBER', 'BIRTHDAY', 'URL') not null comment 'The name of the property', + property_value varchar(256) not null comment 'The value of the property associated with the peer', + privacy_state enum ('PUBLIC', 'PRIVATE', 'CONTACTS', 'TRUSTED') default 'PRIVATE' not null comment 'The privacy setting for the information property', + primary key (property_name, peer_uuid), + constraint peer_information_peer_uuid_property_name_uindex + unique (peer_uuid, property_name) comment 'The Unique Index for the the peer uuid & property name combination', + constraint peer_information_registered_peers_uuid_fk + foreign key (peer_uuid) references peers (uuid) + on update cascade on delete cascade +) + comment 'Table for housing peer information'; + +create index peer_information_peer_uuid_index + on peer_information (peer_uuid) + comment 'The index for the peer uuid'; + diff --git a/src/Socialbox/Classes/Resources/database/peers.sql b/src/Socialbox/Classes/Resources/database/peers.sql new file mode 100644 index 0000000..0d7662c --- /dev/null +++ b/src/Socialbox/Classes/Resources/database/peers.sql @@ -0,0 +1,37 @@ +create table peers +( + uuid varchar(36) default uuid() not null comment 'The Primary index for the peer uuid' + primary key, + username varchar(255) not null comment 'The Unique username associated with the peer', + server varchar(255) default 'host' not null comment 'The server that this peer is registered to', + flags text null comment 'Comma seprted flags associated with the peer', + enabled tinyint(1) default 0 not null comment 'Boolean column to indicate if this account is Enabled, by default it''s Disabled until the account is verified.', + updated timestamp default current_timestamp() not null comment 'The Timestamp for when this record was last updated', + created timestamp default current_timestamp() not null comment 'The Timestamp for when the peer was registered on the network', + constraint registered_peers_server_username_uindex + unique (server, username) comment 'The Unique Username + Server Index Pair', + constraint registered_peers_uuid_uindex + unique (uuid) comment 'The Primary index for the peer uuid' +) + comment 'Table for housing registered peers under this network'; + +create index registered_peers_enabled_index + on peers (enabled) + comment 'The index of the enabled column for registered peers'; + +create index registered_peers_registered_index + on peers (created) + comment 'The Index for the reigstered column of the peer'; + +create index registered_peers_server_index + on peers (server) + comment 'The Index for the peer''s server'; + +create index registered_peers_updated_index + on peers (updated) + comment 'The Index for the update column'; + +create index registered_peers_username_index + on peers (username) + comment 'The index for the registered username'; + diff --git a/src/Socialbox/Classes/Resources/database/registered_peers.sql b/src/Socialbox/Classes/Resources/database/registered_peers.sql deleted file mode 100644 index d0486b0..0000000 --- a/src/Socialbox/Classes/Resources/database/registered_peers.sql +++ /dev/null @@ -1,39 +0,0 @@ -create table registered_peers -( - uuid varchar(36) default uuid() not null comment 'The Primary index for the peer uuid' - primary key, - username varchar(255) not null comment 'The Unique username associated with the peer', - server varchar(255) default 'host' not null comment 'The server that this peer is registered to', - display_name varchar(255) null comment 'Optional. The Non-Unique Display name of the peer', - display_picture varchar(36) null comment 'The UUID of the display picture that is used, null if none is set.', - flags text null comment 'Comma seprted flags associated with the peer', - enabled tinyint(1) default 0 not null comment 'Boolean column to indicate if this account is Enabled, by default it''s Disabled until the account is verified.', - updated timestamp default current_timestamp() not null comment 'The Timestamp for when this record was last updated', - created timestamp default current_timestamp() not null comment 'The Timestamp for when the peer was registered on the network', - constraint registered_peers_server_username_uindex - unique (server, username) comment 'The Unique Username + Server Index Pair', - constraint registered_peers_uuid_uindex - unique (uuid) comment 'The Primary index for the peer uuid' -) - comment 'Table for housing registered peers under this network'; - -create index registered_peers_enabled_index - on registered_peers (enabled) - comment 'The index of the enabled column for registered peers'; - -create index registered_peers_registered_index - on registered_peers (created) - comment 'The Index for the reigstered column of the peer'; - -create index registered_peers_server_index - on registered_peers (server) - comment 'The Index for the peer''s server'; - -create index registered_peers_updated_index - on registered_peers (updated) - comment 'The Index for the update column'; - -create index registered_peers_username_index - on registered_peers (username) - comment 'The index for the registered username'; - diff --git a/src/Socialbox/Classes/Resources/database/sessions.sql b/src/Socialbox/Classes/Resources/database/sessions.sql index dcf4a11..397a44d 100644 --- a/src/Socialbox/Classes/Resources/database/sessions.sql +++ b/src/Socialbox/Classes/Resources/database/sessions.sql @@ -20,7 +20,7 @@ create table sessions constraint sessions_uuid_uindex unique (uuid) comment 'The Unique Primary index for the session UUID', constraint sessions_registered_peers_uuid_fk - foreign key (peer_uuid) references registered_peers (uuid) + foreign key (peer_uuid) references peers (uuid) on update cascade on delete cascade ); diff --git a/src/Socialbox/Classes/StandardMethods/SettingsAddInformationField.php b/src/Socialbox/Classes/StandardMethods/SettingsAddInformationField.php new file mode 100644 index 0000000..782ede0 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/SettingsAddInformationField.php @@ -0,0 +1,147 @@ +containsParameter('field')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The required field parameter is missing'); + } + $fieldName = InformationFieldName::tryFrom(strtoupper($rpcRequest->getParameter('field'))); + if($fieldName === null) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The provided field parameter is invalid'); + } + + // Value parameter is required + if(!$rpcRequest->containsParameter('value')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The required value parameter is missing'); + } + $value = $rpcRequest->getParameter('value'); + if(!$fieldName->validate($value)) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The provided value parameter is invalid'); + } + + // Privacy parameter is optional + $privacy = null; + if($rpcRequest->containsParameter('privacy') && $rpcRequest->getParameter('privacy') !== null) + { + $privacy = PrivacyState::tryFrom(strtoupper($rpcRequest->getParameter('privacy'))); + if($privacy === null) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The provided privacy parameter is invalid'); + } + } + + try + { + $peer = $request->getPeer(); + } + catch (DatabaseOperationException $e) + { + throw new StandardException('Failed to retrieve current peer information', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + try + { + if (PeerInformationManager::fieldExists($peer, $fieldName)) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The provided field parameter is already registered, use settingsUpdateInformationField or settingsUpdateInformationPrivacy instead'); + } + + PeerInformationManager::addField($peer, $fieldName, $value, $privacy); + } + catch (DatabaseOperationException $e) + { + throw new StandardException('Failed to add the information field', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + // Update the session flow if necessary + try + { + switch($fieldName) + { + case InformationFieldName::DISPLAY_NAME: + SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_DISPLAY_NAME]); + break; + + case InformationFieldName::FIRST_NAME: + SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_FIRST_NAME]); + break; + + case InformationFieldName::MIDDLE_NAME: + SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_MIDDLE_NAME]); + break; + + case InformationFieldName::LAST_NAME: + SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_LAST_NAME]); + break; + + case InformationFieldName::BIRTHDAY: + SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_BIRTHDAY]); + break; + + case InformationFieldName::PHONE_NUMBER: + SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_PHONE]); + break; + + case InformationFieldName::EMAIL_ADDRESS: + SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_EMAIL]); + break; + + case InformationFieldName::URL: + SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_URL]); + break; + + default: + break; + } + } + catch (Exception $e) + { + try + { + // Rollback the information field otherwise the peer will be stuck with an incomplete session flow + PeerInformationManager::deleteField($peer, $fieldName); + } + catch (DatabaseOperationException $e) + { + // Something is seriously wrong if we can't roll back the information field + throw new StandardException('Failed to rollback the information field', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if($e instanceof StandardException) + { + throw $e; + } + + throw new StandardException('Failed to update the session flow', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/SettingsDeleteBirthday.php b/src/Socialbox/Classes/StandardMethods/SettingsDeleteBirthday.php deleted file mode 100644 index 8da8fe7..0000000 --- a/src/Socialbox/Classes/StandardMethods/SettingsDeleteBirthday.php +++ /dev/null @@ -1,38 +0,0 @@ -isBirthdayRequired()) - { - return $rpcRequest->produceError(StandardError::FORBIDDEN, 'A birthday is required for this server'); - } - - try - { - RegisteredPeerManager::deleteBirthday($request->getPeer()); - } - catch(Exception $e) - { - throw new StandardException('Failed to delete birthday ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); - } - - return $rpcRequest->produceResponse(true); - } - } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/SettingsDeleteDisplayName.php b/src/Socialbox/Classes/StandardMethods/SettingsDeleteDisplayName.php deleted file mode 100644 index 0e1b443..0000000 --- a/src/Socialbox/Classes/StandardMethods/SettingsDeleteDisplayName.php +++ /dev/null @@ -1,38 +0,0 @@ -isDisplayNameRequired()) - { - return $rpcRequest->produceError(StandardError::FORBIDDEN, 'A display name is required for this server'); - } - - try - { - RegisteredPeerManager::deleteDisplayName($request->getPeer()); - } - catch(Exception $e) - { - throw new StandardException('Failed to delete display name due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); - } - - return $rpcRequest->produceResponse(true); - } - } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/SettingsDeleteDisplayPicture.php b/src/Socialbox/Classes/StandardMethods/SettingsDeleteDisplayPicture.php deleted file mode 100644 index 9d4a95c..0000000 --- a/src/Socialbox/Classes/StandardMethods/SettingsDeleteDisplayPicture.php +++ /dev/null @@ -1,38 +0,0 @@ -isDisplayPictureRequired()) - { - return $rpcRequest->produceError(StandardError::FORBIDDEN, 'A display picture is required for this server'); - } - - try - { - RegisteredPeerManager::deleteDisplayPicture($request->getPeer()); - } - catch(Exception $e) - { - throw new StandardException('Failed to delete display picture: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); - } - - return $rpcRequest->produceResponse(true); - } - } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/SettingsDeleteEmailAddress.php b/src/Socialbox/Classes/StandardMethods/SettingsDeleteEmailAddress.php deleted file mode 100644 index cb82661..0000000 --- a/src/Socialbox/Classes/StandardMethods/SettingsDeleteEmailAddress.php +++ /dev/null @@ -1,38 +0,0 @@ -isEmailAddressRequired()) - { - return $rpcRequest->produceError(StandardError::FORBIDDEN, 'A email address is required for this server'); - } - - try - { - RegisteredPeerManager::deleteEmailAddress($request->getPeer()); - } - catch(Exception $e) - { - throw new StandardException('Failed to delete email address: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); - } - - return $rpcRequest->produceResponse(true); - } - } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/SettingsDeleteInformationField.php b/src/Socialbox/Classes/StandardMethods/SettingsDeleteInformationField.php new file mode 100644 index 0000000..92c537c --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/SettingsDeleteInformationField.php @@ -0,0 +1,119 @@ +containsParameter('field')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The required field parameter is missing'); + } + $fieldName = InformationFieldName::tryFrom(strtoupper($rpcRequest->getParameter('field'))); + if($fieldName === null) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The provided field parameter is invalid'); + } + + try + { + if(!PeerInformationManager::fieldExists($request->getPeer(), $fieldName)) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The information field does not exist'); + } + } + catch(DatabaseOperationException $e) + { + throw new StandardException('Failed to check if the information field exists', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + switch($fieldName) + { + case InformationFieldName::DISPLAY_NAME: + if(Configuration::getRegistrationConfiguration()->isDisplayNameRequired()) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'A display name is required for this server'); + } + break; + + case InformationFieldName::FIRST_NAME: + if(Configuration::getRegistrationConfiguration()->isFirstNameRequired()) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'A first name is required for this server'); + } + break; + + case InformationFieldName::MIDDLE_NAME: + if(Configuration::getRegistrationConfiguration()->isMiddleNameRequired()) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'A middle name is required for this server'); + } + break; + + case InformationFieldName::LAST_NAME: + if(Configuration::getRegistrationConfiguration()->isLastNameRequired()) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'A last name is required for this server'); + } + break; + + case InformationFieldName::BIRTHDAY: + if(Configuration::getRegistrationConfiguration()->isBirthdayRequired()) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'A birthday is required for this server'); + } + break; + + case InformationFieldName::PHONE_NUMBER: + if(Configuration::getRegistrationConfiguration()->isPhoneNumberRequired()) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'A phone number is required for this server'); + } + break; + + case InformationFieldName::EMAIL_ADDRESS: + if(Configuration::getRegistrationConfiguration()->isEmailAddressRequired()) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'An email address is required for this server'); + } + break; + + case InformationFieldName::URL: + if(Configuration::getRegistrationConfiguration()->isUrlRequired()) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'A URL is required for this server'); + } + break; + + default: + break; + } + + try + { + PeerInformationManager::deleteField($request->getPeer(), $fieldName); + } + catch(DatabaseOperationException $e) + { + throw new StandardException('Failed to delete the information field', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/SettingsDeletePhoneNumber.php b/src/Socialbox/Classes/StandardMethods/SettingsDeletePhoneNumber.php deleted file mode 100644 index aeb755e..0000000 --- a/src/Socialbox/Classes/StandardMethods/SettingsDeletePhoneNumber.php +++ /dev/null @@ -1,38 +0,0 @@ -isPhoneNumberRequired()) - { - return $rpcRequest->produceError(StandardError::FORBIDDEN, 'A phone number is required for this server'); - } - - try - { - RegisteredPeerManager::deletePhoneNumber($request->getPeer()); - } - catch(Exception $e) - { - throw new StandardException('Failed to delete phone number: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); - } - - return $rpcRequest->produceResponse(true); - } - } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/SettingsGetInformationFields.php b/src/Socialbox/Classes/StandardMethods/SettingsGetInformationFields.php new file mode 100644 index 0000000..9a90643 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/SettingsGetInformationFields.php @@ -0,0 +1,32 @@ +getPeer()); + } + catch(DatabaseOperationException $e) + { + throw new StandardException('Failed to retrieve existing information fields', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse(array_map(fn($result) => $result->toInformationFieldState(), $fieldRecords)); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/SettingsSetBirthday.php b/src/Socialbox/Classes/StandardMethods/SettingsSetBirthday.php deleted file mode 100644 index 9af21b4..0000000 --- a/src/Socialbox/Classes/StandardMethods/SettingsSetBirthday.php +++ /dev/null @@ -1,63 +0,0 @@ -containsParameter('month')) - { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'month' parameter"); - } - - if(!$rpcRequest->containsParameter('day')) - { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'day' parameter"); - } - - if(!$rpcRequest->containsParameter('year')) - { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'year' parameter"); - } - - $month = $rpcRequest->getParameter('month'); - $day = $rpcRequest->getParameter('day'); - $year = $rpcRequest->getParameter('year'); - - if(!Validator::validateDate($month, $day, $year)) - { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Invalid date provided, must be a valid gregorian calender date."); - } - - try - { - // Set the password - RegisteredPeerManager::updateBirthday($request->getPeer(), $month, $day, $year); - - // Check & update the session flow - SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_BIRTHDAY]); - } - catch(Exception $e) - { - throw new StandardException('Failed to set birthday due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); - } - - return $rpcRequest->produceResponse(true); - } - } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/SettingsSetDisplayName.php b/src/Socialbox/Classes/StandardMethods/SettingsSetDisplayName.php deleted file mode 100644 index 148e0f8..0000000 --- a/src/Socialbox/Classes/StandardMethods/SettingsSetDisplayName.php +++ /dev/null @@ -1,48 +0,0 @@ -containsParameter('name')) - { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'name' parameter"); - } - - try - { - // Update the display name - RegisteredPeerManager::updateDisplayName($request->getPeer(), $rpcRequest->getParameter('name')); - - // Check & update the session flow - SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_DISPLAY_NAME]); - } - catch(InvalidArgumentException) - { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Invalid display name'); - } - catch(Exception $e) - { - throw new StandardException('Failed to set password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); - } - - return $rpcRequest->produceResponse(true); - } - } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/SettingsSetDisplayPicture.php b/src/Socialbox/Classes/StandardMethods/SettingsSetDisplayPicture.php deleted file mode 100644 index 6567b26..0000000 --- a/src/Socialbox/Classes/StandardMethods/SettingsSetDisplayPicture.php +++ /dev/null @@ -1,65 +0,0 @@ -containsParameter('image')) - { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'image' parameter"); - } - - if(strlen($rpcRequest->getParameter('image')) > Configuration::getStorageConfiguration()->getUserDisplayImagesMaxSize()) - { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Image size exceeds the maximum allowed size of " . Configuration::getStorageConfiguration()->getUserDisplayImagesMaxSize() . " bytes"); - } - - try - { - $decodedImage = @base64_decode($rpcRequest->getParameter('image')); - if($decodedImage === false) - { - return $rpcRequest->produceError(StandardError::RPC_BAD_REQUEST, "Failed to decode JPEG image base64 data"); - } - - $sanitizedImage = Utilities::resizeImage(Utilities::sanitizeJpeg($decodedImage), 256, 256); - } - catch(Exception $e) - { - throw new StandardException('Failed to process JPEG image: ' . $e->getMessage(), StandardError::RPC_BAD_REQUEST, $e); - } - - try - { - // Set the password - RegisteredPeerManager::updateDisplayPicture($request->getPeer(), $sanitizedImage); - - // Check & update the session flow - SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_DISPLAY_PICTURE]); - } - catch(Exception $e) - { - throw new StandardException('Failed to update display picture: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); - } - - return $rpcRequest->produceResponse(true); - } - } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/SettingsSetEmailAddress.php b/src/Socialbox/Classes/StandardMethods/SettingsSetEmailAddress.php deleted file mode 100644 index c8fda50..0000000 --- a/src/Socialbox/Classes/StandardMethods/SettingsSetEmailAddress.php +++ /dev/null @@ -1,50 +0,0 @@ -containsParameter('email_address')) - { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'email_address' parameter"); - } - - if(!Validator::validateEmailAddress($rpcRequest->getParameter('email_address'))) - { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Invalid 'email_address' parameter, must be a valid email address"); - } - - try - { - // Set the password - RegisteredPeerManager::updateEmailAddress($request->getPeer(), $rpcRequest->getParameter('email_address')); - - // Check & update the session flow - SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_EMAIL]); - } - catch(Exception $e) - { - throw new StandardException('Failed to set email address due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); - } - - return $rpcRequest->produceResponse(true); - } - } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/SettingsSetPhoneNumber.php b/src/Socialbox/Classes/StandardMethods/SettingsSetPhoneNumber.php deleted file mode 100644 index eaa9a4d..0000000 --- a/src/Socialbox/Classes/StandardMethods/SettingsSetPhoneNumber.php +++ /dev/null @@ -1,49 +0,0 @@ -containsParameter('phone_number')) - { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'phone_number' parameter"); - } - - if(!Validator::validatePhoneNumber($rpcRequest->getParameter('phone_number'))) - { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Invalid 'phone_number' parameter, must be a valid phone number"); - } - - try - { - // Set the phone number - RegisteredPeerManager::updatePhoneNumber($request->getPeer(), $rpcRequest->getParameter('phone_number')); - - // Check & update the session flow - SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_PHONE]); - } - catch(Exception $e) - { - throw new StandardException('Failed to set phone number due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); - } - - return $rpcRequest->produceResponse(true); - } - } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/SettingsUpdateInformationField.php b/src/Socialbox/Classes/StandardMethods/SettingsUpdateInformationField.php new file mode 100644 index 0000000..684fbf2 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/SettingsUpdateInformationField.php @@ -0,0 +1,62 @@ +containsParameter('field')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The required field parameter is missing'); + } + $fieldName = InformationFieldName::tryFrom(strtoupper($rpcRequest->getParameter('field'))); + if($fieldName === null) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The provided field parameter is invalid'); + } + + // Value parameter is required + if(!$rpcRequest->containsParameter('value')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The required value parameter is missing'); + } + $value = $rpcRequest->getParameter('value'); + if(!$fieldName->validate($value)) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The provided value parameter is invalid'); + } + + try + { + $peer = $request->getPeer(); + if(!PeerInformationManager::fieldExists($peer, $fieldName)) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The information field does not exist'); + } + + PeerInformationManager::updateField($peer, $fieldName, $value); + } + catch(DatabaseOperationException $e) + { + throw new StandardException('Failed to update the information field', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/SettingsUpdateInformationPrivacy.php b/src/Socialbox/Classes/StandardMethods/SettingsUpdateInformationPrivacy.php new file mode 100644 index 0000000..a63f494 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/SettingsUpdateInformationPrivacy.php @@ -0,0 +1,64 @@ +containsParameter('field')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The required field parameter is missing'); + } + $fieldName = InformationFieldName::tryFrom(strtoupper($rpcRequest->getParameter('field'))); + if($fieldName === null) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The provided field parameter is invalid'); + } + + // Privacy parameter is required + $privacy = null; + if(!$rpcRequest->containsParameter('privacy')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The required privacy parameter is missing'); + } + $privacy = PrivacyState::tryFrom(strtoupper($rpcRequest->getParameter('privacy'))); + if($privacy === null) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The provided privacy parameter is invalid'); + } + + try + { + $peer = $request->getPeer(); + if(!PeerInformationManager::fieldExists($peer, $fieldName)) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The information field does not exist'); + } + + PeerInformationManager::updatePrivacyState($peer, $fieldName, $privacy); + } + catch(DatabaseOperationException $e) + { + throw new StandardException('Failed to update the information field', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file diff --git a/src/Socialbox/Enums/DatabaseObjects.php b/src/Socialbox/Enums/DatabaseObjects.php index 5a30afe..3f8acad 100644 --- a/src/Socialbox/Enums/DatabaseObjects.php +++ b/src/Socialbox/Enums/DatabaseObjects.php @@ -7,8 +7,9 @@ case VARIABLES = 'variables.sql'; case RESOLVED_DNS_RECORDS = 'resolved_dns_records.sql'; - case REGISTERED_PEERS = 'registered_peers.sql'; + case PEERS = 'peers.sql'; + case PEER_INFORMATION = 'peer_information.sql'; case AUTHENTICATION_PASSWORDS = 'authentication_passwords.sql'; case AUTHENTICATION_OTP = 'authentication_otp.sql'; case CAPTCHA_IMAGES = 'captcha_images.sql'; @@ -27,8 +28,9 @@ self::VARIABLES, self::RESOLVED_DNS_RECORDS => 0, - self::REGISTERED_PEERS => 1, + self::PEERS => 1, + self::PEER_INFORMATION, self::AUTHENTICATION_PASSWORDS, self::AUTHENTICATION_OTP, self::CAPTCHA_IMAGES, diff --git a/src/Socialbox/Enums/Flags/SessionFlags.php b/src/Socialbox/Enums/Flags/SessionFlags.php index ae47c2a..1fd021c 100644 --- a/src/Socialbox/Enums/Flags/SessionFlags.php +++ b/src/Socialbox/Enums/Flags/SessionFlags.php @@ -13,11 +13,15 @@ // Verification, require fields case SET_PASSWORD = 'SET_PASSWORD'; // Peer has to set a password case SET_OTP = 'SET_OTP'; // Peer has to set an OTP - case SET_DISPLAY_NAME = 'SET_DISPLAY_NAME'; // Peer has to set a display name - case SET_DISPLAY_PICTURE = 'SET_DISPLAY_PICTURE'; // Peer has to set a display picture - case SET_EMAIL = 'SET_EMAIL'; // Peer has to set an email - case SET_PHONE = 'SET_PHONE'; // Peer has to set a phone number - case SET_BIRTHDAY = 'SET_BIRTHDAY'; // Peer has to set a birthday + case SET_DISPLAY_NAME = 'SET_DISPLAY_NAME'; // Peer has to set a display name to their profile information + case SET_FIRST_NAME = 'SET_FIRST_NAME'; // Peer has to set a first name to their profile information + case SET_MIDDLE_NAME = 'SET_MIDDLE_NAME'; // Peer has to set a middle name to their profile information + case SET_LAST_NAME = 'SET_LAST_NAME'; // Peer has to set a last name to their profile information + case SET_DISPLAY_PICTURE = 'SET_DISPLAY_PICTURE'; // Peer has to set a display picture to their profile information + case SET_EMAIL = 'SET_EMAIL'; // Peer has to set an email to their profile information + case SET_PHONE = 'SET_PHONE'; // Peer has to set a phone number to their profile information + case SET_BIRTHDAY = 'SET_BIRTHDAY'; // Peer has to set a birthday to their profile information + case SET_URL = 'SET_URL'; // Peer has to set an url to their profile information // Verification, verification requirements case VER_PRIVACY_POLICY = 'VER_PRIVACY_POLICY'; // Peer has to accept the privacy policy @@ -37,7 +41,7 @@ case VER_AUTHENTICATION_CODE = 'VER_AUTHENTICATION_CODE'; // Peer has to enter their authentication code // Session Flags - case RATE_LIMITED = 'RATE_LIMITED'; // Peer is temporarily rate limited + case RATE_LIMITED = 'RATE_LIMITED'; // Peer is temporarily rate-limited /** * Retrieves a list of registration-related flags. @@ -83,7 +87,7 @@ * Converts an array of SessionFlags to a comma-separated string of their values. * * @param array $flags An array of SessionFlags objects to be converted. - * @return string A comma-separated string of the values of the provided SessionFlags. + * @return string A comma-separated string of the provided SessionFlags as string values. */ public static function toString(array $flags): string { diff --git a/src/Socialbox/Enums/PrivacyState.php b/src/Socialbox/Enums/PrivacyState.php new file mode 100644 index 0000000..d69c8f3 --- /dev/null +++ b/src/Socialbox/Enums/PrivacyState.php @@ -0,0 +1,11 @@ + SettingsSetPassword::execute($request, $rpcRequest), self::SETTINGS_UPDATE_PASSWORD => SettingsUpdatePassword::execute($request, $rpcRequest), self::SETTINGS_DELETE_PASSWORD => SettingsDeletePassword::execute($request, $rpcRequest), - self::SETTINGS_SET_DISPLAY_NAME => SettingsSetDisplayName::execute($request, $rpcRequest), - self::SETTINGS_DELETE_DISPLAY_NAME => SettingsDeleteDisplayName::execute($request, $rpcRequest), - self::SETTINGS_SET_DISPLAY_PICTURE => SettingsSetDisplayPicture::execute($request, $rpcRequest), - self::SETTINGS_DELETE_DISPLAY_PICTURE => SettingsDeleteDisplayPicture::execute($request, $rpcRequest), - self::SETTINGS_SET_EMAIL => SettingsSetEmailAddress::execute($request, $rpcRequest), - self::SETTINGS_DELETE_EMAIL => SettingsDeleteEmailAddress::execute($request, $rpcRequest), - self::SETTINGS_SET_PHONE => SettingsSetPhoneNumber::execute($request, $rpcRequest), - self::SETTINGS_DELETE_PHONE => SettingsDeletePhoneNumber::execute($request, $rpcRequest), - self::SETTINGS_SET_BIRTHDAY => SettingsSetBirthday::execute($request, $rpcRequest), - self::SETTINGS_DELETE_BIRTHDAY => SettingsDeleteBirthday::execute($request, $rpcRequest), + self::SETTINGS_SET_OTP => SettingsSetOtp::execute($request, $rpcRequest), + self::SETTINGS_DELETE_OTP => SettingsDeleteOtp::execute($request, $rpcRequest), + + self::SETTINGS_ADD_INFORMATION_FIELD => SettingsAddInformationField::execute($request, $rpcRequest), + self::SETTINGS_GET_INFORMATION_FIELDS => SettingsGetInformationFields::execute($request, $rpcRequest), + self::SETTINGS_UPDATE_INFORMATION_FIELD => SettingsUpdateInformationField::execute($request, $rpcRequest), + self::SETTINGS_UPDATE_INFORMATION_PRIVACY => SettingsUpdateInformationPrivacy::execute($request, $rpcRequest), + self::SETTINGS_DELETE_INFORMATION_FIELD => SettingsDeleteInformationField::execute($request, $rpcRequest), self::SETTINGS_ADD_SIGNING_KEY => SettingsAddSigningKey::execute($request, $rpcRequest), self::SETTINGS_GET_SIGNING_KEYS => SettingsGetSigningKeys::execute($request, $rpcRequest), @@ -277,14 +260,14 @@ $methods = [ self::SETTINGS_ADD_SIGNING_KEY, self::SETTINGS_GET_SIGNING_KEYS, - self::SETTINGS_SET_DISPLAY_NAME, - self::SETTINGS_SET_DISPLAY_PICTURE, + self::SETTINGS_ADD_INFORMATION_FIELD, + self::SETTINGS_GET_INFORMATION_FIELDS, + self::SETTINGS_UPDATE_INFORMATION_FIELD, + self::SETTINGS_UPDATE_INFORMATION_PRIVACY, + self::SETTINGS_DELETE_INFORMATION_FIELD, self::SETTINGS_SET_PASSWORD, self::SETTINGS_UPDATE_PASSWORD, self::SETTINGS_SET_OTP, - self::SETTINGS_SET_EMAIL, - self::SETTINGS_SET_PHONE, - self::SETTINGS_SET_BIRTHDAY, self::RESOLVE_PEER, self::ADDRESS_BOOK_ADD_CONTACT, @@ -292,48 +275,6 @@ self::ADDRESS_BOOK_GET_CONTACTS, ]; - // Prevent the user from deleting their display name if it is required - if(!Configuration::getRegistrationConfiguration()->isDisplayNameRequired()) - { - $methods[] = self::SETTINGS_DELETE_DISPLAY_NAME; - } - - // Prevent the user from deleting their password if it is required - if(!Configuration::getRegistrationConfiguration()->isPasswordRequired()) - { - $methods[] = self::SETTINGS_DELETE_PASSWORD; - } - - // Prevent the user from deleting their display picture if it is required - if(!Configuration::getRegistrationConfiguration()->isDisplayPictureRequired()) - { - $methods[] = self::SETTINGS_DELETE_DISPLAY_PICTURE; - } - - // Prevent the user from deleting their OTP if it is required - if(!Configuration::getRegistrationConfiguration()->isOtpRequired()) - { - $methods[] = self::SETTINGS_DELETE_OTP; - } - - // Prevent the user from deleting their Phone Number if it is required - if(!Configuration::getRegistrationConfiguration()->isPhoneNumberRequired()) - { - $methods[] = self::SETTINGS_DELETE_PHONE; - } - - // Prevent the user from deleting their email address if it is required - if(!Configuration::getRegistrationConfiguration()->isEmailAddressRequired()) - { - $methods[] = self::SETTINGS_DELETE_EMAIL; - } - - // Prevent the user from deleting their birthday if it is required - if(!Configuration::getRegistrationConfiguration()->isBirthdayRequired()) - { - $methods[] = self::SETTINGS_DELETE_BIRTHDAY; - } - return $methods; } @@ -391,33 +332,18 @@ } // If the flag `SET_DISPLAY_NAME` is set, then the user has to set a display name - if($session->flagExists(SessionFlags::SET_DISPLAY_NAME)) + if($session->flagExists([ + SessionFlags::SET_DISPLAY_NAME, + SessionFlags::SET_FIRST_NAME, + SessionFlags::SET_MIDDLE_NAME, + SessionFlags::SET_LAST_NAME, + SessionFlags::SET_BIRTHDAY, + SessionFlags::SET_PHONE, + SessionFlags::SET_EMAIL, + SessionFlags::SET_URL + ])) { - $methods[] = self::SETTINGS_SET_DISPLAY_NAME; - } - - // If the flag `SET_DISPLAY_PICTURE` is set, then the user has to set a display picture - if($session->flagExists(SessionFlags::SET_DISPLAY_PICTURE)) - { - $methods[] = self::SETTINGS_SET_DISPLAY_PICTURE; - } - - // If the flag `SET_EMAIL` is set, then the user has to set an email address - if($session->flagExists(SessionFlags::SET_EMAIL)) - { - $methods[] = self::SETTINGS_SET_EMAIL; - } - - // If the flag `SET_PHONE` is set, then the user has to set a phone number - if($session->flagExists(SessionFlags::SET_PHONE)) - { - $methods[] = self::SETTINGS_SET_PHONE; - } - - // If the flag `SET_BIRTHDAY` is set, then the user has to set a birthday - if($session->flagExists(SessionFlags::SET_BIRTHDAY)) - { - $methods[] = self::SETTINGS_SET_BIRTHDAY; + $methods[] = self::SETTINGS_ADD_INFORMATION_FIELD; } return $methods; diff --git a/src/Socialbox/Enums/Types/InformationFieldName.php b/src/Socialbox/Enums/Types/InformationFieldName.php new file mode 100644 index 0000000..54a18b1 --- /dev/null +++ b/src/Socialbox/Enums/Types/InformationFieldName.php @@ -0,0 +1,71 @@ + strlen($value) >= 3 && strlen($value) <= 50, + InformationFieldName::LAST_NAME, InformationFieldName::MIDDLE_NAME, InformationFieldName::FIRST_NAME => strlen($value) >= 2 && strlen($value) <= 50, + InformationFieldName::EMAIL_ADDRESS => filter_var($value, FILTER_VALIDATE_EMAIL), + InformationFieldName::PHONE_NUMBER => preg_match('/^\+?[0-9]{1,3}-?[0-9]{3}-?[0-9]{3}-?[0-9]{4}$/', $value), + InformationFieldName::BIRTHDAY => preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/', $value), + InformationFieldName::URL => filter_var($value, FILTER_VALIDATE_URL), + default => true, + }; + } + } diff --git a/src/Socialbox/Managers/CaptchaManager.php b/src/Socialbox/Managers/CaptchaManager.php index 3d599c9..2ccba7e 100644 --- a/src/Socialbox/Managers/CaptchaManager.php +++ b/src/Socialbox/Managers/CaptchaManager.php @@ -10,21 +10,21 @@ use Socialbox\Classes\Utilities; use Socialbox\Enums\Status\CaptchaStatus; use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Objects\Database\CaptchaRecord; -use Socialbox\Objects\Database\RegisteredPeerRecord; +use Socialbox\Objects\Database\PeerRecord; class CaptchaManager { /** * Creates a new captcha for the given peer UUID. * - * @param string|RegisteredPeerRecord $peer_uuid The UUID of the peer to create the captcha for. + * @param string|PeerRecord $peer_uuid The UUID of the peer to create the captcha for. * @return string The answer to the captcha. * @throws DatabaseOperationException If the operation fails. */ - public static function createCaptcha(string|RegisteredPeerRecord $peer_uuid): string + public static function createCaptcha(string|PeerRecord $peer_uuid): string { // If the peer_uuid is a RegisteredPeerRecord, get the UUID - if($peer_uuid instanceof RegisteredPeerRecord) + if($peer_uuid instanceof PeerRecord) { $peer_uuid = $peer_uuid->getUuid(); } @@ -73,14 +73,14 @@ class CaptchaManager /** * Answers a captcha for the given peer UUID. * - * @param string|RegisteredPeerRecord $peer_uuid The UUID of the peer to answer the captcha for. + * @param string|PeerRecord $peer_uuid The UUID of the peer to answer the captcha for. * @param string $answer The answer to the captcha. * @return bool True if the answer is correct, false otherwise. * @throws DatabaseOperationException If the operation fails. */ - public static function answerCaptcha(string|RegisteredPeerRecord $peer_uuid, string $answer): bool + public static function answerCaptcha(string|PeerRecord $peer_uuid, string $answer): bool { - if($peer_uuid instanceof RegisteredPeerRecord) + if($peer_uuid instanceof PeerRecord) { $peer_uuid = $peer_uuid->getUuid(); } @@ -129,14 +129,14 @@ class CaptchaManager /** * Retrieves the captcha record for the given peer UUID. * - * @param string|RegisteredPeerRecord $peer_uuid The UUID of the peer to retrieve the captcha for. + * @param string|PeerRecord $peer_uuid The UUID of the peer to retrieve the captcha for. * @return CaptchaRecord|null The captcha record. * @throws DatabaseOperationException If the operation fails. */ - public static function getCaptcha(string|RegisteredPeerRecord $peer_uuid): ?CaptchaRecord + public static function getCaptcha(string|PeerRecord $peer_uuid): ?CaptchaRecord { // If the peer_uuid is a RegisteredPeerRecord, get the UUID - if($peer_uuid instanceof RegisteredPeerRecord) + if($peer_uuid instanceof PeerRecord) { $peer_uuid = $peer_uuid->getUuid(); } @@ -166,14 +166,14 @@ class CaptchaManager /** * Checks if a captcha exists for the given peer UUID. * - * @param string|RegisteredPeerRecord $peer_uuid The UUID of the peer to check for a captcha. + * @param string|PeerRecord $peer_uuid The UUID of the peer to check for a captcha. * @return bool True if a captcha exists, false otherwise. * @throws DatabaseOperationException If the operation fails. */ - public static function captchaExists(string|RegisteredPeerRecord $peer_uuid): bool + public static function captchaExists(string|PeerRecord $peer_uuid): bool { // If the peer_uuid is a RegisteredPeerRecord, get the UUID - if($peer_uuid instanceof RegisteredPeerRecord) + if($peer_uuid instanceof PeerRecord) { $peer_uuid = $peer_uuid->getUuid(); } diff --git a/src/Socialbox/Managers/OneTimePasswordManager.php b/src/Socialbox/Managers/OneTimePasswordManager.php index 18c4e26..5210afa 100644 --- a/src/Socialbox/Managers/OneTimePasswordManager.php +++ b/src/Socialbox/Managers/OneTimePasswordManager.php @@ -10,20 +10,20 @@ use Socialbox\Classes\OtpCryptography; use Socialbox\Exceptions\CryptographyException; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Objects\Database\RegisteredPeerRecord; + use Socialbox\Objects\Database\PeerRecord; class OneTimePasswordManager { /** * Checks if a given peer uses OTP for authentication. * - * @param string|RegisteredPeerRecord $peerUuid Either a UUID as a string or a RegisteredPeerRecord object representing the peer. + * @param string|PeerRecord $peerUuid Either a UUID as a string or a RegisteredPeerRecord object representing the peer. * @return bool Returns true if the peer uses OTP, otherwise false. * @throws DatabaseOperationException Thrown when a database error occurs. */ - public static function usesOtp(string|RegisteredPeerRecord $peerUuid): bool + public static function usesOtp(string|PeerRecord $peerUuid): bool { - if($peerUuid instanceof RegisteredPeerRecord) + if($peerUuid instanceof PeerRecord) { $peerUuid = $peerUuid->getUuid(); } @@ -45,12 +45,12 @@ /** * Creates and stores a new OTP (One-Time Password) secret for the specified peer, and generates a key URI. * - * @param string|RegisteredPeerRecord $peer The unique identifier of the peer, either as a string UUID + * @param string|PeerRecord $peer The unique identifier of the peer, either as a string UUID * or an instance of RegisteredPeerRecord. * @return string The generated OTP key URI that can be used for applications like authenticator apps. * @throws DatabaseOperationException If there is an error during the database operation. */ - public static function createOtp(string|RegisteredPeerRecord $peer): string + public static function createOtp(string|PeerRecord $peer): string { if(is_string($peer)) { @@ -84,16 +84,16 @@ /** * Verifies the provided OTP (One-Time Password) against the stored secret associated with the specified peer. * - * @param string|RegisteredPeerRecord $peerUuid The unique identifier of the peer, either as a string UUID + * @param string|PeerRecord $peerUuid The unique identifier of the peer, either as a string UUID * or an instance of RegisteredPeerRecord. * @param string $otp The OTP to be verified. * @return bool Returns true if the OTP is valid; otherwise, false. * @throws DatabaseOperationException If there is an error during the database operation. * @throws CryptographyException If there is a failure in decrypting the stored OTP secret. */ - public static function verifyOtp(string|RegisteredPeerRecord $peerUuid, string $otp): bool + public static function verifyOtp(string|PeerRecord $peerUuid, string $otp): bool { - if($peerUuid instanceof RegisteredPeerRecord) + if($peerUuid instanceof PeerRecord) { $peerUuid = $peerUuid->getUuid(); } @@ -145,13 +145,13 @@ /** * Deletes the OTP record associated with the specified peer. * - * @param string|RegisteredPeerRecord $peerUuid The peer's UUID or an instance of RegisteredPeerRecord whose OTP record needs to be deleted. + * @param string|PeerRecord $peerUuid The peer's UUID or an instance of RegisteredPeerRecord whose OTP record needs to be deleted. * @return void * @throws DatabaseOperationException if the database operation fails. */ - public static function deleteOtp(string|RegisteredPeerRecord $peerUuid): void + public static function deleteOtp(string|PeerRecord $peerUuid): void { - if($peerUuid instanceof RegisteredPeerRecord) + if($peerUuid instanceof PeerRecord) { $peerUuid = $peerUuid->getUuid(); } @@ -171,12 +171,12 @@ /** * Retrieves the last updated timestamp for the OTP record of the specified peer. * - * @param string|RegisteredPeerRecord $peerUuid The peer's UUID or an instance of RegisteredPeerRecord whose OTP record's last updated timestamp needs to be retrieved + * @param string|PeerRecord $peerUuid The peer's UUID or an instance of RegisteredPeerRecord whose OTP record's last updated timestamp needs to be retrieved * @return int The last updated timestamp of the OTP record, or 0 if no such record exists */ - public static function getLastUpdated(string|RegisteredPeerRecord $peerUuid): int + public static function getLastUpdated(string|PeerRecord $peerUuid): int { - if($peerUuid instanceof RegisteredPeerRecord) + if($peerUuid instanceof PeerRecord) { $peerUuid = $peerUuid->getUuid(); } diff --git a/src/Socialbox/Managers/PasswordManager.php b/src/Socialbox/Managers/PasswordManager.php index e9c3b26..f6a0465 100644 --- a/src/Socialbox/Managers/PasswordManager.php +++ b/src/Socialbox/Managers/PasswordManager.php @@ -10,20 +10,20 @@ use Socialbox\Classes\Database; use Socialbox\Exceptions\CryptographyException; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Objects\Database\RegisteredPeerRecord; + use Socialbox\Objects\Database\PeerRecord; class PasswordManager { /** * Checks if the given peer UUID is associated with a password in the database. * - * @param string|RegisteredPeerRecord $peerUuid The UUID of the peer, or an instance of RegisteredPeerRecord from which the UUID will be retrieved. + * @param string|PeerRecord $peerUuid The UUID of the peer, or an instance of RegisteredPeerRecord from which the UUID will be retrieved. * @return bool Returns true if the peer UUID is associated with a password, otherwise false. * @throws DatabaseOperationException If an error occurs while querying the database. */ - public static function usesPassword(string|RegisteredPeerRecord $peerUuid): bool + public static function usesPassword(string|PeerRecord $peerUuid): bool { - if($peerUuid instanceof RegisteredPeerRecord) + if($peerUuid instanceof PeerRecord) { $peerUuid = $peerUuid->getUuid(); } @@ -45,15 +45,15 @@ /** * Sets a secured password for the given peer UUID or registered peer record. * - * @param string|RegisteredPeerRecord $peerUuid The unique identifier or registered peer record of the user. + * @param string|PeerRecord $peerUuid The unique identifier or registered peer record of the user. * @param string $hash The plaintext password to be securely stored. * @return void * @throws DatabaseOperationException If an error occurs while storing the password in the database. * @throws CryptographyException If an error occurs during password encryption or hashing. */ - public static function setPassword(string|RegisteredPeerRecord $peerUuid, string $hash): void + public static function setPassword(string|PeerRecord $peerUuid, string $hash): void { - if($peerUuid instanceof RegisteredPeerRecord) + if($peerUuid instanceof PeerRecord) { $peerUuid = $peerUuid->getUuid(); } @@ -84,15 +84,15 @@ /** * Updates the secured password associated with the given peer UUID. * - * @param string|RegisteredPeerRecord $peerUuid The unique identifier or registered peer record of the user. + * @param string|PeerRecord $peerUuid The unique identifier or registered peer record of the user. * @param string $hash The new password to be stored securely. * @return void * @throws DatabaseOperationException If an error occurs while updating the password in the database. * @throws CryptographyException If an error occurs while encrypting the password or validating the hash. */ - public static function updatePassword(string|RegisteredPeerRecord $peerUuid, string $hash): void + public static function updatePassword(string|PeerRecord $peerUuid, string $hash): void { - if($peerUuid instanceof RegisteredPeerRecord) + if($peerUuid instanceof PeerRecord) { $peerUuid = $peerUuid->getUuid(); } @@ -124,13 +124,13 @@ /** * Deletes the stored password for a specific peer. * - * @param string|RegisteredPeerRecord $peerUuid The unique identifier of the peer, or an instance of RegisteredPeerRecord. + * @param string|PeerRecord $peerUuid The unique identifier of the peer, or an instance of RegisteredPeerRecord. * @return void * @throws DatabaseOperationException If an error occurs during the database operation. */ - public static function deletePassword(string|RegisteredPeerRecord $peerUuid): void + public static function deletePassword(string|PeerRecord $peerUuid): void { - if($peerUuid instanceof RegisteredPeerRecord) + if($peerUuid instanceof PeerRecord) { $peerUuid = $peerUuid->getUuid(); } @@ -150,15 +150,15 @@ /** * Verifies a given password against a stored password hash for a specific peer. * - * @param string|RegisteredPeerRecord $peerUuid The unique identifier of the peer, or an instance of RegisteredPeerRecord. + * @param string|PeerRecord $peerUuid The unique identifier of the peer, or an instance of RegisteredPeerRecord. * @param string $sha512 The SHA-512 hash of the password to be verified. * @return bool Returns true if the password matches the stored hash; false otherwise. * @throws CryptographyException If the password hash is invalid or an error occurs during the cryptographic operation. * @throws DatabaseOperationException If an error occurs during the database operation. */ - public static function verifyPassword(string|RegisteredPeerRecord $peerUuid, string $sha512): bool + public static function verifyPassword(string|PeerRecord $peerUuid, string $sha512): bool { - if($peerUuid instanceof RegisteredPeerRecord) + if($peerUuid instanceof PeerRecord) { $peerUuid = $peerUuid->getUuid(); } diff --git a/src/Socialbox/Managers/PeerInformationManager.php b/src/Socialbox/Managers/PeerInformationManager.php new file mode 100644 index 0000000..d8b0186 --- /dev/null +++ b/src/Socialbox/Managers/PeerInformationManager.php @@ -0,0 +1,282 @@ +getUuid(); + } + + if($privacyState === null) + { + $privacyState = match($property) + { + InformationFieldName::DISPLAY_NAME => Configuration::getPoliciesConfiguration()->getDefaultDisplayPicturePrivacy(), + InformationFieldName::FIRST_NAME => Configuration::getPoliciesConfiguration()->getDefaultFirstNamePrivacy(), + InformationFieldName::MIDDLE_NAME => Configuration::getPoliciesConfiguration()->getDefaultMiddleNamePrivacy(), + InformationFieldName::LAST_NAME => Configuration::getPoliciesConfiguration()->getDefaultLastNamePrivacy(), + InformationFieldName::EMAIL_ADDRESS => Configuration::getPoliciesConfiguration()->getDefaultEmailAddressPrivacy(), + InformationFieldName::PHONE_NUMBER => Configuration::getPoliciesConfiguration()->getDefaultPhoneNumberPrivacy(), + InformationFieldName::BIRTHDAY => Configuration::getPoliciesConfiguration()->getDefaultBirthdayPrivacy(), + InformationFieldName::DISPLAY_PICTURE => COnfiguration::getPoliciesConfiguration()->getDefaultDisplayPicturePrivacy(), + InformationFieldName::URL => Configuration::getPoliciesConfiguration()->getDefaultUrlPrivacy(), + }; + } + + try + { + $stmt = Database::getConnection()->prepare('INSERT INTO peer_information (peer_uuid, property_name, property_value, privacy_state) VALUES (:peer_uuid, :property_name, :property_value, :privacy_state)'); + $stmt->bindValue(':peer_uuid', $peerUuid); + $propertyName = $property->value; + $stmt->bindValue(':property_name', $propertyName); + $stmt->bindValue(':property_value', $value); + $stmt->bindValue(':privacy_state', $privacyState->value); + $stmt->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException(sprintf('Failed to add property for peer %s', $peerUuid), $e); + } + } + + /** + * Updates a property for a peer's information record. + * + * @param string|PeerRecord $peerUuid The UUID of the peer to update the property for. + * @param InformationFieldName $property The name of the property to update. + * @param string $value The new value of the property. + * + * @return void + * + * @throws DatabaseOperationException Thrown if the operation fails. + */ + public static function updateField(string|PeerRecord $peerUuid, InformationFieldName $property, string $value): void + { + if($peerUuid instanceof PeerRecord) + { + $peerUuid = $peerUuid->getUuid(); + } + + if(!self::fieldExists($peerUuid, $property)) + { + throw new DatabaseOperationException(sprintf('Cannot to update property %s for peer %s, property does not exist', $property->value, $peerUuid)); + } + + try + { + $stmt = Database::getConnection()->prepare('UPDATE peer_information SET property_value=:property_value WHERE peer_uuid=:peer_uuid AND property_name=:property_name'); + $stmt->bindValue(':peer_uuid', $peerUuid); + $propertyName = $property->value; + $stmt->bindValue(':property_name', $propertyName); + $stmt->bindValue(':property_value', $value); + $stmt->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException(sprintf('Failed to update property %s for peer %s', $property->value, $peerUuid), $e); + } + } + + /** + * Updates the privacy state for a property in a peer's information record. + * + * @param string|PeerRecord $peerUuid The UUID of the peer to update the privacy state for. + * @param InformationFieldName $property The name of the property to update the privacy state for. + * @param PrivacyState $privacyState The new privacy state of the property. + * + * @return void + * + * @throws DatabaseOperationException Thrown if the operation fails. + */ + public static function updatePrivacyState(string|PeerRecord $peerUuid, InformationFieldName $property, PrivacyState $privacyState): void + { + if($peerUuid instanceof PeerRecord) + { + $peerUuid = $peerUuid->getUuid(); + } + + if(!self::fieldExists($peerUuid, $property)) + { + throw new \InvalidArgumentException(sprintf('Cannot update privacy state, the requested property %s does not exist with %s', $property->value, $peerUuid)); + } + + try + { + $stmt = Database::getConnection()->prepare('UPDATE peer_information SET privacy_state=:privacy_state WHERE peer_uuid=:peer_uuid AND property_name=:property_name'); + $stmt->bindValue(':peer_uuid', $peerUuid); + $propertyName = $property->value; + $stmt->bindValue(':property_name', $propertyName); + $privacyState = $privacyState->value; + $stmt->bindValue(':privacy_state', $privacyState); + $stmt->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException(sprintf('Failed to update privacy state for property %s for peer %s', $property->value, $peerUuid), $e); + } + } + + /** + * Checks if a property exists for a peer. + * + * @param string|PeerRecord $peerUuid The UUID of the peer to check for the property. + * @param InformationFieldName $property The name of the property to check for. + * + * @return bool + * + * @throws DatabaseOperationException Thrown if the operation fails. + */ + public static function fieldExists(string|PeerRecord $peerUuid, InformationFieldName $property): bool + { + if($peerUuid instanceof PeerRecord) + { + $peerUuid = $peerUuid->getUuid(); + } + + try + { + $stmt = Database::getConnection()->prepare('SELECT COUNT(*) FROM peer_information WHERE peer_uuid=:peer_uuid AND property_name=:property_name LIMIT 1'); + $stmt->bindValue(':peer_uuid', $peerUuid); + $propertyName = $property->value; + $stmt->bindValue(':property_name', $propertyName); + $stmt->execute(); + + return $stmt->fetchColumn() > 0; + } + catch(PDOException $e) + { + throw new DatabaseOperationException(sprintf('Failed to check if property exists for peer %s', $peerUuid), $e); + } + } + + /** + * Gets a property from a peer's information record. + * + * @param string|PeerRecord $peerUuid The UUID of the peer to get the property from. + * @param InformationFieldName $property The name of the property to get. + * + * @return PeerInformationFieldRecord + * + * @throws DatabaseOperationException Thrown if the operation fails. + */ + public static function getField(string|PeerRecord $peerUuid, InformationFieldName $property): PeerInformationFieldRecord + { + if($peerUuid instanceof PeerRecord) + { + $peerUuid = $peerUuid->getUuid(); + } + + try + { + $stmt = Database::getConnection()->prepare('SELECT * FROM peer_information WHERE peer_uuid=:peer_uuid AND property_name=:property_name LIMIT 1'); + $stmt->bindValue(':peer_uuid', $peerUuid); + $propertyName = $property->value; + $stmt->bindValue(':property_name', $propertyName); + $stmt->execute(); + + $result = $stmt->fetch(); + if($result === false) + { + throw new DatabaseOperationException(sprintf('Property %s does not exist for peer %s', $property->value, $peerUuid)); + } + + return PeerInformationFieldRecord::fromArray($result); + } + catch(PDOException $e) + { + throw new DatabaseOperationException(sprintf('Failed to get property %s for peer %s', $property->value, $peerUuid), $e); + } + } + + /** + * Gets all properties from a peer's information record. + * + * @param string|PeerRecord $peerUuid The UUID of the peer to get the properties from. + * + * @return PeerInformationFieldRecord[] + * + * @throws DatabaseOperationException Thrown if the operation fails. + */ + public static function getFields(string|PeerRecord $peerUuid): array + { + if($peerUuid instanceof PeerRecord) + { + $peerUuid = $peerUuid->getUuid(); + } + + try + { + $stmt = Database::getConnection()->prepare('SELECT * FROM peer_information WHERE peer_uuid=:peer_uuid'); + $stmt->bindValue(':peer_uuid', $peerUuid); + $stmt->execute(); + $results = $stmt->fetchAll(); + + if(!$results) + { + return []; + } + + return array_map(fn($result) => PeerInformationFieldRecord::fromArray($result), $results); + } + catch(PDOException $e) + { + throw new DatabaseOperationException(sprintf('Failed to get properties for peer %s', $peerUuid), $e); + } + } + + /** + * Deletes a property from a peer's information record. + * + * @param string|PeerRecord $peerUuid The UUID of the peer to delete the property from. + * @param InformationFieldName $property The name of the property to delete. + * + * @return void + * + * @throws DatabaseOperationException Thrown if the operation fails. + */ + public static function deleteField(string|PeerRecord $peerUuid, InformationFieldName $property): void + { + if($peerUuid instanceof PeerRecord) + { + $peerUuid = $peerUuid->getUuid(); + } + + try + { + $stmt = Database::getConnection()->prepare('DELETE FROM peer_information WHERE peer_uuid=:peer_uuid AND property_name=:property_name'); + $stmt->bindValue(':peer_uuid', $peerUuid); + $propertyName = $property->value; + $stmt->bindValue(':property_name', $propertyName); + $stmt->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException(sprintf('Failed to delete property %s for peer %s', $property->value, $peerUuid), $e); + } + } + } \ No newline at end of file diff --git a/src/Socialbox/Managers/RegisteredPeerManager.php b/src/Socialbox/Managers/RegisteredPeerManager.php index 5adebe6..bf20cb9 100644 --- a/src/Socialbox/Managers/RegisteredPeerManager.php +++ b/src/Socialbox/Managers/RegisteredPeerManager.php @@ -10,11 +10,10 @@ use Socialbox\Classes\Configuration; use Socialbox\Classes\Database; use Socialbox\Classes\Logger; - use Socialbox\Classes\Validator; use Socialbox\Enums\Flags\PeerFlags; use Socialbox\Enums\ReservedUsernames; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Objects\Database\RegisteredPeerRecord; + use Socialbox\Objects\Database\PeerRecord; use Socialbox\Objects\PeerAddress; use Socialbox\Objects\Standard\Peer; use Symfony\Component\Uid\Uuid; @@ -34,7 +33,7 @@ try { - $statement = Database::getConnection()->prepare('SELECT COUNT(*) FROM `registered_peers` WHERE username=?'); + $statement = Database::getConnection()->prepare('SELECT COUNT(*) FROM peers WHERE username=?'); $statement->bindParam(1, $username); $statement->execute(); @@ -68,7 +67,7 @@ try { - $statement = Database::getConnection()->prepare('INSERT INTO `registered_peers` (uuid, username, server, enabled) VALUES (?, ?, ?, ?)'); + $statement = Database::getConnection()->prepare('INSERT INTO peers (uuid, username, server, enabled) VALUES (?, ?, ?, ?)'); $statement->bindParam(1, $uuid); $username = $peerAddress->getUsername(); $statement->bindParam(2, $username); @@ -88,13 +87,13 @@ * Deletes a peer from the database based on the given UUID or RegisteredPeerRecord. * WARNING: This operation is cascading and will delete all associated data. * - * @param string|RegisteredPeerRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer to be deleted. + * @param string|PeerRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer to be deleted. * @return void * @throws DatabaseOperationException If the operation fails. */ - public static function deletePeer(string|RegisteredPeerRecord $uuid): void + public static function deletePeer(string|PeerRecord $uuid): void { - if($uuid instanceof RegisteredPeerRecord) + if($uuid instanceof PeerRecord) { $uuid = $uuid->getUuid(); } @@ -103,7 +102,7 @@ try { - $statement = Database::getConnection()->prepare('DELETE FROM `registered_peers` WHERE uuid=?'); + $statement = Database::getConnection()->prepare('DELETE FROM peers WHERE uuid=?'); $statement->bindParam(1, $uuid); $statement->execute(); } @@ -116,13 +115,13 @@ /** * Retrieves a registered peer record based on the given unique identifier or RegisteredPeerRecord object. * - * @param string|RegisteredPeerRecord $uuid The unique identifier of the registered peer, or an instance of RegisteredPeerRecord. - * @return RegisteredPeerRecord Returns a RegisteredPeerRecord object containing the peer's information. + * @param string|PeerRecord $uuid The unique identifier of the registered peer, or an instance of RegisteredPeerRecord. + * @return PeerRecord Returns a RegisteredPeerRecord object containing the peer's information. * @throws DatabaseOperationException If there is an error during the database operation. */ - public static function getPeer(string|RegisteredPeerRecord $uuid): RegisteredPeerRecord + public static function getPeer(string|PeerRecord $uuid): PeerRecord { - if($uuid instanceof RegisteredPeerRecord) + if($uuid instanceof PeerRecord) { $uuid = $uuid->getUuid(); } @@ -131,7 +130,7 @@ try { - $statement = Database::getConnection()->prepare('SELECT * FROM `registered_peers` WHERE uuid=?'); + $statement = Database::getConnection()->prepare('SELECT * FROM peers WHERE uuid=?'); $statement->bindParam(1, $uuid); $statement->execute(); @@ -142,7 +141,7 @@ throw new DatabaseOperationException(sprintf("The requested peer '%s' does not exist", $uuid)); } - return new RegisteredPeerRecord($result); + return new PeerRecord($result); } catch(Exception $e) { @@ -154,16 +153,16 @@ * Retrieves a peer record by the given username. * * @param PeerAddress $address The address of the peer to be retrieved. - * @return RegisteredPeerRecord|null The record of the peer associated with the given username. + * @return PeerRecord|null The record of the peer associated with the given username. * @throws DatabaseOperationException If there is an error while querying the database. */ - public static function getPeerByAddress(PeerAddress $address): ?RegisteredPeerRecord + public static function getPeerByAddress(PeerAddress $address): ?PeerRecord { Logger::getLogger()->verbose(sprintf("Retrieving peer %s from the database", $address->getAddress())); try { - $statement = Database::getConnection()->prepare('SELECT * FROM `registered_peers` WHERE username=? AND server=?'); + $statement = Database::getConnection()->prepare('SELECT * FROM peers WHERE username=? AND server=?'); $username = $address->getUsername(); $statement->bindParam(1, $username); $server = $address->getDomain(); @@ -184,7 +183,7 @@ return null; } - return new RegisteredPeerRecord($result); + return new PeerRecord($result); } catch(Exception $e) { @@ -223,13 +222,11 @@ try { - $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET display_name=?, updated=? WHERE uuid=?'); - $displayName = $peer->getDisplayName(); - $statement->bindParam(1, $displayName); + $statement = Database::getConnection()->prepare('UPDATE peers SET updated=? WHERE uuid=?'); $updated = new DateTime(); - $statement->bindParam(2, $updated); + $statement->bindParam(':updated', $updated); $uuid = $existingPeer->getUuid(); - $statement->bindParam(3, $uuid); + $statement->bindParam(':uuid', $uuid); $statement->execute(); } catch(PDOException $e) @@ -244,14 +241,12 @@ try { - $statement = Database::getConnection()->prepare('INSERT INTO `registered_peers` (uuid, username, server, display_name, enabled) VALUES (:uuid, :username, :server, :display_name, 1)'); + $statement = Database::getConnection()->prepare('INSERT INTO peers (uuid, username, server, enabled) VALUES (:uuid, :username, :server, 1)'); $statement->bindParam(':uuid', $uuid); $username = $peer->getAddress()->getUsername(); $statement->bindParam(':username', $username); $server = $peer->getAddress()->getDomain(); $statement->bindParam(':server', $server); - $displayName = $peer->getDisplayName(); - $statement->bindParam(':display_name', $displayName); $statement->execute(); } @@ -264,13 +259,13 @@ /** * Enables a peer identified by the given UUID or RegisteredPeerRecord. * - * @param string|RegisteredPeerRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer to be enabled. + * @param string|PeerRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer to be enabled. * @return void * @throws DatabaseOperationException If there is an error while updating the database. */ - public static function enablePeer(string|RegisteredPeerRecord $uuid): void + public static function enablePeer(string|PeerRecord $uuid): void { - if($uuid instanceof RegisteredPeerRecord) + if($uuid instanceof PeerRecord) { $uuid = $uuid->getUuid(); } @@ -279,7 +274,7 @@ try { - $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET enabled=1 WHERE uuid=?'); + $statement = Database::getConnection()->prepare('UPDATE peers SET enabled=1 WHERE uuid=?'); $statement->bindParam(1, $uuid); $statement->execute(); } @@ -292,13 +287,13 @@ /** * Disables the peer identified by the given UUID or RegisteredPeerRecord. * - * @param string|RegisteredPeerRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer. + * @param string|PeerRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer. * @return void * @throws DatabaseOperationException If there is an error while updating the peer's status in the database. */ - public static function disablePeer(string|RegisteredPeerRecord $uuid): void + public static function disablePeer(string|PeerRecord $uuid): void { - if($uuid instanceof RegisteredPeerRecord) + if($uuid instanceof PeerRecord) { $uuid = $uuid->getUuid(); } @@ -307,7 +302,7 @@ try { - $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET enabled=0 WHERE uuid=?'); + $statement = Database::getConnection()->prepare('UPDATE peers SET enabled=0 WHERE uuid=?'); $statement->bindParam(1, $uuid); $statement->execute(); } @@ -320,14 +315,14 @@ /** * Adds a specific flag to the peer identified by the given UUID or RegisteredPeerRecord. * - * @param string|RegisteredPeerRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer. + * @param string|PeerRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer. * @param PeerFlags|array $flags The flag or array of flags to be added to the peer. * @return void * @throws DatabaseOperationException If there is an error while updating the database. */ - public static function addFlag(string|RegisteredPeerRecord $uuid, PeerFlags|array $flags): void + public static function addFlag(string|PeerRecord $uuid, PeerFlags|array $flags): void { - if($uuid instanceof RegisteredPeerRecord) + if($uuid instanceof PeerRecord) { $uuid = $uuid->getUuid(); } @@ -349,7 +344,7 @@ try { $implodedFlags = implode(',', array_map(fn($flag) => $flag->name, $existingFlags)); - $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET flags=? WHERE uuid=?'); + $statement = Database::getConnection()->prepare('UPDATE peers SET flags=? WHERE uuid=?'); $statement->bindParam(1, $implodedFlags); $statement->bindParam(2, $uuid); $statement->execute(); @@ -363,12 +358,12 @@ /** * Removes a specific flag from the peer identified by the given UUID or RegisteredPeerRecord. * - * @param string|RegisteredPeerRecord $peer + * @param string|PeerRecord $peer * @param PeerFlags $flag The flag to be removed from the peer. * @return void * @throws DatabaseOperationException If there is an error while updating the database. */ - public static function removeFlag(string|RegisteredPeerRecord $peer, PeerFlags $flag): void + public static function removeFlag(string|PeerRecord $peer, PeerFlags $flag): void { if(is_string($peer)) { @@ -387,7 +382,7 @@ try { $implodedFlags = PeerFlags::toString($peer->getFlags()); - $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET flags=? WHERE uuid=?'); + $statement = Database::getConnection()->prepare('UPDATE peers SET flags=? WHERE uuid=?'); $statement->bindParam(1, $implodedFlags); $statement->bindParam(2, $registeredPeer); $statement->execute(); @@ -397,435 +392,4 @@ throw new DatabaseOperationException('Failed to remove the flag from the peer in the database', $e); } } - - /** - * Updates the display name of a registered peer based on the given unique identifier or RegisteredPeerRecord object. - * - * @param string|RegisteredPeerRecord $peer The unique identifier of the registered peer, or an instance of RegisteredPeerRecord. - * @param string $name The new display name to set to the user - * @throws DatabaseOperationException Thrown if there was an error while trying to update the display name - */ - public static function updateDisplayName(string|RegisteredPeerRecord $peer, string $name): void - { - if(empty($name)) - { - throw new InvalidArgumentException('The display name cannot be empty'); - } - - if(strlen($name) > 256) - { - throw new InvalidArgumentException('The display name cannot exceed 256 characters'); - } - - if(is_string($peer)) - { - $peer = self::getPeer($peer); - } - - if($peer->isExternal()) - { - throw new InvalidArgumentException('Cannot update the display name of an external peer'); - } - - Logger::getLogger()->verbose(sprintf("Updating display name of peer %s to %s", $peer->getUuid(), $name)); - - try - { - $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET display_name=? WHERE uuid=?'); - $statement->bindParam(1, $name); - $uuid = $peer->getUuid(); - $statement->bindParam(2, $uuid); - $statement->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to update the display name of the peer in the database', $e); - } - } - - /** - * Deletes the display name of a registered peer identified by a unique identifier or RegisteredPeerRecord object. - * - * @param string|RegisteredPeerRecord $peer The unique identifier of the registered peer, or an instance of RegisteredPeerRecord. - * @return void - * @throws InvalidArgumentException If the peer is external and its display name cannot be deleted. - * @throws DatabaseOperationException If there is an error during the database operation. - */ - public static function deleteDisplayName(string|RegisteredPeerRecord $peer): void - { - if(is_string($peer)) - { - $peer = self::getPeer($peer); - } - - if($peer->isExternal()) - { - throw new InvalidArgumentException('Cannot delete the display name of an external peer'); - } - - Logger::getLogger()->verbose(sprintf("Deleting display name of peer %s", $peer->getUuid())); - - try - { - $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET display_name=NULL WHERE uuid=?'); - $uuid = $peer->getUuid(); - $statement->bindParam(1, $uuid); - $statement->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to delete the display name of the peer in the database', $e); - } - } - - /** - * Updates the display picture of a registered peer in the database. - * - * @param string|RegisteredPeerRecord $peer The unique identifier of the peer or an instance of RegisteredPeerRecord. - * @param string $displayPictureData The raw jpeg data of the display picture. - * @return void - * @throws DatabaseOperationException If there is an error during the database operation. - */ - public static function updateDisplayPicture(string|RegisteredPeerRecord $peer, string $displayPictureData): void - { - if(empty($uuid)) - { - throw new InvalidArgumentException('The display picture UUID cannot be empty'); - } - - $uuid = Uuid::v4()->toRfc4122(); - $displayPicturePath = Configuration::getStorageConfiguration()->getUserDisplayImagesPath() . DIRECTORY_SEPARATOR . $uuid . '.jpeg'; - - // Delete the file if it already exists - if(file_exists($displayPicturePath)) - { - unlink($displayPicturePath); - } - - // Write the file contents & set the permissions - file_put_contents($displayPicturePath, $displayPictureData); - chmod($displayPicturePath, 0644); - - if(is_string($peer)) - { - $peer = self::getPeer($peer); - } - - // TODO: Handle for external peers, needs a way to resolve peers to their external counterparts - if($peer->isExternal()) - { - throw new InvalidArgumentException('Cannot update the display picture of an external peer'); - } - - Logger::getLogger()->verbose(sprintf("Updating display picture of peer %s to %s", $peer->getUuid(), $uuid)); - - try - { - $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET display_picture=? WHERE uuid=?'); - $statement->bindParam(1, $uuid); - $peerUuid = $peer->getUuid(); - $statement->bindParam(2, $peerUuid); - $statement->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to update the display picture of the peer in the database', $e); - } - } - - /** - * Deletes the display picture of a registered peer based on the given unique identifier or RegisteredPeerRecord object. - * - * @param string|RegisteredPeerRecord $peer The unique identifier of the registered peer, or an instance of RegisteredPeerRecord. - * @return void - * @throws InvalidArgumentException If the peer is external and its display picture cannot be deleted. - * @throws DatabaseOperationException If there is an error during the database operation. - */ - public static function deleteDisplayPicture(string|RegisteredPeerRecord $peer): void - { - if(is_string($peer)) - { - $peer = self::getPeer($peer); - } - - if($peer->isExternal()) - { - // TODO: Implement this - throw new InvalidArgumentException('Cannot delete the display picture of an external peer'); - } - - Logger::getLogger()->verbose(sprintf("Deleting display picture of peer %s", $peer->getUuid())); - - // Delete the file if it exists - $displayPicturePath = Configuration::getStorageConfiguration()->getUserDisplayImagesPath() . DIRECTORY_SEPARATOR . $peer->getDisplayPicture() . '.jpeg'; - if(file_exists($displayPicturePath)) - { - unlink($displayPicturePath); - } - - try - { - $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET display_picture=NULL WHERE uuid=?'); - $uuid = $peer->getUuid(); - $statement->bindParam(1, $uuid); - $statement->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to delete the display picture of the peer in the database', $e); - } - } - - /** - * Updates the email address of a registered peer. - * - * @param string|RegisteredPeerRecord $peer The unique identifier of the peer, or an instance of RegisteredPeerRecord. - * @param string $emailAddress The new email address to be assigned to the peer. - * @return void - * @throws InvalidArgumentException If the email address is empty, exceeds 256 characters, is not a valid email format, or if the peer is external. - * @throws DatabaseOperationException If there is an error during the database operation. - */ - public static function updateEmailAddress(string|RegisteredPeerRecord $peer, string $emailAddress): void - { - if(empty($emailAddress)) - { - throw new InvalidArgumentException('The email address cannot be empty'); - } - - if(strlen($emailAddress) > 256) - { - throw new InvalidArgumentException('The email address cannot exceed 256 characters'); - } - - if(filter_var($emailAddress, FILTER_VALIDATE_EMAIL) === false) - { - throw new InvalidArgumentException('The email address is not valid'); - } - - if(is_string($peer)) - { - $peer = self::getPeer($peer); - } - - if($peer->isExternal()) - { - throw new InvalidArgumentException('Cannot update the email address of an external peer'); - } - - Logger::getLogger()->verbose(sprintf("Updating email address of peer %s to %s", $peer->getUuid(), $emailAddress)); - - try - { - $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET email_address=? WHERE uuid=?'); - $statement->bindParam(1, $emailAddress); - $uuid = $peer->getUuid(); - $statement->bindParam(2, $uuid); - $statement->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to update the email address of the peer in the database', $e); - } - } - - /** - * Deletes the email address of a registered peer identified by either a unique identifier or a RegisteredPeerRecord object. - * - * @param string|RegisteredPeerRecord $peer The unique identifier of the registered peer, or an instance of RegisteredPeerRecord. - * @return void - * @throws InvalidArgumentException If the peer is external and its email address cannot be deleted. - * @throws DatabaseOperationException If there is an error during the database operation. - */ - public static function deleteEmailAddress(string|RegisteredPeerRecord $peer): void - { - if(is_string($peer)) - { - $peer = self::getPeer($peer); - } - - if($peer->isExternal()) - { - throw new InvalidArgumentException('Cannot delete the email address of an external peer'); - } - - Logger::getLogger()->verbose(sprintf("Deleting email address of peer %s", $peer->getUuid())); - - try - { - $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET email_address=NULL WHERE uuid=?'); - $uuid = $peer->getUuid(); - $statement->bindParam(1, $uuid); - $statement->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to delete the email address of the peer in the database', $e); - } - } - - /** - * Updates the phone number of the specified registered peer. - * - * @param string|RegisteredPeerRecord $peer The unique identifier of the registered peer, or an instance of RegisteredPeerRecord. - * @param string $phoneNumber The new phone number to be set for the peer. - * @return void - * @throws InvalidArgumentException If the phone number is empty, exceeds 16 characters, is invalid, or if the peer is external. - * @throws DatabaseOperationException If there is an error during the database operation. - */ - public static function updatePhoneNumber(string|RegisteredPeerRecord $peer, string $phoneNumber): void - { - if(empty($phoneNumber)) - { - throw new InvalidArgumentException('The phone number cannot be empty'); - } - - if(strlen($phoneNumber) > 16) - { - throw new InvalidArgumentException('The phone number cannot exceed 16 characters'); - } - - if(!Validator::validatePhoneNumber($phoneNumber)) - { - throw new InvalidArgumentException('The phone number is not valid'); - } - - if(is_string($peer)) - { - $peer = self::getPeer($peer); - } - - if($peer->isExternal()) - { - throw new InvalidArgumentException('Cannot update the phone number of an external peer'); - } - - Logger::getLogger()->verbose(sprintf("Updating phone number of peer %s to %s", $peer->getUuid(), $phoneNumber)); - - try - { - $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET phone_number=? WHERE uuid=?'); - $statement->bindParam(1, $phoneNumber); - $uuid = $peer->getUuid(); - $statement->bindParam(2, $uuid); - $statement->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to update the phone number of the peer in the database', $e); - } - } - - /** - * Deletes the phone number of a registered peer based on the given unique identifier or RegisteredPeerRecord object. - * - * @param string|RegisteredPeerRecord $peer The unique identifier of the registered peer, or an instance of RegisteredPeerRecord. - * @return void This method does not return a value. - * @throws InvalidArgumentException If the peer is external and its phone number cannot be deleted. - * @throws DatabaseOperationException If there is an error during the database operation. - */ - public static function deletePhoneNumber(string|RegisteredPeerRecord $peer): void - { - if(is_string($peer)) - { - $peer = self::getPeer($peer); - } - - if($peer->isExternal()) - { - throw new InvalidArgumentException('Cannot delete the phone number of an external peer'); - } - - Logger::getLogger()->verbose(sprintf("Deleting phone number of peer %s", $peer->getUuid())); - - try - { - $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET phone_number=NULL WHERE uuid=?'); - $uuid = $peer->getUuid(); - $statement->bindParam(1, $uuid); - $statement->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to delete the phone number of the peer in the database', $e); - } - } - - /** - * Sets the birthday of a registered peer record based on the provided date components. - * - * @param string|RegisteredPeerRecord $peer The unique identifier of the peer or an instance of RegisteredPeerRecord. - * @param int $month The month component of the birthday. - * @param int $day The day component of the birthday. - * @param int $year The year component of the birthday. - * @return void - * @throws InvalidArgumentException If the peer is external or the provided date is invalid. - * @throws DatabaseOperationException If there is an error during the database operation. - */ - public static function updateBirthday(string|RegisteredPeerRecord $peer, int $month, int $day, int $year): void - { - if(is_string($peer)) - { - $peer = self::getPeer($peer); - } - - if($peer->isExternal()) - { - throw new InvalidArgumentException('Cannot set the birthday of an external peer'); - } - - if(!Validator::validateDate($month, $day, $year)) - { - throw new InvalidArgumentException('The provided date is not valid'); - } - - Logger::getLogger()->verbose(sprintf("Setting birthday of peer %s to %d-%d-%d", $peer->getUuid(), $year, $month, $day)); - - try - { - $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET birthday=? WHERE uuid=?'); - $birthday = (new DateTime())->setDate($year, $month, $day)->format('Y-m-d'); - $statement->bindParam(1, $birthday); - $uuid = $peer->getUuid(); - $statement->bindParam(2, $uuid); - $statement->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to set the birthday of the peer in the database', $e); - } - } - - /** - * Deletes the birthday of a registered peer based on the given unique identifier or RegisteredPeerRecord object. - * - * @param string|RegisteredPeerRecord $peer The unique identifier of the registered peer, or an instance of RegisteredPeerRecord. - * @throws InvalidArgumentException If the peer is marked as external and cannot have its birthday deleted. - * @throws DatabaseOperationException If there is an error during the database operation. - */ - public static function deleteBirthday(string|RegisteredPeerRecord $peer): void - { - if(is_string($peer)) - { - $peer = self::getPeer($peer); - } - - if($peer->isExternal()) - { - throw new InvalidArgumentException('Cannot delete the birthday of an external peer'); - } - - Logger::getLogger()->verbose(sprintf("Deleting birthday of peer %s", $peer->getUuid())); - - try - { - $statement = Database::getConnection()->prepare('UPDATE `registered_peers` SET birthday=NULL WHERE uuid=?'); - $uuid = $peer->getUuid(); - $statement->bindParam(1, $uuid); - $statement->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to delete the birthday of the peer in the database', $e); - } - } } \ No newline at end of file diff --git a/src/Socialbox/Managers/SessionManager.php b/src/Socialbox/Managers/SessionManager.php index 6bf5071..dc87a4d 100644 --- a/src/Socialbox/Managers/SessionManager.php +++ b/src/Socialbox/Managers/SessionManager.php @@ -16,7 +16,7 @@ use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\StandardException; - use Socialbox\Objects\Database\RegisteredPeerRecord; + use Socialbox\Objects\Database\PeerRecord; use Socialbox\Objects\Database\SessionRecord; use Socialbox\Objects\KeyPair; use Symfony\Component\Uid\Uuid; @@ -26,7 +26,7 @@ /** * Creates a new session for a given peer and client details, and stores it in the database. * - * @param RegisteredPeerRecord $peer The peer record for which the session is being created. + * @param PeerRecord $peer The peer record for which the session is being created. * @param string $clientName The name of the client application. * @param string $clientVersion The version of the client application. * @param string $clientPublicSigningKey The client's public signing key, which must be a valid Ed25519 key. @@ -36,7 +36,7 @@ * @throws InvalidArgumentException If the provided public signing key or encryption key is invalid. * @throws DatabaseOperationException If there is an error during the session creation in the database. */ - public static function createSession(RegisteredPeerRecord $peer, string $clientName, string $clientVersion, string $clientPublicSigningKey, string $clientPublicEncryptionKey, KeyPair $serverEncryptionKeyPair): string + public static function createSession(PeerRecord $peer, string $clientName, string $clientVersion, string $clientPublicSigningKey, string $clientPublicEncryptionKey, KeyPair $serverEncryptionKeyPair): string { if($clientPublicSigningKey === '' || Cryptography::validatePublicSigningKey($clientPublicSigningKey) === false) { diff --git a/src/Socialbox/Objects/ClientRequest.php b/src/Socialbox/Objects/ClientRequest.php index d9f957a..5d28c4f 100644 --- a/src/Socialbox/Objects/ClientRequest.php +++ b/src/Socialbox/Objects/ClientRequest.php @@ -10,7 +10,7 @@ use Socialbox\Exceptions\RequestException; use Socialbox\Managers\RegisteredPeerManager; use Socialbox\Managers\SessionManager; - use Socialbox\Objects\Database\RegisteredPeerRecord; + use Socialbox\Objects\Database\PeerRecord; use Socialbox\Objects\Database\SessionRecord; class ClientRequest @@ -182,10 +182,10 @@ /** * Retrieves the associated peer for the current session. * - * @return RegisteredPeerRecord|null Returns the associated RegisteredPeerRecord if available, or null if no session exists. + * @return PeerRecord|null Returns the associated RegisteredPeerRecord if available, or null if no session exists. * @throws DatabaseOperationException Thrown if an error occurs while retrieving the peer. */ - public function getPeer(): ?RegisteredPeerRecord + public function getPeer(): ?PeerRecord { $session = $this->getSession(); diff --git a/src/Socialbox/Objects/Database/PeerInformationFieldRecord.php b/src/Socialbox/Objects/Database/PeerInformationFieldRecord.php new file mode 100644 index 0000000..2647476 --- /dev/null +++ b/src/Socialbox/Objects/Database/PeerInformationFieldRecord.php @@ -0,0 +1,105 @@ +peerUuid = $data['peer_uuid']; + $this->propertyName = InformationFieldName::from($data['property_name']); + $this->propertyValue = $data['property_value']; + $this->privacyState = PrivacyState::from($data['privacy_state']); + } + + /** + * @return string + */ + public function getPeerUuid(): string + { + return $this->peerUuid; + } + + /** + * @return InformationFieldName + */ + public function getPropertyName(): InformationFieldName + { + return $this->propertyName; + } + + /** + * @return string + */ + public function getPropertyValue(): string + { + return $this->propertyValue; + } + + /** + * @return PrivacyState + */ + public function getPrivacyState(): PrivacyState + { + return $this->privacyState; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): PeerInformationFieldRecord + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'peer_uuid' => $this->peerUuid, + 'property_name' => $this->propertyName->value, + 'property_value' => $this->propertyValue, + 'privacy_state' => $this->privacyState->value + ]; + } + + /** + * Converts the record to a standard InformationField object + * + * @return InformationField + */ + public function toInformationField(): InformationField + { + return new InformationField([ + 'name' => $this->propertyName->value, + 'value' => $this->propertyValue + ]); + } + + /** + * Converts the record to a standard InformationFieldState object + * + * @return InformationFieldState + */ + public function toInformationFieldState(): InformationFieldState + { + return new InformationFieldState([ + 'name' => $this->propertyName->value, + 'value' => $this->propertyValue, + 'privacy_state' => $this->privacyState->value + ]); + } + } \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/RegisteredPeerRecord.php b/src/Socialbox/Objects/Database/PeerRecord.php similarity index 70% rename from src/Socialbox/Objects/Database/RegisteredPeerRecord.php rename to src/Socialbox/Objects/Database/PeerRecord.php index aa69195..6143295 100644 --- a/src/Socialbox/Objects/Database/RegisteredPeerRecord.php +++ b/src/Socialbox/Objects/Database/PeerRecord.php @@ -10,16 +10,11 @@ use Socialbox\Objects\Standard\Peer; use Socialbox\Objects\Standard\SelfUser; - class RegisteredPeerRecord implements SerializableInterface + class PeerRecord implements SerializableInterface { private string $uuid; private string $username; private string $server; - private ?string $displayName; - private ?string $displayPicture; - private ?string $emailAddress; - private ?string $phoneNumber; - private ?DateTime $birthday; /** * @var PeerFlags[] */ @@ -38,31 +33,6 @@ $this->uuid = $data['uuid']; $this->username = $data['username']; $this->server = $data['server']; - $this->displayName = $data['display_name'] ?? null; - $this->displayPicture = $data['display_picture'] ?? null; - $this->emailAddress = $data['email_address'] ?? null; - $this->phoneNumber = $data['phone_number'] ?? null; - - if(!isset($data['birthday'])) - { - $this->birthday = null; - } - elseif(is_int($data['birthday'])) - { - $this->birthday = (new DateTime())->setTimestamp($data['birthday']); - } - elseif(is_string($data['birthday'])) - { - $this->birthday = new DateTime($data['birthday']); - } - elseif($data['birthday'] instanceof DateTime) - { - $this->birthday = $data['birthday']; - } - else - { - throw new InvalidArgumentException("The birthday field must be a valid timestamp or date string."); - } if($data['flags']) { @@ -150,56 +120,6 @@ return sprintf("%s@%s", $this->username, Configuration::getInstanceConfiguration()->getDomain()); } - /** - * Retrieves the display name. - * - * @return string|null The display name if set, or null otherwise. - */ - public function getDisplayName(): ?string - { - return $this->displayName; - } - - /** - * Retrieves the display picture. - * - * @return string|null The display picture if set, or null otherwise. - */ - public function getDisplayPicture(): ?string - { - return $this->displayPicture; - } - - /** - * Retrieves the email address. - * - * @return string|null The email address if set, or null otherwise. - */ - public function getEmailAddress(): ?string - { - return $this->emailAddress; - } - - /** - * Retrieves the phone number. - * - * @return string|null The phone number if set, or null otherwise. - */ - public function getPhoneNumber(): ?string - { - return $this->phoneNumber; - } - - /** - * Retrieves the birthday. - * - * @return DateTime|null The birthday if set, or null otherwise. - */ - public function getBirthday(): ?DateTime - { - return $this->birthday; - } - /** * Retrieves the flags. * @@ -292,9 +212,9 @@ */ public function toStandardPeer(): Peer { + // TODO: TO be updated return Peer::fromArray([ 'address' => $this->getAddress(), - 'display_name' => $this->displayName, 'flags' => array_map(fn(PeerFlags $flag) => $flag->value, $this->flags), 'registered' => $this->created->getTimestamp() ]); @@ -317,11 +237,6 @@ 'uuid' => $this->uuid, 'username' => $this->username, 'server' => $this->server, - 'display_name' => $this->displayName, - 'display_picture' => $this->displayPicture, - 'email_address' => $this->emailAddress, - 'phone_number' => $this->phoneNumber, - 'birthday' => $this->birthday?->getTimestamp(), 'flags' => PeerFlags::toString($this->flags), 'enabled' => $this->enabled, 'created' => $this->created, diff --git a/src/Socialbox/Objects/Standard/InformationField.php b/src/Socialbox/Objects/Standard/InformationField.php new file mode 100644 index 0000000..3cdc949 --- /dev/null +++ b/src/Socialbox/Objects/Standard/InformationField.php @@ -0,0 +1,53 @@ +name = InformationFieldName::from($data['name']); + $this->value = $data['value']; + } + + /** + * @return InformationFieldName + */ + public function getName(): InformationFieldName + { + return $this->name; + } + + /** + * @return string + */ + public function getValue(): string + { + return $this->value; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): InformationField + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'name' => $this->name->getValue(), + 'value' => $this->value, + ]; + } + } \ No newline at end of file diff --git a/src/Socialbox/Objects/Standard/InformationFieldState.php b/src/Socialbox/Objects/Standard/InformationFieldState.php new file mode 100644 index 0000000..d95b7e6 --- /dev/null +++ b/src/Socialbox/Objects/Standard/InformationFieldState.php @@ -0,0 +1,62 @@ +name = InformationFieldName::from($data['name']); + $this->value = $data['value']; + $this->privacyState = PrivacyState::from($data['privacy_state']); + } + + /** + * @return InformationFieldName + */ + public function getName(): InformationFieldName + { + return $this->name; + } + + /** + * @return string + */ + public function getValue(): string + { + return $this->value; + } + + public function getPrivacyState(): PrivacyState + { + return $this->privacyState; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): InformationFieldState + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'name' => $this->name->value, + 'value' => $this->value, + 'privacy_state' => $this->privacyState->value + ]; + } + } \ No newline at end of file diff --git a/src/Socialbox/Objects/Standard/SelfUser.php b/src/Socialbox/Objects/Standard/SelfUser.php index 443f8dc..1d862bd 100644 --- a/src/Socialbox/Objects/Standard/SelfUser.php +++ b/src/Socialbox/Objects/Standard/SelfUser.php @@ -5,7 +5,7 @@ namespace Socialbox\Objects\Standard; use DateTime; use Socialbox\Enums\Flags\PeerFlags; use Socialbox\Interfaces\SerializableInterface; -use Socialbox\Objects\Database\RegisteredPeerRecord; +use Socialbox\Objects\Database\PeerRecord; class SelfUser implements SerializableInterface { @@ -23,11 +23,11 @@ class SelfUser implements SerializableInterface /** * Constructor for initializing the object with provided data. * - * @param array|RegisteredPeerRecord $data Data array containing initial values for object properties. + * @param array|PeerRecord $data Data array containing initial values for object properties. */ - public function __construct(array|RegisteredPeerRecord $data) + public function __construct(array|PeerRecord $data) { - if($data instanceof RegisteredPeerRecord) + if($data instanceof PeerRecord) { $this->uuid = $data->getUuid(); $this->enabled = $data->isEnabled(); diff --git a/src/Socialbox/SocialClient.php b/src/Socialbox/SocialClient.php index 90e11f1..252b9ee 100644 --- a/src/Socialbox/SocialClient.php +++ b/src/Socialbox/SocialClient.php @@ -7,7 +7,9 @@ use Socialbox\Classes\Cryptography; use Socialbox\Classes\RpcClient; use Socialbox\Classes\Utilities; + use Socialbox\Enums\PrivacyState; use Socialbox\Enums\StandardMethods; + use Socialbox\Enums\Types\InformationFieldName; use Socialbox\Exceptions\CryptographyException; use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\ResolutionException; @@ -15,6 +17,7 @@ use Socialbox\Objects\ExportedSession; use Socialbox\Objects\PeerAddress; use Socialbox\Objects\RpcRequest; + use Socialbox\Objects\Standard\InformationField; use Socialbox\Objects\Standard\Peer; use Socialbox\Objects\Standard\ServerDocument; use Socialbox\Objects\Standard\SessionState; @@ -484,132 +487,66 @@ } /** - * Sets the display name in the settings by sending a remote procedure call request. + * Updates the user's OTP settings by sending a remote procedure call request. * - * @param string $displayName The new display name to be set. - * @return true Returns true upon successful update of the display name. + * @param InformationFieldName $field The field to be updated. + * @param string $value The value to be set. + * @param PrivacyState|null $privacy The privacy state to be set. Default is null. + * @return bool True if the OTP was successfully updated, false otherwise. * @throws RpcException Thrown if the RPC request fails. */ - public function settingsSetDisplayName(string $displayName): true + public function settingsAddInformationField(InformationFieldName $field, string $value, ?PrivacyState $privacy=null): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_SET_DISPLAY_NAME, Utilities::randomCrc32(), [ - 'name' => $displayName - ]) + new RpcRequest(StandardMethods::SETTINGS_ADD_INFORMATION_FIELD, Utilities::randomCrc32(), [ + 'field' => $field->value, + 'value' => $value, + 'privacy' => $privacy?->value + ]), )->getResponse()->getResult(); } /** + * Deletes an information field by sending a remote procedure call request. * - */ - public function settingsDeleteDisplayName(): true - { - return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_DELETE_DISPLAY_NAME, Utilities::randomCrc32()) - )->getResponse()->getResult(); - } - - /** - * Updates the display picture by sending a remote procedure call request with the specified file identifier. - * - * @param string $fileId The identifier of the file to be set as the display picture. - * @return true Returns true upon successful update of the + * @param InformationField $field The field to be deleted. + * @return bool True if the field was successfully deleted, false otherwise. * @throws RpcException Thrown if the RPC request fails. */ - public function settingsSetDisplayPicture(string $fileId): true + public function settingsDeleteInformationField(InformationField $field): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_SET_DISPLAY_PICTURE, Utilities::randomCrc32(), [ - 'file_id' => $fileId - ]) + new RpcRequest(StandardMethods::SETTINGS_DELETE_INFORMATION_FIELD, Utilities::randomCrc32()) )->getResponse()->getResult(); } /** - * Updates the email address for the settings by sending a remote procedure call request. + * Updates an information field by sending a remote procedure call request. * - * @param string $emailAddress The new email address to set. - * @return true Returns true if the email address was successfully updated. + * @param InformationField $field The field to be updated. + * @param string $value The value to be set. + * @return bool True if the field was successfully updated, false otherwise. * @throws RpcException Thrown if the RPC request fails. */ - public function settingsSetEmail(string $emailAddress): true + public function settingsUpdateInformationField(InformationField $field, string $value): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_SET_EMAIL, Utilities::randomCrc32(), [ - 'email_address' => $emailAddress - ]) + new RpcRequest(StandardMethods::SETTINGS_UPDATE_INFORMATION_FIELD, Utilities::randomCrc32()) )->getResponse()->getResult(); } /** - * Deletes the email associated with the user settings by sending a remote procedure call request. + * Updates the privacy of an information field by sending a remote procedure call request. * - * @return true Returns true if the email deletion request is successful. - * @throws RpcException - */ - public function settingsDeleteEmail(): true - { - return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_DELETE_EMAIL, Utilities::randomCrc32()) - )->getResponse()->getResult(); - } - - /** - * Updates the phone number in the settings by sending a remote procedure call request. - * - * @param string $phoneNumber The phone number to be set in the settings. - * @return true Returns true if the operation was successful + * @param InformationField $field The field to be updated. + * @param PrivacyState $privacy The privacy state to be set. + * @return bool True if the privacy was successfully updated, false otherwise. * @throws RpcException Thrown if the RPC request fails. */ - public function settingsSetPhone(string $phoneNumber): true + public function settingsUpdateInformationPrivacy(InformationField $field, PrivacyState $privacy): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_SET_DISPLAY_NAME, Utilities::randomCrc32(), [ - 'phone_number' => $phoneNumber - ]) - )->getResponse()->getResult(); - } - - /** - * - */ - public function settingsDeletePhone(): true - { - return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_DELETE_PHONE, Utilities::randomCrc32()) - )->getResponse()->getResult(); - } - - /** - * Updates the user's birthday by sending a remote procedure call request with the specified date. - * - * @param int $year The year of the user's birthday. - * @param int $month The month of the user's birthday. - * @param int $day The day of the user's birthday. - * @return true Returns true if the birthday was successfully updated. - * @throws RpcException Thrown if the RPC request fails. - */ - public function settingsSetBirthday(int $year, int $month, int $day): true - { - return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_SET_BIRTHDAY, Utilities::randomCrc32(), [ - 'year' => $year, - 'month' => $month, - 'day' => $day - ]) - )->getResponse()->getResult(); - } - - /** - * Deletes the saved birthday setting by sending a remote procedure call request. - * - * @return true Returns true if the birthday deletion request is successful. - * @throws RpcException Thrown if the RPC request fails. - */ - public function deleteBirthday(): true - { - return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_DELETE_BIRTHDAY, Utilities::randomCrc32()) + new RpcRequest(StandardMethods::SETTINGS_UPDATE_INFORMATION_PRIVACY, Utilities::randomCrc32()) )->getResponse()->getResult(); } diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index 133665d..d4383c9 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -27,7 +27,7 @@ use Socialbox\Managers\RegisteredPeerManager; use Socialbox\Managers\SessionManager; use Socialbox\Objects\ClientRequest; - use Socialbox\Objects\Database\RegisteredPeerRecord; + use Socialbox\Objects\Database\PeerRecord; use Socialbox\Objects\PeerAddress; use Socialbox\Objects\Standard\Peer; use Socialbox\Objects\Standard\ServerInformation; @@ -836,7 +836,7 @@ throw new StandardException('The requested peer was not found', StandardError::PEER_NOT_FOUND); } - if($registeredPeer instanceof RegisteredPeerRecord) + if($registeredPeer instanceof PeerRecord) { $registeredPeer = $registeredPeer->toStandardPeer(); } diff --git a/tests/Socialbox/SocialClientTest.php b/tests/Socialbox/SocialClientTest.php index 43d6505..b4e143b 100644 --- a/tests/Socialbox/SocialClientTest.php +++ b/tests/Socialbox/SocialClientTest.php @@ -5,6 +5,7 @@ use PHPUnit\Framework\TestCase; use Socialbox\Classes\ServerResolver; use Socialbox\Enums\Flags\SessionFlags; + use Socialbox\Enums\Types\InformationFieldName; class SocialClientTest extends TestCase { @@ -45,7 +46,7 @@ { $client = new SocialClient(self::generateUsername($domain)); $client->settingsSetPassword("password"); - $client->settingsSetDisplayName("Example User"); + $client->settingsAddInformationField(InformationFieldName::DISPLAY_NAME, "Example User"); return $client; } @@ -62,7 +63,7 @@ // Check progressive session state $this->assertTrue($coffeeClient->settingsSetPassword('coffeePassword')); $this->assertFalse($coffeeClient->getSessionState()->containsFlag(SessionFlags::SET_PASSWORD)); - $this->assertTrue($coffeeClient->settingsSetDisplayName('Coffee User')); + $this->assertTrue($coffeeClient->settingsAddInformationField(InformationFieldName::DISPLAY_NAME, 'Coffee User')); $this->assertFalse($coffeeClient->getSessionState()->containsFlag(SessionFlags::SET_DISPLAY_NAME)); $this->assertFalse($coffeeClient->getSessionState()->containsFlag(SessionFlags::REGISTRATION_REQUIRED)); From a826f4f3a9305e329173f2f572cfd41be7aa638b Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 24 Jan 2025 15:20:19 -0500 Subject: [PATCH 219/420] Updated Standard Peer object to include standard information fields and updated PeerInformationManager to retrieve filtered fields per privacy condition --- .../Managers/PeerInformationManager.php | 55 ++++++++++++++----- .../Standard/InformationFieldState.php | 13 +++++ src/Socialbox/Objects/Standard/Peer.php | 40 +++++++++++--- 3 files changed, 86 insertions(+), 22 deletions(-) diff --git a/src/Socialbox/Managers/PeerInformationManager.php b/src/Socialbox/Managers/PeerInformationManager.php index d8b0186..f529b19 100644 --- a/src/Socialbox/Managers/PeerInformationManager.php +++ b/src/Socialbox/Managers/PeerInformationManager.php @@ -20,9 +20,7 @@ * @param InformationFieldName $property The name of the property to add. * @param string $value The value of the property to add. * @param PrivacyState|null $privacyState The privacy state of the property to add. - * * @return void - * * @throws DatabaseOperationException Thrown if the operation fails. */ public static function addField(string|PeerRecord $peerUuid, InformationFieldName $property, string $value, ?PrivacyState $privacyState=null): void @@ -70,9 +68,7 @@ * @param string|PeerRecord $peerUuid The UUID of the peer to update the property for. * @param InformationFieldName $property The name of the property to update. * @param string $value The new value of the property. - * * @return void - * * @throws DatabaseOperationException Thrown if the operation fails. */ public static function updateField(string|PeerRecord $peerUuid, InformationFieldName $property, string $value): void @@ -108,9 +104,7 @@ * @param string|PeerRecord $peerUuid The UUID of the peer to update the privacy state for. * @param InformationFieldName $property The name of the property to update the privacy state for. * @param PrivacyState $privacyState The new privacy state of the property. - * * @return void - * * @throws DatabaseOperationException Thrown if the operation fails. */ public static function updatePrivacyState(string|PeerRecord $peerUuid, InformationFieldName $property, PrivacyState $privacyState): void @@ -146,9 +140,7 @@ * * @param string|PeerRecord $peerUuid The UUID of the peer to check for the property. * @param InformationFieldName $property The name of the property to check for. - * * @return bool - * * @throws DatabaseOperationException Thrown if the operation fails. */ public static function fieldExists(string|PeerRecord $peerUuid, InformationFieldName $property): bool @@ -179,9 +171,7 @@ * * @param string|PeerRecord $peerUuid The UUID of the peer to get the property from. * @param InformationFieldName $property The name of the property to get. - * * @return PeerInformationFieldRecord - * * @throws DatabaseOperationException Thrown if the operation fails. */ public static function getField(string|PeerRecord $peerUuid, InformationFieldName $property): PeerInformationFieldRecord @@ -217,9 +207,7 @@ * Gets all properties from a peer's information record. * * @param string|PeerRecord $peerUuid The UUID of the peer to get the properties from. - * * @return PeerInformationFieldRecord[] - * * @throws DatabaseOperationException Thrown if the operation fails. */ public static function getFields(string|PeerRecord $peerUuid): array @@ -249,14 +237,53 @@ } } + /** + * Gets all properties from a peer's information record that match the provided privacy filters. + * + * @param string|PeerRecord $peerUuid The UUID of the peer to get the properties from. + * @param PrivacyState[] $privacyFilters The privacy filters to apply. + * @return PeerInformationFieldRecord[] The filtered properties. + * @throws DatabaseOperationException Thrown if the operation fails. + */ + public static function getFilteredFields(string|PeerRecord $peerUuid, array $privacyFilters): array + { + if($peerUuid instanceof PeerRecord) + { + $peerUuid = $peerUuid->getUuid(); + } + + $results = []; + /** @var PrivacyState $privacyState */ + foreach($privacyFilters as $privacyState) + { + try + { + $stmt = Database::getConnection()->prepare('SELECT * FROM peer_information WHERE peer_uuid=:peer_uuid AND privacy_state=:privacy_state'); + $stmt->bindValue(':peer_uuid', $peerUuid); + $stmt->bindValue(':privacy_state', $privacyState->value); + $stmt->execute(); + $results = array_merge($results, $stmt->fetchAll()); + } + catch(PDOException $e) + { + throw new DatabaseOperationException(sprintf('Failed to get properties for peer %s with privacy state %s', $peerUuid, $privacyState->value), $e); + } + } + + if(!$results) + { + return []; + } + + return array_map(fn($result) => PeerInformationFieldRecord::fromArray($result), $results); + } + /** * Deletes a property from a peer's information record. * * @param string|PeerRecord $peerUuid The UUID of the peer to delete the property from. * @param InformationFieldName $property The name of the property to delete. - * * @return void - * * @throws DatabaseOperationException Thrown if the operation fails. */ public static function deleteField(string|PeerRecord $peerUuid, InformationFieldName $property): void diff --git a/src/Socialbox/Objects/Standard/InformationFieldState.php b/src/Socialbox/Objects/Standard/InformationFieldState.php index d95b7e6..3d502de 100644 --- a/src/Socialbox/Objects/Standard/InformationFieldState.php +++ b/src/Socialbox/Objects/Standard/InformationFieldState.php @@ -59,4 +59,17 @@ 'privacy_state' => $this->privacyState->value ]; } + + /** + * Converts the object to an InformationField instance. + * + * @return InformationField Returns the converted InformationField instance. + */ + public function toInformationField(): InformationField + { + return new InformationField([ + 'name' => $this->name->value, + 'value' => $this->value, + ]); + } } \ No newline at end of file diff --git a/src/Socialbox/Objects/Standard/Peer.php b/src/Socialbox/Objects/Standard/Peer.php index e8d839c..b9dcea2 100644 --- a/src/Socialbox/Objects/Standard/Peer.php +++ b/src/Socialbox/Objects/Standard/Peer.php @@ -4,12 +4,16 @@ use InvalidArgumentException; use Socialbox\Interfaces\SerializableInterface; + use Socialbox\Objects\Database\PeerInformationFieldRecord; use Socialbox\Objects\PeerAddress; class Peer implements SerializableInterface { private PeerAddress $address; - private string $displayName; + /** + * @var InformationField[] + */ + private array $informationFields; private array $flags; private int $registered; @@ -27,7 +31,6 @@ */ public function __construct(array $data) { - // TODO: Bug: PHP message: PHP Warning: Undefined array key "address" in /var/ncc/packages/net.nosial.socialbox=1.0.0/bin/src/Socialbox/Objects/Standard/Peer.php on line 28 if(is_string($data['address'])) { $this->address = PeerAddress::fromAddress($data['address']); @@ -41,7 +44,28 @@ throw new InvalidArgumentException('Invalid address value type, got type ' . gettype($data['address'])); } - $this->displayName = $data['display_name']; + $informationFields = []; + foreach($data['information_fields'] as $field) + { + if($field instanceof PeerInformationFieldRecord) + { + $informationFields[] = $field->toInformationField(); + } + elseif($field instanceof InformationFieldState) + { + $informationFields[] = $field->toInformationField(); + } + elseif(is_array($field)) + { + $informationFields[] = new InformationField($field); + } + else + { + throw new InvalidArgumentException('Invalid information field type, got type ' . gettype($field)); + } + } + + $this->informationFields = $informationFields; $this->flags = $data['flags']; $this->registered = $data['registered']; } @@ -57,13 +81,13 @@ } /** - * Retrieves the display name of the entity. + * Retrieves the information fields associated with the peer. * - * @return string The display name associated with the entity. + * @return InformationField[] An array containing the information fields. */ - public function getDisplayName(): string + public function getInformationFields(): array { - return $this->displayName; + return $this->informationFields; } /** @@ -101,7 +125,7 @@ { return [ 'address' => $this->address->getAddress(), - 'display_name' => $this->displayName, + 'information_fields' => array_map(fn($field) => $field->toArray(), $this->informationFields), 'flags' => $this->flags, 'registered' => $this->registered ]; From 71d921def58492213eed227a3bf798ea708c918b Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 24 Jan 2025 15:20:42 -0500 Subject: [PATCH 220/420] Updated PhpDoc --- src/Socialbox/Objects/Standard/InformationFieldState.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Socialbox/Objects/Standard/InformationFieldState.php b/src/Socialbox/Objects/Standard/InformationFieldState.php index 3d502de..c4f7f41 100644 --- a/src/Socialbox/Objects/Standard/InformationFieldState.php +++ b/src/Socialbox/Objects/Standard/InformationFieldState.php @@ -12,6 +12,14 @@ private string $value; private PrivacyState $privacyState; + /** + * Constructor method. + * + * @param array $data An associative array containing the keys: + * - 'name': The name of the information field. + * - 'value': The value of the information field. + * - 'privacy_state': The privacy state of the information field. + */ public function __construct(array $data) { $this->name = InformationFieldName::from($data['name']); From baff154b05434837f5d72b4ed3245cc92256e976 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 24 Jan 2025 15:21:06 -0500 Subject: [PATCH 221/420] Added missing import --- src/Socialbox/Enums/StandardMethods.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index efd1344..b01853c 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -22,6 +22,7 @@ use Socialbox\Classes\StandardMethods\SettingsDeleteInformationField; use Socialbox\Classes\StandardMethods\SettingsDeleteOtp; use Socialbox\Classes\StandardMethods\SettingsDeletePassword; + use Socialbox\Classes\StandardMethods\SettingsGetInformationFields; use Socialbox\Classes\StandardMethods\SettingsGetSigningKeys; use Socialbox\Classes\StandardMethods\SettingsSetOtp; use Socialbox\Classes\StandardMethods\SettingsSetPassword; From 6401e46bc0fcc071ba155ca3758161c337d527dc Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 24 Jan 2025 15:21:58 -0500 Subject: [PATCH 222/420] Made registration stage always allow information field updates --- src/Socialbox/Enums/StandardMethods.php | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index b01853c..4c445ee 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -293,7 +293,11 @@ return []; } - $methods = []; + $methods = [ + self::SETTINGS_ADD_INFORMATION_FIELD, + self::SETTINGS_UPDATE_INFORMATION_FIELD, + self::SETTINGS_DELETE_INFORMATION_FIELD + ]; // If the flag `VER_PRIVACY_POLICY` is set, then the user can accept the privacy policy if($session->flagExists(SessionFlags::VER_PRIVACY_POLICY)) @@ -332,21 +336,6 @@ $methods[] = self::SETTINGS_SET_OTP; } - // If the flag `SET_DISPLAY_NAME` is set, then the user has to set a display name - if($session->flagExists([ - SessionFlags::SET_DISPLAY_NAME, - SessionFlags::SET_FIRST_NAME, - SessionFlags::SET_MIDDLE_NAME, - SessionFlags::SET_LAST_NAME, - SessionFlags::SET_BIRTHDAY, - SessionFlags::SET_PHONE, - SessionFlags::SET_EMAIL, - SessionFlags::SET_URL - ])) - { - $methods[] = self::SETTINGS_ADD_INFORMATION_FIELD; - } - return $methods; } From e2a0e2f27f9196338012fc29c3a68a235493bc5b Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 24 Jan 2025 15:25:06 -0500 Subject: [PATCH 223/420] Updated error messages --- .../Classes/StandardMethods/AddressBookGetContacts.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/AddressBookGetContacts.php b/src/Socialbox/Classes/StandardMethods/AddressBookGetContacts.php index 46122ab..2abffb0 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBookGetContacts.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBookGetContacts.php @@ -23,9 +23,9 @@ if($rpcRequest->containsParameter('limit')) { $limit = (int)$rpcRequest->getParameter('limit'); - if($limit < 0) + if($limit < 1) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Invalid limit'); + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Invalid limit, must be greater than 0'); } $limit = min($limit, Configuration::getPoliciesConfiguration()->getGetContactsLimit()); @@ -37,7 +37,7 @@ $page = (int)$rpcRequest->getParameter('page'); if($page < 0) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Invalid page'); + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Invalid page, must be greater than or equal to 0'); } $page = max($page, 0); From aba9adf9169ba5c8f70df58d77a565c160a5d286 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 24 Jan 2025 19:12:03 -0500 Subject: [PATCH 224/420] Added the method AddressBookGetContact and minor improvements in other implementations --- .../StandardMethods/AddressBookAddContact.php | 2 +- .../StandardMethods/AddressBookGetContact.php | 53 +++++++++++++++++++ .../AddressBookGetContacts.php | 7 +-- src/Socialbox/Socialbox.php | 8 ++- 4 files changed, 62 insertions(+), 8 deletions(-) create mode 100644 src/Socialbox/Classes/StandardMethods/AddressBookGetContact.php diff --git a/src/Socialbox/Classes/StandardMethods/AddressBookAddContact.php b/src/Socialbox/Classes/StandardMethods/AddressBookAddContact.php index 39b70d6..70442ca 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBookAddContact.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBookAddContact.php @@ -24,7 +24,7 @@ { if(!$rpcRequest->containsParameter('peer')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing required \'peer\' parameter'); + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing required peer parameter'); } try diff --git a/src/Socialbox/Classes/StandardMethods/AddressBookGetContact.php b/src/Socialbox/Classes/StandardMethods/AddressBookGetContact.php new file mode 100644 index 0000000..8bb51d5 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/AddressBookGetContact.php @@ -0,0 +1,53 @@ +containsParameter('peer')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing required peer parameter'); + } + + try + { + $address = PeerAddress::fromAddress($rpcRequest->getParameter('peer')); + } + catch(InvalidArgumentException $e) + { + throw new StandardException('Invalid peer address', StandardError::RPC_INVALID_ARGUMENTS, $e); + } + + try + { + if(!ContactManager::isContact($request->getPeer(), $address)) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'Contact does not exist'); + } + + $contact = ContactManager::getContact($request->getPeer(), $address); + } + catch(DatabaseOperationException $e) + { + throw new StandardException('Failed to get contact', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse($contact->toStandard()); + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/AddressBookGetContacts.php b/src/Socialbox/Classes/StandardMethods/AddressBookGetContacts.php index 2abffb0..ed355d1 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBookGetContacts.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBookGetContacts.php @@ -52,12 +52,7 @@ throw new StandardException('Failed to get contacts', StandardError::INTERNAL_SERVER_ERROR, $e); } - $results = []; - foreach($contacts as $contact) - { - $results[] = $contact->toStandard(); - } - return $rpcRequest->produceResponse($results); + return $rpcRequest->produceResponse(array_map(function($contact) {return $contact->toStandard();}, $contacts)); } } \ No newline at end of file diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index d4383c9..5d25cdf 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -761,10 +761,11 @@ * Resolves an external peer based on the given peer address or string identifier. * * @param PeerAddress|string $peerAddress The external peer address or string identifier to be resolved. + * @param PeerAddress|string|null $identifiedAs Optional. The peer address or string identifier by which the caller is identified * @return Peer The resolved external peer after synchronization. * @throws StandardException Thrown if there was an error with the resolution process */ - public static function resolvePeer(PeerAddress|string $peerAddress): Peer + public static function resolvePeer(PeerAddress|string $peerAddress, null|PeerAddress|string $identifiedAs=null): Peer { if(is_string($peerAddress)) { @@ -844,6 +845,11 @@ return $registeredPeer; } + private static function localResolvePeer(PeerAddress|string $peerAddress, null|PeerAddress|string $identifiedAs=null) + { + + } + /** * Retrieves the server information by assembling data from the configuration settings. * From 3311862263d1575a3d8f84c2304eaa5c0d3324ff Mon Sep 17 00:00:00 2001 From: netkas Date: Sat, 25 Jan 2025 17:26:13 -0500 Subject: [PATCH 225/420] Refactor and enhance peer resolution logic --- src/Socialbox/Socialbox.php | 271 +++++++++++++++++++++++------------- 1 file changed, 176 insertions(+), 95 deletions(-) diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index 5d25cdf..efcc20f 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -11,11 +11,14 @@ use Socialbox\Classes\ServerResolver; use Socialbox\Classes\Utilities; use Socialbox\Classes\Validator; + use Socialbox\Enums\Flags\PeerFlags; + use Socialbox\Enums\PrivacyState; use Socialbox\Enums\ReservedUsernames; use Socialbox\Enums\SessionState; use Socialbox\Enums\StandardError; use Socialbox\Enums\StandardHeaders; use Socialbox\Enums\StandardMethods; + use Socialbox\Enums\Types\ContactRelationshipType; use Socialbox\Enums\Types\RequestType; use Socialbox\Exceptions\CryptographyException; use Socialbox\Exceptions\DatabaseOperationException; @@ -23,11 +26,12 @@ use Socialbox\Exceptions\ResolutionException; use Socialbox\Exceptions\RpcException; use Socialbox\Exceptions\StandardException; + use Socialbox\Managers\ContactManager; use Socialbox\Managers\ExternalSessionManager; + use Socialbox\Managers\PeerInformationManager; use Socialbox\Managers\RegisteredPeerManager; use Socialbox\Managers\SessionManager; use Socialbox\Objects\ClientRequest; - use Socialbox\Objects\Database\PeerRecord; use Socialbox\Objects\PeerAddress; use Socialbox\Objects\Standard\Peer; use Socialbox\Objects\Standard\ServerInformation; @@ -239,21 +243,21 @@ } // If-clause for handling the host peer, host peers are always enabled unless the fist clause is true // in which case the host was blocked by this server. - elseif($clientRequest->getIdentifyAs() === ReservedUsernames::HOST->value) + elseif($clientRequest->getIdentifyAs()->getUsername() === ReservedUsernames::HOST->value) { - $serverInformation = self::getExternalServerInformation($clientRequest->getIdentifyAs()->getDomain()); - // If the host is not registered, register it if($registeredPeer === null) { $peerUuid = RegisteredPeerManager::createPeer(PeerAddress::fromAddress($clientRequest->getHeader(StandardHeaders::IDENTIFY_AS))); - RegisteredPeerManager::updateDisplayName($peerUuid, $serverInformation->getServerName()); RegisteredPeerManager::enablePeer($peerUuid); } - // Otherwise, update the display name if it has changed else { - RegisteredPeerManager::updateDisplayName($registeredPeer->getUuid(), $serverInformation->getServerName()); + // If the host is registered, but disabled, enable it + if(!$registeredPeer->isEnabled()) + { + RegisteredPeerManager::enablePeer($registeredPeer->getUuid()); + } } } // Otherwise the peer isn't registered, so we need to register it @@ -273,15 +277,22 @@ } // Generate server's encryption keys for this session - $serverEncryptionKey = Cryptography::generateEncryptionKeyPair(); + $serverEncryptionKeyPair = Cryptography::generateEncryptionKeyPair(); // Create the session passing on the registered peer, client name, version, and public keys - $sessionUuid = SessionManager::createSession($registeredPeer, $clientRequest->getClientName(), $clientRequest->getClientVersion(), $clientPublicSigningKey, $clientPublicEncryptionKey, $serverEncryptionKey); + $sessionUuid = SessionManager::createSession( + peer: $registeredPeer, + clientName: $clientRequest->getClientName(), + clientVersion: $clientRequest->getClientVersion(), + clientPublicSigningKey: $clientPublicSigningKey, + clientPublicEncryptionKey: $clientPublicEncryptionKey, + serverEncryptionKeyPair: $serverEncryptionKeyPair + ); // The server responds back with the session UUID & The server's public encryption key as the header http_response_code(201); // Created header('Content-Type: text/plain'); - header(StandardHeaders::ENCRYPTION_PUBLIC_KEY->value . ': ' . $serverEncryptionKey->getPublicKey()); + header(StandardHeaders::ENCRYPTION_PUBLIC_KEY->value . ': ' . $serverEncryptionKeyPair->getPublicKey()); print($sessionUuid); // Return the session UUID } catch(InvalidArgumentException $e) @@ -514,7 +525,7 @@ // Synchronize the peer try { - self::synchronizeExternalPeer($clientRequest->getIdentifyAs()); + self::resolvePeer($clientRequest->getIdentifyAs()); } catch (DatabaseOperationException $e) { @@ -730,33 +741,6 @@ } } - /** - * Synchronizes an external peer by resolving and integrating its information into the system. - * - * @param PeerAddress|Peer|string $externalPeer The external peer to synchronize, provided as a PeerAddress instance or a string. - * @return void - * @throws CryptographyException If there is an error in the cryptography - * @throws DatabaseOperationException If there is an error while processing the peer against the database - * @throws ResolutionException If the synchronization process fails due to unresolved peer information or other errors. - * @throws RpcException If there is an RPC exception while connecting to the remote server - */ - public static function synchronizeExternalPeer(PeerAddress|Peer|string $externalPeer): void - { - if($externalPeer instanceof Peer) - { - RegisteredPeerManager::synchronizeExternalPeer($externalPeer); - return; - } - - if($externalPeer instanceof PeerAddress) - { - $externalPeer = $externalPeer->getAddress(); - } - - $client = self::getExternalSession($externalPeer->getDomain()); - RegisteredPeerManager::synchronizeExternalPeer($client->resolvePeer($externalPeer)); - } - /** * Resolves an external peer based on the given peer address or string identifier. * @@ -766,88 +750,185 @@ * @throws StandardException Thrown if there was an error with the resolution process */ public static function resolvePeer(PeerAddress|string $peerAddress, null|PeerAddress|string $identifiedAs=null): Peer + { + if($peerAddress->getDomain() !== Configuration::getInstanceConfiguration()->getDomain()) + { + return self::resolveExternalPeer($peerAddress, $identifiedAs); + } + + return self::resolveLocalPeer($peerAddress); + } + + /** + * Resolves a peer based on the given peer address or string identifier. + * + * @param PeerAddress|string $peerAddress The peer address or string identifier to be resolved. + * @param PeerAddress|string|null $identifiedAs Optional. The peer address or string identifier by which the caller is identified + * @return Peer The resolved peer after synchronization. + * @throws StandardException Thrown if there was an error with the resolution process + */ + private static function resolveExternalPeer(PeerAddress|string $peerAddress, null|PeerAddress|string $identifiedAs=null): Peer { if(is_string($peerAddress)) { $peerAddress = PeerAddress::fromAddress($peerAddress); } + if(is_string($identifiedAs)) + { + $identifiedAs = PeerAddress::fromAddress($identifiedAs); + } + + // Resolve the peer from the local database if it exists try { - $registeredPeer = RegisteredPeerManager::getPeerByAddress($peerAddress); + $existingPeer = RegisteredPeerManager::getPeerByAddress($peerAddress); + } + catch(DatabaseOperationException $e) + { + throw new StandardException('Failed to resolve the peer: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if($existingPeer === null) + { + // if the peer doesn't exist, resolve it externally and synchronize it + + try + { + $peer = self::getExternalSession($peerAddress->getDomain())->resolvePeer($peerAddress, $identifiedAs); + } + catch(Exception $e) + { + throw new StandardException('Failed to resolve the peer: ' . $e->getMessage(), StandardError::RESOLUTION_FAILED, $e); + } + + try + { + RegisteredPeerManager::synchronizeExternalPeer($peer); + } + catch(DatabaseOperationException $e) + { + throw new StandardException('Failed to synchronize the external peer: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $peer; + } + + // If the peer exists, but it's outdated, synchronize it + if($existingPeer->getUpdated()->getTimestamp() < time() - Configuration::getPoliciesConfiguration()->getPeerSyncInterval()) + { + try + { + $peer = self::getExternalSession($peerAddress->getDomain())->resolvePeer($peerAddress, $identifiedAs); + } + catch(Exception $e) + { + throw new StandardException('Failed to resolve the peer: ' . $e->getMessage(), StandardError::RESOLUTION_FAILED, $e); + } + + try + { + RegisteredPeerManager::synchronizeExternalPeer($peer); + } + catch(DatabaseOperationException $e) + { + throw new StandardException('Failed to synchronize the external peer: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $peer; + } + + // If the peer exists and is up to date, return it + return $existingPeer->toStandardPeer(); + } + + /** + * Resolves a peer locally based on the given peer address or string identifier. + * + * @param PeerAddress|string $peerAddress The peer address or string identifier to be resolved. + * @param PeerAddress|string|null $identifiedAs Optional. The peer address or string identifier by which the caller is identified + * @return Peer The resolved peer after synchronization. + * @throws StandardException Thrown if there was an error with the resolution process + */ + private static function resolveLocalPeer(PeerAddress|string $peerAddress, null|PeerAddress|string $identifiedAs=null): Peer + { + if(is_string($peerAddress)) + { + $peerAddress = PeerAddress::fromAddress($peerAddress); + } + + if(is_string($identifiedAs)) + { + $identifiedAs = PeerAddress::fromAddress($identifiedAs); + } + + // Resolve the peer + try + { + $peer = RegisteredPeerManager::getPeerByAddress($peerAddress); + + if($peer === null) + { + throw new StandardException('The requested peer was not found', StandardError::PEER_NOT_FOUND); + } + } + catch(DatabaseOperationException $e) + { + throw new StandardException('Failed to resolve the peer: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); + } + + try + { + // Get the initial peer information fields, public always + $peerInformationFields = PeerInformationManager::getFilteredFields($peer, [PrivacyState::PUBLIC]); } catch (DatabaseOperationException $e) { - throw new StandardException('There was an unexpected error while trying to resolve the peer internally', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardException('Failed to resolve peer information: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); } - // Return not found if the peer was found but is disabled - if($registeredPeer !== null && !$registeredPeer->isEnabled()) + // If there's an identifier, we can resolve more information fields if the target peer has added the caller + // as a contact or if the caller is a trusted contact + if($identifiedAs !== null) { - throw new StandardException('The requested peer is disabled', StandardError::PEER_NOT_FOUND); - } + try + { + $peerContact = ContactManager::getContact($peer->getUuid(), $identifiedAs); + } + catch (DatabaseOperationException $e) + { + throw new StandardException('Failed to resolve peer because there was an error while trying to retrieve contact information for the peer', StandardError::INTERNAL_SERVER_ERROR, $e); + } - // If the peer was not found but the peer resides in an external server, resolve it - try - { - if($registeredPeer === null && $peerAddress->getDomain() !== Configuration::getInstanceConfiguration()->getDomain()) + // If it is a contact, what sort of contact? retrieve depending on the contact type + if($peerContact !== null) { try { - $registeredPeer = self::getExternalSession($peerAddress->getDomain())->resolvePeer($peerAddress); + if($peerContact->getRelationship() === ContactRelationshipType::MUTUAL) + { + // Retrieve the mutual information fields + array_merge($peerInformationFields, PeerInformationManager::getFilteredFields($peer, [PrivacyState::CONTACTS])); + } + elseif($peerContact->getRelationship() === ContactRelationshipType::TRUSTED) + { + // Retrieve the mutual and trusted information fields + array_merge($peerInformationFields, PeerInformationManager::getFilteredFields($peer, [PrivacyState::CONTACTS, PrivacyState::TRUSTED])); + } } catch (DatabaseOperationException $e) { - throw new StandardException('There was an unexpected error while trying to resolve the peer externally: ' . $e->getMessage(), StandardError::RESOLUTION_FAILED, $e); - } - - // Synchronize the peer for future use - self::synchronizeExternalPeer($registeredPeer); - } - // If the peer was found and the peer does reside in an external server, re-resolve it if necessary - elseif($registeredPeer !== null && $peerAddress->getDomain() !== Configuration::getInstanceConfiguration()->getDomain()) - { - if($registeredPeer->getUpdated()->getTimestamp() < time() - Configuration::getPoliciesConfiguration()->getPeerSyncInterval()) - { - try - { - $registeredPeer = self::getExternalSession($peerAddress->getDomain())->resolvePeer($peerAddress); - } - catch (DatabaseOperationException $e) - { - throw new StandardException('There was an unexpected error while trying to resolve the peer externally: ' . $e->getMessage(), StandardError::RESOLUTION_FAILED, $e); - } - - // Synchronize the peer for future use - self::synchronizeExternalPeer($registeredPeer); + throw new StandardException('Failed to resolve peer information: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); } } } - catch(StandardException $e) - { - throw $e; - } - catch(Exception $e) - { - throw new StandardException('Failed to resolve the peer: ' . $e->getMessage(), StandardError::RESOLUTION_FAILED, $e); - } - - if($registeredPeer === null) - { - throw new StandardException('The requested peer was not found', StandardError::PEER_NOT_FOUND); - } - - if($registeredPeer instanceof PeerRecord) - { - $registeredPeer = $registeredPeer->toStandardPeer(); - } - - return $registeredPeer; - } - - private static function localResolvePeer(PeerAddress|string $peerAddress, null|PeerAddress|string $identifiedAs=null) - { + return new Peer([ + 'address' => $peer->getAddress(), + 'information_fields' => $peerInformationFields, + 'flags' => PeerFlags::toString($peer->getFlags()), + 'registered' => $peer->getCreated()->getTimestamp() + ]); } /** From a9e5d6eb460b6a1114118642dcafb186c8f54526 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 27 Jan 2025 02:34:54 -0500 Subject: [PATCH 226/420] Added check for adding self as a contact --- src/Socialbox/SocialClient.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Socialbox/SocialClient.php b/src/Socialbox/SocialClient.php index 252b9ee..0be51e7 100644 --- a/src/Socialbox/SocialClient.php +++ b/src/Socialbox/SocialClient.php @@ -572,17 +572,22 @@ * @return Peer The resolved peer object. * @throws RpcException Thrown if the RPC request fails. */ - public function resolvePeer(string|PeerAddress $peerAddress): Peer + public function resolvePeer(string|PeerAddress $peerAddress, null|string|PeerAddress $identifiedAs=null): Peer { if($peerAddress instanceof PeerAddress) { $peerAddress = $peerAddress->getAddress(); } + if($identifiedAs instanceof PeerAddress) + { + $identifiedAs = $identifiedAs->getAddress(); + } + return Peer::fromArray($this->sendRequest( new RpcRequest(StandardMethods::RESOLVE_PEER, Utilities::randomCrc32(), [ 'peer' => $peerAddress - ]) + ]), $identifiedAs )->getResponse()->getResult()); } } \ No newline at end of file From 8d5a95da9b7b2a4ef27741317c3e2827b6228d9e Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 27 Jan 2025 02:35:41 -0500 Subject: [PATCH 227/420] Added self-host resolution for resolvePeer --- src/Socialbox/Socialbox.php | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index efcc20f..32747e5 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -19,6 +19,7 @@ use Socialbox\Enums\StandardHeaders; use Socialbox\Enums\StandardMethods; use Socialbox\Enums\Types\ContactRelationshipType; + use Socialbox\Enums\Types\InformationFieldName; use Socialbox\Enums\Types\RequestType; use Socialbox\Exceptions\CryptographyException; use Socialbox\Exceptions\DatabaseOperationException; @@ -33,6 +34,7 @@ use Socialbox\Managers\SessionManager; use Socialbox\Objects\ClientRequest; use Socialbox\Objects\PeerAddress; + use Socialbox\Objects\Standard\InformationField; use Socialbox\Objects\Standard\Peer; use Socialbox\Objects\Standard\ServerInformation; use Throwable; @@ -756,7 +758,23 @@ return self::resolveExternalPeer($peerAddress, $identifiedAs); } - return self::resolveLocalPeer($peerAddress); + if($peerAddress->getUsername() === ReservedUsernames::HOST->value) + { + return new Peer([ + 'address' => sprintf('%s@%s', ReservedUsernames::HOST->value, Configuration::getInstanceConfiguration()->getDomain()), + 'information_fields' => [ + new InformationField([ + 'name' => InformationFieldName::DISPLAY_NAME, + 'value' => Configuration::getInstanceConfiguration()->getName() + ]) + ], + 'flags' => [], + // TODO: Should use existed-since field + 'registered' => 0 + ]); + } + + return self::resolveLocalPeer($peerAddress, $identifiedAs); } /** From 481ea884f201d8a96ede112af20325792c45177c Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 27 Jan 2025 02:35:53 -0500 Subject: [PATCH 228/420] Added self-contact check --- .../Classes/StandardMethods/AddressBookAddContact.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Socialbox/Classes/StandardMethods/AddressBookAddContact.php b/src/Socialbox/Classes/StandardMethods/AddressBookAddContact.php index 70442ca..ebad0d1 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBookAddContact.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBookAddContact.php @@ -51,11 +51,16 @@ try { + $peer = $request->getPeer(); + if($peer->getAddress() == $address) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'Cannot add self as contact'); + } + // Resolve the peer, this would throw a StandardException if something goes wrong Socialbox::resolvePeer($address); // Check if the contact already exists - $peer = $request->getPeer(); if(ContactManager::isContact($peer, $address)) { return $rpcRequest->produceError(StandardError::FORBIDDEN, 'Contact already exists'); From c3f6f6096ef7681832d764fd2c5912a3a39ec3d5 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 27 Jan 2025 02:44:56 -0500 Subject: [PATCH 229/420] RegisteredPeerManager synchronizeExternalPeer() method now synchronizes peer information too that is public by default --- .../Managers/RegisteredPeerManager.php | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/Socialbox/Managers/RegisteredPeerManager.php b/src/Socialbox/Managers/RegisteredPeerManager.php index bf20cb9..66f6ebe 100644 --- a/src/Socialbox/Managers/RegisteredPeerManager.php +++ b/src/Socialbox/Managers/RegisteredPeerManager.php @@ -11,6 +11,7 @@ use Socialbox\Classes\Database; use Socialbox\Classes\Logger; use Socialbox\Enums\Flags\PeerFlags; + use Socialbox\Enums\PrivacyState; use Socialbox\Enums\ReservedUsernames; use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Objects\Database\PeerRecord; @@ -234,6 +235,25 @@ throw new DatabaseOperationException('Failed to update the external peer in the database', $e); } + foreach($peer->getInformationFields() as $informationField) + { + try + { + if(PeerInformationManager::fieldExists($existingPeer, $informationField->getName())) + { + PeerInformationManager::updateField($existingPeer, $informationField->getName(), $informationField->getValue()); + } + else + { + PeerInformationManager::addField($existingPeer, $informationField->getName(), $informationField->getValue(), PrivacyState::PUBLIC); + } + } + catch(DatabaseOperationException $e) + { + throw new DatabaseOperationException('Failed to update the external peer information in the database', $e); + } + } + return; } @@ -254,6 +274,18 @@ { throw new DatabaseOperationException('Failed to synchronize the external peer in the database', $e); } + + foreach($peer->getInformationFields() as $informationField) + { + try + { + PeerInformationManager::addField($uuid, $informationField->getName(), $informationField->getValue(), PrivacyState::PUBLIC); + } + catch(DatabaseOperationException $e) + { + throw new DatabaseOperationException('Failed to add the external peer information in the database', $e); + } + } } /** From ec18199c086f0f7eb6717de2ba40e82dfbdb663d Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 27 Jan 2025 02:45:04 -0500 Subject: [PATCH 230/420] ResolvePeer method now checks if the caller is a host and the request contains a IdentifyAs header --- src/Socialbox/Classes/StandardMethods/ResolvePeer.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Socialbox/Classes/StandardMethods/ResolvePeer.php b/src/Socialbox/Classes/StandardMethods/ResolvePeer.php index 0cdc6bf..f2be123 100644 --- a/src/Socialbox/Classes/StandardMethods/ResolvePeer.php +++ b/src/Socialbox/Classes/StandardMethods/ResolvePeer.php @@ -6,6 +6,7 @@ use InvalidArgumentException; use Socialbox\Abstracts\Method; use Socialbox\Classes\Configuration; + use Socialbox\Enums\ReservedUsernames; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\RpcException; @@ -41,10 +42,17 @@ throw new StandardException('Peer Address Error: ' . $e->getMessage(), StandardError::RPC_INVALID_ARGUMENTS, $e); } + // Check if host is making the request & the identifier is not empty + $identifyAs = null; + if($request->getPeer()->getUsername() == ReservedUsernames::HOST && $request->getIdentifyAs() != null) + { + $identifyAs = $request->getIdentifyAs(); + } + // Resolve the peer using the server's peer resolver, this will resolve both internal peers and external peers try { - return $rpcRequest->produceResponse(Socialbox::resolvePeer($peerAddress)); + return $rpcRequest->produceResponse(Socialbox::resolvePeer($peerAddress, $identifyAs)); } catch(Exception $e) { From 3744cf54127e284a2bbc5862a70b7e4768cc2c67 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 27 Jan 2025 02:53:59 -0500 Subject: [PATCH 231/420] Updated Database Structure --- .idea/sqldialects.xml | 3 ++ .../Resources/database/contact_known_keys.sql | 20 ++++++++++++ .../Classes/Resources/database/contacts.sql | 18 +++++++++++ .../Resources/database/signing_keys.sql | 32 +++++++++++++++++++ src/Socialbox/Enums/DatabaseObjects.php | 8 +++++ 5 files changed, 81 insertions(+) create mode 100644 src/Socialbox/Classes/Resources/database/contact_known_keys.sql create mode 100644 src/Socialbox/Classes/Resources/database/contacts.sql create mode 100644 src/Socialbox/Classes/Resources/database/signing_keys.sql diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml index f682500..9d2b463 100644 --- a/.idea/sqldialects.xml +++ b/.idea/sqldialects.xml @@ -3,10 +3,13 @@ + + + diff --git a/src/Socialbox/Classes/Resources/database/contact_known_keys.sql b/src/Socialbox/Classes/Resources/database/contact_known_keys.sql new file mode 100644 index 0000000..9304131 --- /dev/null +++ b/src/Socialbox/Classes/Resources/database/contact_known_keys.sql @@ -0,0 +1,20 @@ +create table contacts_known_keys +( + contact_uuid varchar(36) not null comment 'The UUID of the contact in reference to', + key_name varchar(64) not null comment 'The name of the key', + public_key varchar(64) not null comment 'The public signing key', + expires timestamp not null comment 'The Timestamp for when this key expires', + trusted_at timestamp default current_timestamp() not null comment 'The Timestamp for when this signing key was trusted', + primary key (contact_uuid, key_name) comment 'The unique key-name pair with the contact uuid to ensure no keys with the same names should exist', + constraint contacts_known_keys_contact_uuid_key_name_uindex + unique (contact_uuid, key_name) comment 'The unique key-name pair with the contact uuid to ensure no keys with the same names should exist', + constraint contacts_known_keys_contacts_uuid_fk + foreign key (contact_uuid) references contacts (uuid) + on update cascade on delete cascade +) + comment 'Table for housing known signing keys for peer contacts'; + +create index contacts_known_keys_key_name_index + on contacts_known_keys (key_name) + comment 'The index for the key name'; + diff --git a/src/Socialbox/Classes/Resources/database/contacts.sql b/src/Socialbox/Classes/Resources/database/contacts.sql new file mode 100644 index 0000000..b1124f1 --- /dev/null +++ b/src/Socialbox/Classes/Resources/database/contacts.sql @@ -0,0 +1,18 @@ +create table contacts +( + uuid varchar(36) default uuid() not null comment 'The contact UUID for the record' + primary key comment 'The Primary Unique Universal Identifier for the contact record', + peer_uuid varchar(36) not null comment 'The Peer UUID', + contact_peer_address varchar(256) not null comment 'The contact peer address', + relationship enum ('MUTUAL', 'TRUSTED', 'BLOCKED') default 'MUTUAL' not null comment 'The relationship between the two peers, MUTUAL=The contact peer is recognized', + created timestamp default current_timestamp() not null comment 'The Timestamp for when this contact was created', + constraint contacts_uuid_uindex + unique (uuid) comment 'The Primary Unique Universal Identifier for the contact record', + constraint peer_contacts_peer_uuid_contact_peer_address_uindex + unique (peer_uuid, contact_peer_address) comment 'The Unique Peer UUID & Contact Peer Address combination pair', + constraint peer_contacts_registered_peers_uuid_fk + foreign key (peer_uuid) references registered_peers (uuid) + on update cascade on delete cascade +) + comment 'Table for housing personal contacts for peers'; + diff --git a/src/Socialbox/Classes/Resources/database/signing_keys.sql b/src/Socialbox/Classes/Resources/database/signing_keys.sql new file mode 100644 index 0000000..8de9fbd --- /dev/null +++ b/src/Socialbox/Classes/Resources/database/signing_keys.sql @@ -0,0 +1,32 @@ +create table signing_keys +( + peer_uuid varchar(36) not null comment 'The UUID of the peer', + uuid varchar(36) default uuid() not null comment 'The UUID of the key record', + name varchar(64) null comment 'Optional. User provided name for the key', + public_key varchar(64) not null comment 'The Public Signature Key', + state enum ('ACTIVE', 'EXPIRED') default 'ACTIVE' not null comment 'The state of the public key', + expires timestamp null comment 'The Timestamp for when this key expires, null = Never expires', + created timestamp default current_timestamp() not null comment 'The Timestamp for when the signing key record was created', + primary key (peer_uuid, uuid) comment 'The Unique Index pair for the signing key name and the UUID of the peer', + constraint signing_keys_peer_uuid_uuid_uindex + unique (peer_uuid, uuid) comment 'The Unique Index pair for the signing key name and the UUID of the peer', + constraint signing_keys_pk + unique (peer_uuid, uuid) comment 'The Unique Index pair for the signing key name and the UUID of the peer', + constraint signing_keys_registered_peers_uuid_fk + foreign key (peer_uuid) references registered_peers (uuid) + on update cascade on delete cascade +) + comment 'Table for housing public signing keys for peers on the network'; + +create index signing_keys_peer_uuid_index + on signing_keys (peer_uuid) + comment 'The primary index for the peer UUID column'; + +create index signing_keys_state_index + on signing_keys (state) + comment 'Signing key state index'; + +create index signing_keys_uuid_index + on signing_keys (uuid) + comment 'The index for the signing key name'; + diff --git a/src/Socialbox/Enums/DatabaseObjects.php b/src/Socialbox/Enums/DatabaseObjects.php index 3f8acad..327698b 100644 --- a/src/Socialbox/Enums/DatabaseObjects.php +++ b/src/Socialbox/Enums/DatabaseObjects.php @@ -14,8 +14,12 @@ case AUTHENTICATION_OTP = 'authentication_otp.sql'; case CAPTCHA_IMAGES = 'captcha_images.sql'; case SESSIONS = 'sessions.sql'; + case CONTACTS = 'contacts.sql'; + case SIGNING_KEYS = 'signing_keys.sql'; case EXTERNAL_SESSIONS = 'external_sessions.sql'; + case CONTACT_KNOWN_KEYS = 'contact_known_keys.sql'; + /** * Returns the priority of the database object * @@ -34,8 +38,12 @@ self::AUTHENTICATION_PASSWORDS, self::AUTHENTICATION_OTP, self::CAPTCHA_IMAGES, + self::CONTACTS, self::SESSIONS, + self::SIGNING_KEYS, self::EXTERNAL_SESSIONS => 2, + + self::CONTACT_KNOWN_KEYS => 3, }; } From a04621b97db11cd89c89842b9afb441afaa85b79 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 27 Jan 2025 03:25:25 -0500 Subject: [PATCH 232/420] Reference correction & update configurations --- .idea/sqldialects.xml | 1 + coffee_socialbox/config/socialbox.conf | 17 +++++++++++-- .../Resources/database/authentication_otp.sql | 24 +++++++++---------- .../database/authentication_passwords.sql | 4 ++-- .../Resources/database/captcha_images.sql | 4 ++-- .../Resources/database/signing_keys.sql | 4 ++-- teapot_socialbox/config/socialbox.conf | 17 +++++++++++-- 7 files changed, 49 insertions(+), 22 deletions(-) diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml index 9d2b463..b4dcf43 100644 --- a/.idea/sqldialects.xml +++ b/.idea/sqldialects.xml @@ -2,6 +2,7 @@ + diff --git a/coffee_socialbox/config/socialbox.conf b/coffee_socialbox/config/socialbox.conf index e2baf17..0d01283 100755 --- a/coffee_socialbox/config/socialbox.conf +++ b/coffee_socialbox/config/socialbox.conf @@ -84,7 +84,11 @@ "email_address_required": false, "phone_number_required": false, "birthday_required": false, - "image_captcha_verification_required": false + "image_captcha_verification_required": false, + "first_name_required": false, + "middle_name_required": false, + "last_name_required": false, + "url_required": false }, "authentication": { "enabled": true, @@ -94,7 +98,16 @@ "max_signing_keys": 20, "session_inactivity_expires": 43200, "image_captcha_expires": 300, - "peer_sync_interval": 3600 + "peer_sync_interval": 3600, + "get_contacts_limit": 100, + "default_display_picture_privacy": "PUBLIC", + "default_first_name_privacy": "CONTACTS", + "default_middle_name_privacy": "PRIVATE", + "default_last_name_privacy": "PRIVATE", + "default_email_address_privacy": "CONTACTS", + "default_phone_number_privacy": "CONTACTS", + "default_birthday_privacy": "PRIVATE", + "default_url_privacy": "PUBLIC" }, "storage": { "path": "/etc/socialbox", diff --git a/src/Socialbox/Classes/Resources/database/authentication_otp.sql b/src/Socialbox/Classes/Resources/database/authentication_otp.sql index 4e32763..54a3d28 100644 --- a/src/Socialbox/Classes/Resources/database/authentication_otp.sql +++ b/src/Socialbox/Classes/Resources/database/authentication_otp.sql @@ -1,18 +1,18 @@ -create table authentication_otp +create table authentication_passwords ( - peer_uuid varchar(36) not null comment 'The Peer UUID associated with this record' - primary key comment 'The Peer UUID unique Index', - secret mediumtext not null comment 'The encrypted secret for the OTP', - updated timestamp default current_timestamp() not null comment 'The Timestamp for when the record was last updated', - constraint authentication_otp_peer_uuid_uindex - unique (peer_uuid) comment 'The Peer UUID unique Index', - constraint authentication_otp_registered_peers_uuid_fk + peer_uuid varchar(36) not null comment 'The primary unique index of the peer uuid' + primary key, + hash mediumtext not null comment 'The encrypted hash of the password', + updated timestamp default current_timestamp() not null comment 'The Timestamp for when this record was last updated', + constraint authentication_passwords_peer_uuid_uindex + unique (peer_uuid) comment 'The primary unique index of the peer uuid', + constraint authentication_passwords_registered_peers_uuid_fk foreign key (peer_uuid) references peers (uuid) on update cascade on delete cascade ) - comment 'Table for housing encrypted OTP secrets for for verification'; + comment 'Table for housing authentication passwords for registered peers'; -create index authentication_otp_updated_index - on authentication_otp (updated) - comment 'The index for the updated column'; +create index authentication_passwords_updated_index + on authentication_passwords (updated) + comment 'The index of the of the updated column of the record'; diff --git a/src/Socialbox/Classes/Resources/database/authentication_passwords.sql b/src/Socialbox/Classes/Resources/database/authentication_passwords.sql index 5a7b36c..4a8da96 100644 --- a/src/Socialbox/Classes/Resources/database/authentication_passwords.sql +++ b/src/Socialbox/Classes/Resources/database/authentication_passwords.sql @@ -1,7 +1,7 @@ create table authentication_passwords ( - peer_uuid varchar(36) not null comment 'The Universal Unique Identifier for the peer that is associated with this password record' - primary key comment 'The primary unique index of the peer uuid', + peer_uuid varchar(36) not null comment 'The primary unique index of the peer uuid' + primary key, hash mediumtext not null comment 'The encrypted hash of the password', updated timestamp default current_timestamp() not null comment 'The Timestamp for when this record was last updated', constraint authentication_passwords_peer_uuid_uindex diff --git a/src/Socialbox/Classes/Resources/database/captcha_images.sql b/src/Socialbox/Classes/Resources/database/captcha_images.sql index 1a3958b..e4c00a5 100644 --- a/src/Socialbox/Classes/Resources/database/captcha_images.sql +++ b/src/Socialbox/Classes/Resources/database/captcha_images.sql @@ -1,7 +1,7 @@ create table captcha_images ( - uuid varchar(36) default uuid() not null comment 'The Unique Universal Identifier of the captcha record' - primary key comment 'The Unique index for the UUID column', + uuid varchar(36) default uuid() not null comment 'The Unique index for the UUID column' + primary key, peer_uuid varchar(36) not null comment 'The UUID of the peer that is associated with this captcha challenge', status enum ('UNSOLVED', 'SOLVED') default 'UNSOLVED' not null comment 'The status of the current captcha', answer varchar(8) null comment 'The current answer for the captcha', diff --git a/src/Socialbox/Classes/Resources/database/signing_keys.sql b/src/Socialbox/Classes/Resources/database/signing_keys.sql index 8de9fbd..3853718 100644 --- a/src/Socialbox/Classes/Resources/database/signing_keys.sql +++ b/src/Socialbox/Classes/Resources/database/signing_keys.sql @@ -13,7 +13,7 @@ create table signing_keys constraint signing_keys_pk unique (peer_uuid, uuid) comment 'The Unique Index pair for the signing key name and the UUID of the peer', constraint signing_keys_registered_peers_uuid_fk - foreign key (peer_uuid) references registered_peers (uuid) + foreign key (peer_uuid) references peers (uuid) on update cascade on delete cascade ) comment 'Table for housing public signing keys for peers on the network'; @@ -28,5 +28,5 @@ create index signing_keys_state_index create index signing_keys_uuid_index on signing_keys (uuid) - comment 'The index for the signing key name'; + comment 'The index for the signing key namee'; diff --git a/teapot_socialbox/config/socialbox.conf b/teapot_socialbox/config/socialbox.conf index 44c4cdb..bf3b757 100755 --- a/teapot_socialbox/config/socialbox.conf +++ b/teapot_socialbox/config/socialbox.conf @@ -84,7 +84,11 @@ "email_address_required": false, "phone_number_required": false, "birthday_required": false, - "image_captcha_verification_required": false + "image_captcha_verification_required": false, + "first_name_required": false, + "middle_name_required": false, + "last_name_required": false, + "url_required": false }, "authentication": { "enabled": true, @@ -94,7 +98,16 @@ "max_signing_keys": 20, "session_inactivity_expires": 43200, "image_captcha_expires": 300, - "peer_sync_interval": 3600 + "peer_sync_interval": 3600, + "get_contacts_limit": 100, + "default_display_picture_privacy": "PUBLIC", + "default_first_name_privacy": "CONTACTS", + "default_middle_name_privacy": "PRIVATE", + "default_last_name_privacy": "PRIVATE", + "default_email_address_privacy": "CONTACTS", + "default_phone_number_privacy": "CONTACTS", + "default_birthday_privacy": "PRIVATE", + "default_url_privacy": "PUBLIC" }, "storage": { "path": "/etc/socialbox", From 23fe57d514cbf23dd2098bfa1a908fdad67b7109 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 27 Jan 2025 03:27:58 -0500 Subject: [PATCH 233/420] Oops --- .../Resources/database/authentication_otp.sql | 24 +++++++++---------- .../database/authentication_passwords.sql | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Socialbox/Classes/Resources/database/authentication_otp.sql b/src/Socialbox/Classes/Resources/database/authentication_otp.sql index 54a3d28..4e32763 100644 --- a/src/Socialbox/Classes/Resources/database/authentication_otp.sql +++ b/src/Socialbox/Classes/Resources/database/authentication_otp.sql @@ -1,18 +1,18 @@ -create table authentication_passwords +create table authentication_otp ( - peer_uuid varchar(36) not null comment 'The primary unique index of the peer uuid' - primary key, - hash mediumtext not null comment 'The encrypted hash of the password', - updated timestamp default current_timestamp() not null comment 'The Timestamp for when this record was last updated', - constraint authentication_passwords_peer_uuid_uindex - unique (peer_uuid) comment 'The primary unique index of the peer uuid', - constraint authentication_passwords_registered_peers_uuid_fk + peer_uuid varchar(36) not null comment 'The Peer UUID associated with this record' + primary key comment 'The Peer UUID unique Index', + secret mediumtext not null comment 'The encrypted secret for the OTP', + updated timestamp default current_timestamp() not null comment 'The Timestamp for when the record was last updated', + constraint authentication_otp_peer_uuid_uindex + unique (peer_uuid) comment 'The Peer UUID unique Index', + constraint authentication_otp_registered_peers_uuid_fk foreign key (peer_uuid) references peers (uuid) on update cascade on delete cascade ) - comment 'Table for housing authentication passwords for registered peers'; + comment 'Table for housing encrypted OTP secrets for for verification'; -create index authentication_passwords_updated_index - on authentication_passwords (updated) - comment 'The index of the of the updated column of the record'; +create index authentication_otp_updated_index + on authentication_otp (updated) + comment 'The index for the updated column'; diff --git a/src/Socialbox/Classes/Resources/database/authentication_passwords.sql b/src/Socialbox/Classes/Resources/database/authentication_passwords.sql index 4a8da96..54a3d28 100644 --- a/src/Socialbox/Classes/Resources/database/authentication_passwords.sql +++ b/src/Socialbox/Classes/Resources/database/authentication_passwords.sql @@ -7,7 +7,7 @@ create table authentication_passwords constraint authentication_passwords_peer_uuid_uindex unique (peer_uuid) comment 'The primary unique index of the peer uuid', constraint authentication_passwords_registered_peers_uuid_fk - foreign key (peer_uuid) references registered_peers (uuid) + foreign key (peer_uuid) references peers (uuid) on update cascade on delete cascade ) comment 'Table for housing authentication passwords for registered peers'; From efc74cfa49dde83fecde5822f7de8f8597ef7809 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 27 Jan 2025 03:29:39 -0500 Subject: [PATCH 234/420] Updated fgkey --- src/Socialbox/Classes/Resources/database/contacts.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Socialbox/Classes/Resources/database/contacts.sql b/src/Socialbox/Classes/Resources/database/contacts.sql index b1124f1..5c766bb 100644 --- a/src/Socialbox/Classes/Resources/database/contacts.sql +++ b/src/Socialbox/Classes/Resources/database/contacts.sql @@ -11,7 +11,7 @@ create table contacts constraint peer_contacts_peer_uuid_contact_peer_address_uindex unique (peer_uuid, contact_peer_address) comment 'The Unique Peer UUID & Contact Peer Address combination pair', constraint peer_contacts_registered_peers_uuid_fk - foreign key (peer_uuid) references registered_peers (uuid) + foreign key (peer_uuid) references peers (uuid) on update cascade on delete cascade ) comment 'Table for housing personal contacts for peers'; From 93eff1320f2b93c8e972050df11af6f494dc4082 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 27 Jan 2025 03:38:11 -0500 Subject: [PATCH 235/420] Corrected session initiation logic --- src/Socialbox/Socialbox.php | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index 32747e5..d4d99a2 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -243,9 +243,10 @@ self::returnError(403, StandardError::FORBIDDEN, 'Unauthorized: The requested peer is disabled/banned'); return; } + // If-clause for handling the host peer, host peers are always enabled unless the fist clause is true // in which case the host was blocked by this server. - elseif($clientRequest->getIdentifyAs()->getUsername() === ReservedUsernames::HOST->value) + if($clientRequest->getIdentifyAs()->getUsername() === ReservedUsernames::HOST->value) { // If the host is not registered, register it if($registeredPeer === null) @@ -262,8 +263,8 @@ } } } - // Otherwise the peer isn't registered, so we need to register it - else + + if($registeredPeer === null) { // Check if registration is enabled if(!Configuration::getRegistrationConfiguration()->isRegistrationEnabled()) @@ -290,22 +291,24 @@ clientPublicEncryptionKey: $clientPublicEncryptionKey, serverEncryptionKeyPair: $serverEncryptionKeyPair ); - - // The server responds back with the session UUID & The server's public encryption key as the header - http_response_code(201); // Created - header('Content-Type: text/plain'); - header(StandardHeaders::ENCRYPTION_PUBLIC_KEY->value . ': ' . $serverEncryptionKeyPair->getPublicKey()); - print($sessionUuid); // Return the session UUID } catch(InvalidArgumentException $e) { // This is usually thrown due to an invalid input self::returnError(400, StandardError::BAD_REQUEST, $e->getMessage(), $e); + return; } catch(Exception $e) { self::returnError(500, StandardError::INTERNAL_SERVER_ERROR, 'An internal error occurred while initiating the session', $e); + return; } + + // The server responds back with the session UUID & The server's public encryption key as the header + http_response_code(201); // Created + header('Content-Type: text/plain'); + header(StandardHeaders::ENCRYPTION_PUBLIC_KEY->value . ': ' . $serverEncryptionKeyPair->getPublicKey()); + print($sessionUuid); // Return the session UUID } /** From 3e5199d77667d6a275982ff8c0819d626f4f23aa Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 27 Jan 2025 03:51:28 -0500 Subject: [PATCH 236/420] Corrected property access in InformationField --- src/Socialbox/Objects/Standard/InformationField.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Socialbox/Objects/Standard/InformationField.php b/src/Socialbox/Objects/Standard/InformationField.php index 3cdc949..0fc1a95 100644 --- a/src/Socialbox/Objects/Standard/InformationField.php +++ b/src/Socialbox/Objects/Standard/InformationField.php @@ -46,7 +46,7 @@ public function toArray(): array { return [ - 'name' => $this->name->getValue(), + 'name' => $this->name->value, 'value' => $this->value, ]; } From 1b89a7f9219dfbb22bf6260bd475ed9dbdfd8054 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 27 Jan 2025 03:51:44 -0500 Subject: [PATCH 237/420] Improved Flag Parsing in Peer object constructor --- src/Socialbox/Objects/Standard/Peer.php | 31 ++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/Socialbox/Objects/Standard/Peer.php b/src/Socialbox/Objects/Standard/Peer.php index b9dcea2..1d2ae72 100644 --- a/src/Socialbox/Objects/Standard/Peer.php +++ b/src/Socialbox/Objects/Standard/Peer.php @@ -3,6 +3,7 @@ namespace Socialbox\Objects\Standard; use InvalidArgumentException; + use Socialbox\Enums\Flags\PeerFlags; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\Database\PeerInformationFieldRecord; use Socialbox\Objects\PeerAddress; @@ -66,7 +67,35 @@ } $this->informationFields = $informationFields; - $this->flags = $data['flags']; + + if(is_array($data['flags'])) + { + $this->flags = []; + foreach($data['flags'] as $flag) + { + if($flag instanceof PeerFlags) + { + $this->flags[] = $flag->value; + } + elseif(is_string($flag)) + { + $this->flags[] = $flag; + } + else + { + throw new InvalidArgumentException('Invalid flag type, got type ' . gettype($flag)); + } + } + } + elseif(is_string($data['flags'])) + { + $this->flags = PeerFlags::fromString($data['flags']); + } + else + { + throw new InvalidArgumentException('Invalid flags value type, got type ' . gettype($data['flags'])); + } + $this->registered = $data['registered']; } From f40b9ed7f47128fd31992e05efc4d37c9c6e1664 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 27 Jan 2025 03:52:17 -0500 Subject: [PATCH 238/420] Resolve peer at host identification to prevent accidental peer creation when it isn't necessary --- src/Socialbox/Socialbox.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index d4d99a2..f6bfb88 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -253,6 +253,7 @@ { $peerUuid = RegisteredPeerManager::createPeer(PeerAddress::fromAddress($clientRequest->getHeader(StandardHeaders::IDENTIFY_AS))); RegisteredPeerManager::enablePeer($peerUuid); + $registeredPeer = RegisteredPeerManager::getPeer($peerUuid); } else { From 321bf1eadb5301f2c72d3531e579d98264b3eda8 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 27 Jan 2025 03:52:28 -0500 Subject: [PATCH 239/420] Argument correction --- src/Socialbox/SocialClient.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Socialbox/SocialClient.php b/src/Socialbox/SocialClient.php index 0be51e7..cf6cb70 100644 --- a/src/Socialbox/SocialClient.php +++ b/src/Socialbox/SocialClient.php @@ -587,7 +587,7 @@ return Peer::fromArray($this->sendRequest( new RpcRequest(StandardMethods::RESOLVE_PEER, Utilities::randomCrc32(), [ 'peer' => $peerAddress - ]), $identifiedAs + ]), true, $identifiedAs )->getResponse()->getResult()); } } \ No newline at end of file From e9f70e71158b124cf4f4b6e9f29bc9394a9581a0 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 27 Jan 2025 12:38:36 -0500 Subject: [PATCH 240/420] Added method SettingsDeleteSignature --- .../SettingsDeleteSignature.php | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/Socialbox/Classes/StandardMethods/SettingsDeleteSignature.php diff --git a/src/Socialbox/Classes/StandardMethods/SettingsDeleteSignature.php b/src/Socialbox/Classes/StandardMethods/SettingsDeleteSignature.php new file mode 100644 index 0000000..4c250cd --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/SettingsDeleteSignature.php @@ -0,0 +1,49 @@ +containsParameter('uuid')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'uuid' parameter"); + } + + try + { + $uuid = Uuid::fromString($rpcRequest->getParameter('uuid')); + } + catch(InvalidArgumentException $e) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Invalid UUID'); + } + + try + { + SigningKeysManager::deleteSigningKey($request->getPeer()->getUuid(), $uuid); + } + catch(Exception $e) + { + throw new StandardException('Failed to delete the signing key', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file From a05b02da718292d150cf8e2c1ab90b8f6241f980 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 27 Jan 2025 12:45:25 -0500 Subject: [PATCH 241/420] Added missing previous --- .../Classes/StandardMethods/SettingsDeleteSignature.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Socialbox/Classes/StandardMethods/SettingsDeleteSignature.php b/src/Socialbox/Classes/StandardMethods/SettingsDeleteSignature.php index 4c250cd..0e86af9 100644 --- a/src/Socialbox/Classes/StandardMethods/SettingsDeleteSignature.php +++ b/src/Socialbox/Classes/StandardMethods/SettingsDeleteSignature.php @@ -32,7 +32,7 @@ } catch(InvalidArgumentException $e) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Invalid UUID'); + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Invalid UUID', $e); } try From e38ebf277a37fe33f406948d92c68e6ad8f60890 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 27 Jan 2025 15:28:41 -0500 Subject: [PATCH 242/420] Rewrote contact_known_keys structure --- .../Resources/database/contact_known_keys.sql | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/Socialbox/Classes/Resources/database/contact_known_keys.sql b/src/Socialbox/Classes/Resources/database/contact_known_keys.sql index 9304131..164dc48 100644 --- a/src/Socialbox/Classes/Resources/database/contact_known_keys.sql +++ b/src/Socialbox/Classes/Resources/database/contact_known_keys.sql @@ -1,20 +1,22 @@ create table contacts_known_keys ( - contact_uuid varchar(36) not null comment 'The UUID of the contact in reference to', - key_name varchar(64) not null comment 'The name of the key', - public_key varchar(64) not null comment 'The public signing key', - expires timestamp not null comment 'The Timestamp for when this key expires', - trusted_at timestamp default current_timestamp() not null comment 'The Timestamp for when this signing key was trusted', - primary key (contact_uuid, key_name) comment 'The unique key-name pair with the contact uuid to ensure no keys with the same names should exist', - constraint contacts_known_keys_contact_uuid_key_name_uindex - unique (contact_uuid, key_name) comment 'The unique key-name pair with the contact uuid to ensure no keys with the same names should exist', + contact_uuid varchar(36) not null comment 'The Unique Universal Identifier of the personal contact that this record is associated with', + signature_uuid varchar(36) not null comment 'The Unique Universal Identifier for the signature key', + signature_name varchar(64) not null comment 'The name of the signing key', + signature_key varchar(32) not null comment 'The public signing key', + expires timestamp null comment 'The Timestamp for when this key expires, null means never', + created timestamp not null comment 'The Timestamp for when this key was created', + trusted_on timestamp default current_timestamp() not null comment 'The Timestamp for when the peer trusted this key', + primary key (contact_uuid, signature_uuid), + constraint contacts_known_keys_signature_uuid_contact_uuid_uindex + unique (signature_uuid, contact_uuid) comment 'The Unique Signature Index Pair for the contact UUID and key UUID', constraint contacts_known_keys_contacts_uuid_fk foreign key (contact_uuid) references contacts (uuid) on update cascade on delete cascade ) - comment 'Table for housing known signing keys for peer contacts'; + comment 'Table for housing known keys associated with personal contact records'; -create index contacts_known_keys_key_name_index - on contacts_known_keys (key_name) - comment 'The index for the key name'; +create index contacts_known_keys_contact_uuid_index + on contacts_known_keys (contact_uuid) + comment 'The Index of the contact UUID'; From 674ca58f0888d185c321f24d6294cf59370e9511 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 29 Jan 2025 15:23:16 -0500 Subject: [PATCH 243/420] Added method AddressBookTrustSignature --- .../AddressBookTrustSignature.php | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/Socialbox/Classes/StandardMethods/AddressBookTrustSignature.php diff --git a/src/Socialbox/Classes/StandardMethods/AddressBookTrustSignature.php b/src/Socialbox/Classes/StandardMethods/AddressBookTrustSignature.php new file mode 100644 index 0000000..93f4b4e --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/AddressBookTrustSignature.php @@ -0,0 +1,73 @@ +containsParameter('peer')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing required peer parameter'); + } + + try + { + $address = PeerAddress::fromAddress($rpcRequest->getParameter('peer')); + } + catch(InvalidArgumentException $e) + { + throw new StandardException('Invalid peer address', StandardError::RPC_INVALID_ARGUMENTS, $e); + } + + if(!$rpcRequest->containsParameter('uuid')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'uuid' parameter"); + } + + try + { + $uuid = Uuid::fromString($rpcRequest->getParameter('uuid')); + } + catch(InvalidArgumentException $e) + { + throw new StandardException('Invalid UUID', StandardError::RPC_INVALID_ARGUMENTS, $e); + } + + try + { + // Check if the contact already exists + $peer = $request->getPeer(); + if(ContactManager::isContact($peer, $address)) + { + + } + + // Create the contact + ContactManager::updateContactRelationship($peer, $address, $relationship); + } + catch (DatabaseOperationException $e) + { + throw new StandardException('Failed to update contact relationship', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + // Return success + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file From 6c1c77ad20f52221b26556868fe2b4df8cc3830e Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 29 Jan 2025 15:23:25 -0500 Subject: [PATCH 244/420] Added method AddressBookUpdateRelationship --- .../AddressBookUpdateRelationship.php | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/Socialbox/Classes/StandardMethods/AddressBookUpdateRelationship.php diff --git a/src/Socialbox/Classes/StandardMethods/AddressBookUpdateRelationship.php b/src/Socialbox/Classes/StandardMethods/AddressBookUpdateRelationship.php new file mode 100644 index 0000000..cd913ec --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/AddressBookUpdateRelationship.php @@ -0,0 +1,68 @@ +containsParameter('peer')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing required peer parameter'); + } + + try + { + $address = PeerAddress::fromAddress($rpcRequest->getParameter('peer')); + } + catch(InvalidArgumentException $e) + { + throw new StandardException('Invalid peer address', StandardError::RPC_INVALID_ARGUMENTS, $e); + } + + if(!$rpcRequest->containsParameter('relationship')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing required relationship parameter'); + } + $relationship = ContactRelationshipType::tryFrom(strtoupper($rpcRequest->getParameter('relationship'))); + if($relationship === null) + { + throw new StandardException('Invalid relationship type', StandardError::RPC_INVALID_ARGUMENTS); + } + + try + { + // Check if the contact already exists + $peer = $request->getPeer(); + if(!ContactManager::isContact($peer, $address)) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'Contact does not exist'); + } + + // Create the contact + ContactManager::updateContactRelationship($peer, $address, $relationship); + } + catch (DatabaseOperationException $e) + { + throw new StandardException('Failed to update contact relationship', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + // Return success + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file From bdd35dc8ac49ccb96f64e00fccc33215e2ad03a8 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 29 Jan 2025 15:23:42 -0500 Subject: [PATCH 245/420] Added ContactKnownKeyRecord.php --- .../Database/ContactKnownKeyRecord.php | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/Socialbox/Objects/Database/ContactKnownKeyRecord.php diff --git a/src/Socialbox/Objects/Database/ContactKnownKeyRecord.php b/src/Socialbox/Objects/Database/ContactKnownKeyRecord.php new file mode 100644 index 0000000..c553c02 --- /dev/null +++ b/src/Socialbox/Objects/Database/ContactKnownKeyRecord.php @@ -0,0 +1,25 @@ + Date: Wed, 29 Jan 2025 15:24:06 -0500 Subject: [PATCH 246/420] Moved ExportedSession from Objects to Objects\Client & Added signing keypairs & a default keypair --- .../Objects/{ => Client}/ExportedSession.php | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) rename src/Socialbox/Objects/{ => Client}/ExportedSession.php (88%) diff --git a/src/Socialbox/Objects/ExportedSession.php b/src/Socialbox/Objects/Client/ExportedSession.php similarity index 88% rename from src/Socialbox/Objects/ExportedSession.php rename to src/Socialbox/Objects/Client/ExportedSession.php index 4c7752c..db69534 100644 --- a/src/Socialbox/Objects/ExportedSession.php +++ b/src/Socialbox/Objects/Client/ExportedSession.php @@ -1,6 +1,6 @@ privateSharedSecret = $data['private_shared_secret']; $this->clientTransportEncryptionKey = $data['client_transport_encryption_key']; $this->serverTransportEncryptionKey = $data['server_transport_encryption_key']; + $this->defaultSigningKey = $data['default_signing_key'] ?? null; + $this->signingKeys = array_map(fn($key) => SignatureKeyPair::fromArray($key), $data['signing_keys']); } /** @@ -207,6 +214,26 @@ return $this->serverTransportEncryptionKey; } + /** + * Retrieves the default signing key associated with the current instance. + * + * @return string|null The default signing key. + */ + public function getDefaultSigningKey(): ?string + { + return $this->defaultSigningKey; + } + + /** + * Retrieves the signing keys associated with the current instance. + * + * @return SignatureKeyPair[] The signing keys. + */ + public function getSigningKeys(): array + { + return $this->signingKeys; + } + /** * @inheritDoc */ @@ -228,6 +255,8 @@ 'private_shared_secret' => $this->privateSharedSecret, 'client_transport_encryption_key' => $this->clientTransportEncryptionKey, 'server_transport_encryption_key' => $this->serverTransportEncryptionKey, + 'default_signing_key' => $this->defaultSigningKey, + 'signing_keys' => array_map(fn($key) => $key->toArray(), $this->signingKeys) ]; } From 99c6c1385ecf5d7701f5c400c5fcf974bb204cac Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 29 Jan 2025 15:24:22 -0500 Subject: [PATCH 247/420] Cleanup --- src/Socialbox/Managers/ExternalSessionManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Socialbox/Managers/ExternalSessionManager.php b/src/Socialbox/Managers/ExternalSessionManager.php index 648a5f1..bd02658 100644 --- a/src/Socialbox/Managers/ExternalSessionManager.php +++ b/src/Socialbox/Managers/ExternalSessionManager.php @@ -7,7 +7,7 @@ use Socialbox\Classes\Database; use Socialbox\Enums\ReservedUsernames; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Objects\ExportedSession; + use Socialbox\Objects\Client\ExportedSession; class ExternalSessionManager { From c5093e4d0ca504c0c6dab34fa6454e8d37d0abc2 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 29 Jan 2025 15:24:32 -0500 Subject: [PATCH 248/420] Added ExternalUrlVerification as a standard object --- .../Standard/ExternalUrlVerification.php | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/Socialbox/Objects/Standard/ExternalUrlVerification.php diff --git a/src/Socialbox/Objects/Standard/ExternalUrlVerification.php b/src/Socialbox/Objects/Standard/ExternalUrlVerification.php new file mode 100644 index 0000000..2dcb333 --- /dev/null +++ b/src/Socialbox/Objects/Standard/ExternalUrlVerification.php @@ -0,0 +1,60 @@ +expires = $data['expires']; + $this->url = $data['url']; + } + + /** + * Returns the expiration time of the captcha + * + * @return int The expiration time of the captcha in Unix timestamp format + */ + public function getExpires(): int + { + return $this->expires; + } + + /** + * Returns the URL of the external verification + * + * @return string The URL of the external verification + */ + public function getUrl(): string + { + return $this->url; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): ExternalUrlVerification + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'expires' => $this->expires, + 'url' => $this->url + ]; + } + } \ No newline at end of file From 4a9c42a52d2dbe166293b54f9a74999aebbdb625 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 29 Jan 2025 15:24:39 -0500 Subject: [PATCH 249/420] Removed ImageCaptcha (Old) --- .../Objects/Standard/ImageCaptcha.php | 52 ------------------- 1 file changed, 52 deletions(-) delete mode 100644 src/Socialbox/Objects/Standard/ImageCaptcha.php diff --git a/src/Socialbox/Objects/Standard/ImageCaptcha.php b/src/Socialbox/Objects/Standard/ImageCaptcha.php deleted file mode 100644 index cbcc158..0000000 --- a/src/Socialbox/Objects/Standard/ImageCaptcha.php +++ /dev/null @@ -1,52 +0,0 @@ -expires = $data['expires']; - $this->image = $data['image']; - } - - /** - * @return int - */ - public function getExpires(): int - { - return $this->expires; - } - - /** - * @return string - */ - public function getImage(): string - { - return $this->image; - } - - /** - * @inheritDoc - */ - public static function fromArray(array $data): object - { - return new self($data); - } - - /** - * @inheritDoc - */ - public function toArray(): array - { - return [ - 'expires' => $this->expires, - 'image' => $this->image - ]; - } -} \ No newline at end of file From dd35bc69b367d6c1e996830472f0c37bb4c6a0bc Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 29 Jan 2025 15:24:48 -0500 Subject: [PATCH 250/420] Added ImageCaptchaVerification as a standard object --- .../Standard/ImageCaptchaVerification.php | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/Socialbox/Objects/Standard/ImageCaptchaVerification.php diff --git a/src/Socialbox/Objects/Standard/ImageCaptchaVerification.php b/src/Socialbox/Objects/Standard/ImageCaptchaVerification.php new file mode 100644 index 0000000..3a775c2 --- /dev/null +++ b/src/Socialbox/Objects/Standard/ImageCaptchaVerification.php @@ -0,0 +1,87 @@ +expires = $data['expires']; + $this->imageBase64 = $data['image_base64']; + } + + /** + * Returns the expiration time of the captcha + * + * @return int The expiration time of the captcha in Unix timestamp format + */ + public function getExpires(): int + { + return $this->expires; + } + + /** + * Returns the image data of the captcha + * + * @return string The image data of the captcha + */ + public function getImageBase64(): string + { + return $this->imageBase64; + } + + /** + * Saves the image to the specified path + * + * @param string $path The path to save the image to + * @throws Exception If the image could not be saved + */ + public function saveImage(string $path): void + { + // Check if the base64 is prefixed with html data + if (str_starts_with($this->imageBase64, 'data:image/jpeg;base64,')) + { + $this->imageBase64 = substr($this->imageBase64, strlen('data:image/jpeg;base64,')); + } + + $decoded = @base64_decode($this->imageBase64); + if($decoded === false) + { + throw new Exception('Failed to decode base64 image'); + } + + if(file_put_contents($path, $decoded) === false) + { + throw new Exception('Failed to save image'); + } + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): ImageCaptchaVerification + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'expires' => $this->expires, + 'image_base64' => $this->imageBase64 + ]; + } + } \ No newline at end of file From a02a765f11f8b63ab7379f732741fb87dd66c1a2 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 29 Jan 2025 15:25:22 -0500 Subject: [PATCH 251/420] Updated method getField() to return null instead of an exception if the requested record was not found --- src/Socialbox/Managers/PeerInformationManager.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Socialbox/Managers/PeerInformationManager.php b/src/Socialbox/Managers/PeerInformationManager.php index f529b19..1cbe88f 100644 --- a/src/Socialbox/Managers/PeerInformationManager.php +++ b/src/Socialbox/Managers/PeerInformationManager.php @@ -171,10 +171,10 @@ * * @param string|PeerRecord $peerUuid The UUID of the peer to get the property from. * @param InformationFieldName $property The name of the property to get. - * @return PeerInformationFieldRecord + * @return PeerInformationFieldRecord|null The property record, or null if it does not exist. * @throws DatabaseOperationException Thrown if the operation fails. */ - public static function getField(string|PeerRecord $peerUuid, InformationFieldName $property): PeerInformationFieldRecord + public static function getField(string|PeerRecord $peerUuid, InformationFieldName $property): ?PeerInformationFieldRecord { if($peerUuid instanceof PeerRecord) { @@ -192,7 +192,7 @@ $result = $stmt->fetch(); if($result === false) { - throw new DatabaseOperationException(sprintf('Property %s does not exist for peer %s', $property->value, $peerUuid)); + return null; } return PeerInformationFieldRecord::fromArray($result); From fe217002e3cc36f02b315f89140f529eba91edce Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 29 Jan 2025 15:25:36 -0500 Subject: [PATCH 252/420] Added method (incomplete) ResolvePeerSignature --- .../StandardMethods/ResolvePeerSignature.php | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/Socialbox/Classes/StandardMethods/ResolvePeerSignature.php diff --git a/src/Socialbox/Classes/StandardMethods/ResolvePeerSignature.php b/src/Socialbox/Classes/StandardMethods/ResolvePeerSignature.php new file mode 100644 index 0000000..be46141 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/ResolvePeerSignature.php @@ -0,0 +1,68 @@ +containsParameter('peer')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'peer' parameter"); + } + + if(!$rpcRequest->containsParameter('uuid')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'uuid' parameter"); + } + + try + { + $uuid = Uuid::fromString($rpcRequest->getParameter('uuid')); + } + catch(InvalidArgumentException $e) + { + throw new StandardException('Invalid UUID', StandardError::RPC_INVALID_ARGUMENTS, $e); + } + + // Parse the peer address + try + { + $peerAddress = PeerAddress::fromAddress($rpcRequest->getParameter('peer')); + } + catch(InvalidArgumentException $e) + { + throw new StandardException('Peer Address Error: ' . $e->getMessage(), StandardError::RPC_INVALID_ARGUMENTS, $e); + } + + try + { + return $rpcRequest->produceResponse(Socialbox::resolvePeerSignature($peerAddress, $uuid->toRfc4122())); + } + catch(StandardException $e) + { + throw $e; + } + catch (Exception $e) + { + throw new StandardException('Failed to resolve peer signature', StandardError::INTERNAL_SERVER_ERROR, $e); + } + } + } \ No newline at end of file From 38699b6bcc6a8d2581e5b22036b146a1ab9c42a1 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 29 Jan 2025 15:25:50 -0500 Subject: [PATCH 253/420] Added additional methods and the ability to create signature keys --- src/Socialbox/Classes/RpcClient.php | 102 +++++++++++++++++++++++++++- 1 file changed, 100 insertions(+), 2 deletions(-) diff --git a/src/Socialbox/Classes/RpcClient.php b/src/Socialbox/Classes/RpcClient.php index a7b320a..e1ad9e1 100644 --- a/src/Socialbox/Classes/RpcClient.php +++ b/src/Socialbox/Classes/RpcClient.php @@ -2,6 +2,7 @@ namespace Socialbox\Classes; + use InvalidArgumentException; use Socialbox\Enums\StandardError; use Socialbox\Enums\StandardHeaders; use Socialbox\Enums\Types\RequestType; @@ -9,7 +10,8 @@ use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\ResolutionException; use Socialbox\Exceptions\RpcException; - use Socialbox\Objects\ExportedSession; + use Socialbox\Objects\Client\ExportedSession; + use Socialbox\Objects\Client\SignatureKeyPair; use Socialbox\Objects\KeyPair; use Socialbox\Objects\PeerAddress; use Socialbox\Objects\RpcRequest; @@ -35,6 +37,8 @@ private string $rpcEndpoint; private string $remoteServer; private string $sessionUuid; + private ?string $defaultSigningKey; + private array $signingKeys; /** * Constructs a new instance with the specified peer address. @@ -72,6 +76,8 @@ $this->privateSharedSecret = $exportedSession->getPrivateSharedSecret(); $this->clientTransportEncryptionKey = $exportedSession->getClientTransportEncryptionKey(); $this->serverTransportEncryptionKey = $exportedSession->getServerTransportEncryptionKey(); + $this->signingKeys = $exportedSession->getSigningKeys(); + $this->defaultSigningKey = $exportedSession->getDefaultSigningKey(); // Still solve the server information $this->serverInformation = self::getServerInformation(); @@ -100,6 +106,8 @@ } // Set the initial properties + $this->signingKeys = []; + $this->defaultSigningKey = null; $this->identifiedAs = $identifiedAs; $this->remoteServer = $server ?? $identifiedAs->getDomain(); @@ -701,6 +709,94 @@ return $this->sessionUuid; } + /** + * Returns the signing keys associated with the current instance. + * + * @return SignatureKeyPair[] The signing keys. + */ + public function getSigningKeys(): array + { + return $this->signingKeys; + } + + /** + * Adds a new signing key to the current instance. + * + * @param SignatureKeyPair $key The signing key to be added. + * @return void + */ + protected function addSigningKey(SignatureKeyPair $key): void + { + $this->signingKeys[$key->getUuid()] = $key; + + if($this->defaultSigningKey === null) + { + $this->defaultSigningKey = $key->getUuid(); + } + } + + /** + * @param string $uuid + * @return bool + */ + public function signingKeyExists(string $uuid): bool + { + return isset($this->signingKeys[$uuid]); + } + + /** + * Removes a signing key from the current instance. + * + * @param string $uuid The UUID of the signing key to be removed. + * @return void + */ + protected function removeSigningKey(string $uuid): void + { + unset($this->signingKeys[$uuid]); + + if($this->defaultSigningKey === $uuid) + { + $this->defaultSigningKey = null; + } + } + + /** + * Retrieves the signing key associated with the specified UUID. + * + * @param string $uuid The UUID of the signing key to be retrieved. + * @return SignatureKeyPair|null The signing key associated with the UUID, or null if not found. + */ + public function getSigningKey(string $uuid): ?SignatureKeyPair + { + return $this->signingKeys[$uuid] ?? null; + } + + /** + * Retrieves the default signing key associated with the current instance. + * + * @return SignatureKeyPair|null The default signing key. + */ + public function getDefaultSigningKey(): ?SignatureKeyPair + { + return $this->signingKeys[$this->defaultSigningKey] ?? null; + } + + /** + * Sets the default signing key for the current instance. + * + * @param string $uuid The UUID of the signing key to be set as default. + * @return void + */ + public function setDefaultSigningKey(string $uuid): void + { + if(!isset($this->signingKeys[$uuid])) + { + throw new InvalidArgumentException('The specified signing key does not exist'); + } + + $this->defaultSigningKey = $uuid; + } + /** * Exports the current session details into an ExportedSession object. * @@ -723,7 +819,9 @@ 'client_private_encryption_key' => $this->clientEncryptionKeyPair->getPrivateKey(), 'private_shared_secret' => $this->privateSharedSecret, 'client_transport_encryption_key' => $this->clientTransportEncryptionKey, - 'server_transport_encryption_key' => $this->serverTransportEncryptionKey + 'server_transport_encryption_key' => $this->serverTransportEncryptionKey, + 'default_signing_key' => $this->defaultSigningKey, + 'signing_keys' => array_map(fn(SignatureKeyPair $key) => $key->toArray(), $this->signingKeys) ]); } } \ No newline at end of file From f76f76d31a0cebb9f0057cfb4223047fcbf66fe1 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 29 Jan 2025 15:26:10 -0500 Subject: [PATCH 254/420] Improved expiration check --- .../{SettingsAddSigningKey.php => SettingsAddSignature.php} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename src/Socialbox/Classes/StandardMethods/{SettingsAddSigningKey.php => SettingsAddSignature.php} (88%) diff --git a/src/Socialbox/Classes/StandardMethods/SettingsAddSigningKey.php b/src/Socialbox/Classes/StandardMethods/SettingsAddSignature.php similarity index 88% rename from src/Socialbox/Classes/StandardMethods/SettingsAddSigningKey.php rename to src/Socialbox/Classes/StandardMethods/SettingsAddSignature.php index 60b4f6a..f4a1b5a 100644 --- a/src/Socialbox/Classes/StandardMethods/SettingsAddSigningKey.php +++ b/src/Socialbox/Classes/StandardMethods/SettingsAddSignature.php @@ -13,7 +13,7 @@ use Socialbox\Objects\ClientRequest; use Socialbox\Objects\RpcRequest; - class SettingsAddSigningKey extends Method + class SettingsAddSignature extends Method { /** * @inheritDoc @@ -26,13 +26,13 @@ } $expires = null; - if($rpcRequest->containsParameter('expires')) + if($rpcRequest->containsParameter('expires') && $rpcRequest->getParameter('expires') !== null) { $expires = (int)$rpcRequest->getParameter('expires'); } $name = null; - if($rpcRequest->containsParameter('name')) + if($rpcRequest->containsParameter('name') && $rpcRequest->getParameter('name') !== null) { $name = $rpcRequest->getParameter('name'); } From 42d276939bc78097f8d7292e2f522841c9f18e55 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 29 Jan 2025 15:26:22 -0500 Subject: [PATCH 255/420] Added SettingsGetInformationField as a standard method --- .../SettingsGetInformationField.php | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/Socialbox/Classes/StandardMethods/SettingsGetInformationField.php diff --git a/src/Socialbox/Classes/StandardMethods/SettingsGetInformationField.php b/src/Socialbox/Classes/StandardMethods/SettingsGetInformationField.php new file mode 100644 index 0000000..640e12e --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/SettingsGetInformationField.php @@ -0,0 +1,48 @@ +containsParameter('field')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The required field parameter is missing'); + } + $fieldName = InformationFieldName::tryFrom(strtoupper($rpcRequest->getParameter('field'))); + if($fieldName === null) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The provided field parameter is invalid'); + } + + try + { + $fieldRecord = PeerInformationManager::getField($request->getPeer(), $fieldName); + } + catch(DatabaseOperationException $e) + { + throw new StandardException('Failed to retrieve existing information fields', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if($fieldRecord === null) + { + return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The requested field does not exist'); + } + + return $rpcRequest->produceResponse($fieldRecord->toInformationFieldState()); + } + } \ No newline at end of file From 6732112e127c87b60075ed3bef423beaf636b666 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 29 Jan 2025 15:26:27 -0500 Subject: [PATCH 256/420] Added SettingsGetSigningKey as a standard method --- .../StandardMethods/SettingsGetSigningKey.php | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/Socialbox/Classes/StandardMethods/SettingsGetSigningKey.php diff --git a/src/Socialbox/Classes/StandardMethods/SettingsGetSigningKey.php b/src/Socialbox/Classes/StandardMethods/SettingsGetSigningKey.php new file mode 100644 index 0000000..e004dea --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/SettingsGetSigningKey.php @@ -0,0 +1,43 @@ +containsParameter('uuid') && $rpcRequest->getParameter('uuid') !== null) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'uuid' parameter"); + } + + try + { + $key = SigningKeysManager::getSigningKey($request->getPeer()->getUuid(), $rpcRequest->getParameter('uuid')); + } + catch (DatabaseOperationException $e) + { + throw new StandardException('Failed to get the signing keys', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if($key === null) + { + return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The signing key does not exist'); + } + + // Return the signing key + return $rpcRequest->produceResponse($key->toStandard()); + } + } \ No newline at end of file From 75897da9e9e219d55c39ec27275ced24606da9df Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 29 Jan 2025 15:26:36 -0500 Subject: [PATCH 257/420] Added SignatureKeyPair for a client object --- .../Objects/Client/SignatureKeyPair.php | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 src/Socialbox/Objects/Client/SignatureKeyPair.php diff --git a/src/Socialbox/Objects/Client/SignatureKeyPair.php b/src/Socialbox/Objects/Client/SignatureKeyPair.php new file mode 100644 index 0000000..c758db8 --- /dev/null +++ b/src/Socialbox/Objects/Client/SignatureKeyPair.php @@ -0,0 +1,100 @@ +uuid = $data['uuid']; + $this->name = $data['name'] ?? null; + $this->publicKey = $data['public_key']; + $this->privateKey = $data['private_key']; + $this->expires = (int)$data['expires'] ?? null; + } + + /** + * Returns the UUID of the key pair + * + * @return string The UUID of the key pair + */ + public function getUuid(): string + { + return $this->uuid; + } + + /** + * Returns the name of the key pair + * + * @return string|null The name of the key pair + */ + public function getName(): ?string + { + return $this->name; + } + + /** + * Returns the public key of the key pair + * + * @return string The public key of the key pair + */ + public function getPublicKey(): string + { + return $this->publicKey; + } + + /** + * Returns the private key of the key pair + * + * @return string The private key of the key pair + */ + public function getPrivateKey(): string + { + return $this->privateKey; + } + + /** + * Returns the expiration date of the key pair + * + * @return int|null The expiration date of the key pair + */ + public function getExpires(): ?int + { + return $this->expires; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): SignatureKeyPair + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'uuid' => $this->uuid, + 'name' => $this->name, + 'public_key' => $this->publicKey, + 'private_key' => $this->privateKey, + 'expires' => $this->expires + ]; + } + } \ No newline at end of file From 716a5f15eb42f2e87f4b32c7cab0a07542f34073 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 29 Jan 2025 15:26:51 -0500 Subject: [PATCH 258/420] Minor changes --- tests/Socialbox/SocialClientTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Socialbox/SocialClientTest.php b/tests/Socialbox/SocialClientTest.php index b4e143b..992ec53 100644 --- a/tests/Socialbox/SocialClientTest.php +++ b/tests/Socialbox/SocialClientTest.php @@ -42,11 +42,11 @@ return 'user' . $randomString . '@' . $domain; } - private static function registerUser(string $domain): SocialClient + private static function registerUser(string $domain, string $displayName): SocialClient { $client = new SocialClient(self::generateUsername($domain)); $client->settingsSetPassword("password"); - $client->settingsAddInformationField(InformationFieldName::DISPLAY_NAME, "Example User"); + $client->settingsAddInformationField(InformationFieldName::DISPLAY_NAME, $displayName); return $client; } @@ -72,9 +72,9 @@ public function testResolveDecentralizedPeer(): void { - $coffeeUser = self::registerUser(self::COFFEE_DOMAIN); + $coffeeUser = self::registerUser(self::COFFEE_DOMAIN, "Coffee Lover"); $this->assertTrue($coffeeUser->getSessionState()->isAuthenticated()); - $teapotUser = self::registerUser(self::TEAPOT_DOMAIN); + $teapotUser = self::registerUser(self::TEAPOT_DOMAIN, "Tea & Biscuits"); $this->assertTrue($teapotUser->getSessionState()->isAuthenticated()); $coffeePeer = $coffeeUser->resolvePeer($teapotUser->getIdentifiedAs()); From c2330b374ab7e4fe767dd6d8c88f7bba54841663 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 29 Jan 2025 15:26:58 -0500 Subject: [PATCH 259/420] Added TextCaptchaVerification --- .../Standard/TextCaptchaVerification.php | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/Socialbox/Objects/Standard/TextCaptchaVerification.php diff --git a/src/Socialbox/Objects/Standard/TextCaptchaVerification.php b/src/Socialbox/Objects/Standard/TextCaptchaVerification.php new file mode 100644 index 0000000..5795497 --- /dev/null +++ b/src/Socialbox/Objects/Standard/TextCaptchaVerification.php @@ -0,0 +1,60 @@ +expires = $data['expires']; + $this->question = $data['question']; + } + + /** + * Returns the expiration time of the captcha + * + * @return int The expiration time of the captcha in Unix timestamp format + */ + public function getExpires(): int + { + return $this->expires; + } + + /** + * Returns the question of the captcha + * + * @return string The question of the captcha + */ + public function getQuestion(): string + { + return $this->question; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): TextCaptchaVerification + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'expires' => $this->expires, + 'question' => $this->question + ]; + } + } \ No newline at end of file From 3fe2d9e0df1476bcbf2ba6fd865a12b2d2d1f87e Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 29 Jan 2025 15:27:09 -0500 Subject: [PATCH 260/420] Updated VerificationGetImageCaptcha to return the new object --- .../Classes/StandardMethods/VerificationGetImageCaptcha.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php b/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php index 34672d7..cfbc27e 100644 --- a/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php +++ b/src/Socialbox/Classes/StandardMethods/VerificationGetImageCaptcha.php @@ -12,7 +12,7 @@ use Socialbox\Managers\CaptchaManager; use Socialbox\Objects\ClientRequest; use Socialbox\Objects\RpcRequest; - use Socialbox\Objects\Standard\ImageCaptcha; + use Socialbox\Objects\Standard\ImageCaptchaVerification; class VerificationGetImageCaptcha extends Method { @@ -59,7 +59,7 @@ // Build the captcha // Returns HTML base64 encoded image of the captcha // Important note: Must always be HTML-BASE64 which means it must be prefixed with `data:image/jpeg;base64,` - return $rpcRequest->produceResponse(new ImageCaptcha([ + return $rpcRequest->produceResponse(new ImageCaptchaVerification([ 'expires' => $captchaRecord->getExpires(), 'content' => (new CaptchaBuilder($answer))->build()->inline() ])); From 7809f32a5e9b151f9872b5ec7094222d52d5e786 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 29 Jan 2025 15:27:21 -0500 Subject: [PATCH 261/420] Refactored StandardMethods and added to-be-implemented methods --- src/Socialbox/Enums/StandardMethods.php | 83 ++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 10 deletions(-) diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index 4c445ee..775da0a 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -9,6 +9,7 @@ use Socialbox\Classes\StandardMethods\AddressBookAddContact; use Socialbox\Classes\StandardMethods\AddressBookDeleteContact; use Socialbox\Classes\StandardMethods\AddressBookGetContacts; + use Socialbox\Classes\StandardMethods\AddressBookUpdateRelationship; use Socialbox\Classes\StandardMethods\Authenticate; use Socialbox\Classes\StandardMethods\GetAllowedMethods; use Socialbox\Classes\StandardMethods\GetCommunityGuidelines; @@ -17,12 +18,16 @@ use Socialbox\Classes\StandardMethods\GetTermsOfService; use Socialbox\Classes\StandardMethods\Ping; use Socialbox\Classes\StandardMethods\ResolvePeer; + use Socialbox\Classes\StandardMethods\ResolvePeerSignature; use Socialbox\Classes\StandardMethods\SettingsAddInformationField; - use Socialbox\Classes\StandardMethods\SettingsAddSigningKey; + use Socialbox\Classes\StandardMethods\SettingsAddSignature; use Socialbox\Classes\StandardMethods\SettingsDeleteInformationField; use Socialbox\Classes\StandardMethods\SettingsDeleteOtp; use Socialbox\Classes\StandardMethods\SettingsDeletePassword; + use Socialbox\Classes\StandardMethods\SettingsDeleteSignature; + use Socialbox\Classes\StandardMethods\SettingsGetInformationField; use Socialbox\Classes\StandardMethods\SettingsGetInformationFields; + use Socialbox\Classes\StandardMethods\SettingsGetSigningKey; use Socialbox\Classes\StandardMethods\SettingsGetSigningKeys; use Socialbox\Classes\StandardMethods\SettingsSetOtp; use Socialbox\Classes\StandardMethods\SettingsSetPassword; @@ -56,6 +61,7 @@ case GET_COMMUNITY_GUIDELINES = 'getCommunityGuidelines'; case ACCEPT_COMMUNITY_GUIDELINES = 'acceptCommunityGuidelines'; + case VERIFICATION_AUTHENTICATE = 'authenticate'; case VERIFICATION_EMAIL = 'verificationEmail'; // NOT IMPLEMENTED case VERIFICATION_ANSWER_EMAIL = 'verificationAnswerEmail'; // NOT IMPLEMENTED case VERIFICATION_SMS = 'verificationSms'; // NOT IMPLEMENTED @@ -78,19 +84,60 @@ case SETTINGS_DELETE_OTP = 'settingsDeleteOtp'; case SETTINGS_ADD_INFORMATION_FIELD = 'settingsAddInformationField'; case SETTINGS_GET_INFORMATION_FIELDS = 'settingsGetInformationFields'; + case SETTINGS_GET_INFORMATION_FIELD = 'settingsGetInformationField'; case SETTINGS_UPDATE_INFORMATION_FIELD = 'settingsUpdateInformationField'; case SETTINGS_DELETE_INFORMATION_FIELD = 'settingsDeleteInformationField'; case SETTINGS_UPDATE_INFORMATION_PRIVACY = 'settingsUpdateInformationPrivacy'; - case SETTINGS_ADD_SIGNING_KEY = 'settingsAddSigningKey'; - case SETTINGS_GET_SIGNING_KEYS = 'settingsGetSigningKeys'; + case SETTINGS_ADD_SIGNATURE = 'settingsAddSigningKey'; + case SETTINGS_DELETE_SIGNATURE = 'settingsDeleteSigningKey'; + case SETTINGS_GET_SIGNATURES = 'settingsGetSigningKeys'; + case SETTINGS_GET_SIGNATURE = 'settingsGetSigningKey'; case ADDRESS_BOOK_ADD_CONTACT = 'addressBookAddContact'; case ADDRESS_BOOK_DELETE_CONTACT = 'addressBookDeleteContact'; case ADDRESS_BOOK_GET_CONTACTS = 'addressBookGetContacts'; + case ADDRESS_BOOK_UPDATE_RELATIONSHIP = 'addressBookUpdateRelationship'; + case ADDRESS_BOOK_TRUST_SIGNATURE = 'addressBookTrustSignature'; + + case GET_STATE = 'getState'; + + // End-to-End channels for communication purposes + case END_TO_END_CREATE_REQUEST = 'e2eCreateRequest'; + case END_TO_END_GET_REQUESTS = 'e2eGetRequests'; + case END_TO_END_ACCEPT_REQUEST = 'e2eAcceptRequest'; + case END_TO_END_REJECT_REQUEST = 'e2eRejectRequest'; + case END_TO_END_GET_CHANNELS = 'e2eGetChannels'; + case END_TO_END_CLOSE_CHANNEL = 'e2eCloseChannel'; + + // Messaging methods + case MESSAGES_GET_INBOX = 'messagesGetInbox'; + case MESSAGES_GET_UNTRUSTED = 'messagesGetUntrusted'; + case MESSAGES_GET_ARCHIVED = 'messagesGetArchived'; + case MESSAGES_GET_OUTBOX = 'messagesGetOutbox'; + case MESSAGES_GET_MESSAGE = 'messagesGetMessage'; + case MESSAGES_GET_DRAFTS = 'messagesGetDrafts'; + case MESSAGES_GET_DRAFT = 'messagesGetDraft'; + case MESSAGES_TOGGLE_MESSAGE_READ = 'messagesToggleMessageRead'; + case MESSAGES_TOGGLE_MESSAGE_STAR = 'messagesToggleMessageStar'; + case MESSAGES_TOGGLE_MESSAGE_FLAG = 'messagesToggleMessageFlag'; + case MESSAGES_ARCHIVE_MESSAGE = 'messagesArchiveMessage'; + case MESSAGES_UNARCHIVE_MESSAGE = 'messagesUnarchiveMessage'; + case MESSAGES_DELETE_MESSAGE = 'messagesDeleteMessage'; + case MESSAGES_DELETE_DRAFT = 'messagesDeleteDraft'; + case MESSAGES_COMPOSE_NEW_MESSAGE = 'messagesComposeNewMessage'; + case MESSAGES_COMPOSE_REPLY_MESSAGE = 'messagesComposeReplyMessage'; + case MESSAGES_COMPOSE_FORWARD_MESSAGE = 'messagesComposeForwardMessage'; + case MESSAGES_SET_MESSAGE_RECIPIENTS = 'messagesSetMessageRecipients'; + case MESSAGES_SET_MESSAGE_CARBON_COPY_RECIPIENTS = 'messagesSetMessageCarbonCopyRecipients'; + case MESSAGES_SET_MESSAGE_BLIND_CARBON_COPY_RECIPIENTS = 'messagesSetMessageBlindCarbonCopyRecipients'; + case MESSAGES_SET_MESSAGE_ENCRYPTION_CHANNEL = 'messagesSetMessageEncryptionChannel'; + case MESSAGES_SET_MESSAGE_SUBJECT = 'messagesSetMessageSubject'; + case MESSAGES_SET_MESSAGE_BODY = 'messagesSetMessageBody'; + case MESSAGES_SEND_MESSAGE = 'messagesSendMessage'; - case AUTHENTICATE = 'authenticate'; case RESOLVE_PEER = 'resolvePeer'; + case RESOLVE_PEER_SIGNATURE = 'resolvePeerSignature'; /** * Executes the appropriate operation based on the current context and requests provided. @@ -129,19 +176,25 @@ self::SETTINGS_ADD_INFORMATION_FIELD => SettingsAddInformationField::execute($request, $rpcRequest), self::SETTINGS_GET_INFORMATION_FIELDS => SettingsGetInformationFields::execute($request, $rpcRequest), + self::SETTINGS_GET_INFORMATION_FIELD => SettingsGetInformationField::execute($request, $rpcRequest), self::SETTINGS_UPDATE_INFORMATION_FIELD => SettingsUpdateInformationField::execute($request, $rpcRequest), self::SETTINGS_UPDATE_INFORMATION_PRIVACY => SettingsUpdateInformationPrivacy::execute($request, $rpcRequest), self::SETTINGS_DELETE_INFORMATION_FIELD => SettingsDeleteInformationField::execute($request, $rpcRequest), - self::SETTINGS_ADD_SIGNING_KEY => SettingsAddSigningKey::execute($request, $rpcRequest), - self::SETTINGS_GET_SIGNING_KEYS => SettingsGetSigningKeys::execute($request, $rpcRequest), + self::SETTINGS_ADD_SIGNATURE => SettingsAddSignature::execute($request, $rpcRequest), + self::SETTINGS_DELETE_SIGNATURE => SettingsDeleteSignature::execute($request, $rpcRequest), + self::SETTINGS_GET_SIGNATURES => SettingsGetSigningKeys::execute($request, $rpcRequest), + self::SETTINGS_GET_SIGNATURE => SettingsGetSigningKey::execute($request, $rpcRequest), self::ADDRESS_BOOK_ADD_CONTACT => AddressBookAddContact::execute($request, $rpcRequest), self::ADDRESS_BOOK_DELETE_CONTACT => AddressBookDeleteContact::execute($request, $rpcRequest), self::ADDRESS_BOOK_GET_CONTACTS => AddressBookGetContacts::execute($request, $rpcRequest), + self::ADDRESS_BOOK_UPDATE_RELATIONSHIP => AddressBookUpdateRelationship::execute($request, $rpcRequest), + self::ADDRESS_BOOK_TRUST_SIGNATURE => AddressBookTrustSignature::execute($request, $rpcRequest), - self::AUTHENTICATE => Authenticate::execute($request, $rpcRequest), + self::VERIFICATION_AUTHENTICATE => Authenticate::execute($request, $rpcRequest), self::RESOLVE_PEER => ResolvePeer::execute($request, $rpcRequest), + self::RESOLVE_PEER_SIGNATURE => ResolvePeerSignature::execute($request, $rpcRequest), default => $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, sprintf("The method %s is not supported by the server", $rpcRequest->getMethod())) }; @@ -239,7 +292,7 @@ $session = $clientRequest->getSession(); if(!$session->isAuthenticated() || $session->flagExists(SessionFlags::AUTHENTICATION_REQUIRED)) { - $methods[] = self::AUTHENTICATE; + $methods[] = self::VERIFICATION_AUTHENTICATE; } else { @@ -259,17 +312,24 @@ // These methods are always allowed for authenticated users $methods = [ - self::SETTINGS_ADD_SIGNING_KEY, - self::SETTINGS_GET_SIGNING_KEYS, + self::SETTINGS_ADD_SIGNATURE, + self::SETTINGS_GET_SIGNATURES, + self::SETTINGS_GET_SIGNATURE, + self::SETTINGS_ADD_INFORMATION_FIELD, self::SETTINGS_GET_INFORMATION_FIELDS, + self::SETTINGS_GET_INFORMATION_FIELD, self::SETTINGS_UPDATE_INFORMATION_FIELD, self::SETTINGS_UPDATE_INFORMATION_PRIVACY, self::SETTINGS_DELETE_INFORMATION_FIELD, + self::SETTINGS_SET_PASSWORD, + self::SETTINGS_DELETE_PASSWORD, self::SETTINGS_UPDATE_PASSWORD, self::SETTINGS_SET_OTP, + self::SETTINGS_DELETE_OTP, self::RESOLVE_PEER, + self::RESOLVE_PEER_SIGNATURE, self::ADDRESS_BOOK_ADD_CONTACT, self::ADDRESS_BOOK_DELETE_CONTACT, @@ -295,7 +355,10 @@ $methods = [ self::SETTINGS_ADD_INFORMATION_FIELD, + self::SETTINGS_GET_INFORMATION_FIELDS, + self::SETTINGS_GET_INFORMATION_FIELD, self::SETTINGS_UPDATE_INFORMATION_FIELD, + self::SETTINGS_UPDATE_INFORMATION_PRIVACY, self::SETTINGS_DELETE_INFORMATION_FIELD ]; From b1c669dfa846582ca855eecf6800cb3caceb74d3 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 29 Jan 2025 15:40:14 -0500 Subject: [PATCH 262/420] Added check if the peer is enabled and if it uses a password, a password check is required to set the otp post registration --- .../Classes/StandardMethods/SettingsSetOtp.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/SettingsSetOtp.php b/src/Socialbox/Classes/StandardMethods/SettingsSetOtp.php index 1c959cf..be07301 100644 --- a/src/Socialbox/Classes/StandardMethods/SettingsSetOtp.php +++ b/src/Socialbox/Classes/StandardMethods/SettingsSetOtp.php @@ -37,13 +37,21 @@ throw new StandardException('Failed to check One Time Password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } - try + if($peer->isEnabled()) { - $usesPassword = PasswordManager::usesPassword($peer); + try + { + // If the peer is disabled, the password is not used because we assume the peer is registering + $usesPassword = PasswordManager::usesPassword($peer); + } + catch (DatabaseOperationException $e) + { + throw new StandardException('Failed to check password usage due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + } } - catch (DatabaseOperationException $e) + else { - throw new StandardException('Failed to check password usage due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + $usesPassword = false; } // Password verification is required to set an OTP if a password is set From 5e26b08f9e196f3277a3c81dfb7d400141629f0a Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 29 Jan 2025 15:40:24 -0500 Subject: [PATCH 263/420] Added additional check --- src/Socialbox/Objects/Standard/SigningKey.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Socialbox/Objects/Standard/SigningKey.php b/src/Socialbox/Objects/Standard/SigningKey.php index 0dd7757..fdf2ecb 100644 --- a/src/Socialbox/Objects/Standard/SigningKey.php +++ b/src/Socialbox/Objects/Standard/SigningKey.php @@ -104,7 +104,7 @@ */ public function getState(): SigningKeyState { - if(time() > $this->expires) + if($this->expires > 0 && time() > $this->expires) { return SigningKeyState::EXPIRED; } @@ -143,7 +143,7 @@ 'uuid' => $record->getUuid(), 'name' => $record->getName(), 'public_key' => $record->getPublicKey(), - 'state' => $record->getState(), + 'state' => $record->getState()->value, 'expires' => $record->getExpires(), 'created' => $record->getCreated() ]); @@ -166,7 +166,7 @@ 'uuid' => $this->uuid, 'name' => $this->name, 'public_key' => $this->publicKey, - 'state' => $this->state->value, + 'state' => $this->getState()->value, 'expires' => $this->expires, 'created' => $this->created ]; From 10e1368942e1fb3b86bceef6064e7b3d5f3fd43a Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 29 Jan 2025 15:40:40 -0500 Subject: [PATCH 264/420] Added the ability to parse created and expired as strings/null --- .../Objects/Database/SigningKeyRecord.php | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/Socialbox/Objects/Database/SigningKeyRecord.php b/src/Socialbox/Objects/Database/SigningKeyRecord.php index c00bc3e..62ac8af 100644 --- a/src/Socialbox/Objects/Database/SigningKeyRecord.php +++ b/src/Socialbox/Objects/Database/SigningKeyRecord.php @@ -47,9 +47,24 @@ { $this->expires = $data['expires']->getTimestamp(); } + elseif(is_string($data['expires'])) + { + if(empty($data['expires'])) + { + $this->expires = 0; + } + else + { + $this->expires = strtotime($data['expires']); + } + } + elseif($data['expires'] === null) + { + $this->expires = 0; + } else { - throw new InvalidArgumentException('Invalid expires value'); + throw new InvalidArgumentException('Invalid expires value, got type: ' . gettype($data['expires'])); } if(is_int($data['created'])) @@ -60,9 +75,20 @@ { $this->created = $data['created']->getTimestamp(); } + elseif(is_string($data['created'])) + { + if(empty($data['created'])) + { + $this->created = 0; + } + else + { + $this->created = strtotime($data['created']); + } + } else { - throw new InvalidArgumentException('Invalid created value'); + throw new InvalidArgumentException('Invalid created value type, got ' . gettype($data['created'])); } } From 94a7ce05f84034d76dcb96678ba2e6b26409f777 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 29 Jan 2025 15:40:59 -0500 Subject: [PATCH 265/420] Added additional conditional checks and minor corrections --- src/Socialbox/Managers/SigningKeysManager.php | 99 +++++++++---------- 1 file changed, 49 insertions(+), 50 deletions(-) diff --git a/src/Socialbox/Managers/SigningKeysManager.php b/src/Socialbox/Managers/SigningKeysManager.php index c544f15..f55a482 100644 --- a/src/Socialbox/Managers/SigningKeysManager.php +++ b/src/Socialbox/Managers/SigningKeysManager.php @@ -15,44 +15,6 @@ class SigningKeysManager { - /** - * Retrieves the state of a signing key identified by its UUID. - * - * @param string $uuid The UUID of the signing key whose state is to be retrieved. - * @return SigningKeyState The state of the signing key. Returns SigningKeyState::EXPIRED if the key is expired, - * SigningKeyState::NOT_FOUND if the key does not exist, or the state as defined in the database. - * @throws DatabaseOperationException If an error occurs during the database operation. - */ - public static function getSigningKeyState(string $uuid): SigningKeyState - { - try - { - $statement = Database::getConnection()->prepare("SELECT state, expires FROM signing_keys WHERE uuid=:uuid"); - $statement->bindParam(':uuid', $uuid); - $statement->execute(); - - if($row = $statement->fetch()) - { - if(is_int($row['expires']) && $row['expires'] < time()) - { - return SigningKeyState::EXPIRED; - } - - if($row['expires'] instanceof DateTime && $row['expires'] < new DateTime()) - { - return SigningKeyState::EXPIRED; - } - - return SigningKeyState::tryFrom($row['state']) ?? SigningKeyState::NOT_FOUND; - } - } - catch (PDOException $e) - { - throw new DatabaseOperationException('Failed to get the signing key state from the database', $e); - } - - return SigningKeyState::NOT_FOUND; - } /** * Retrieves the count of signing keys associated with a specific peer UUID. @@ -104,17 +66,27 @@ throw new InvalidArgumentException('The name cannot be empty'); } - if($expires !== null && $expires < time()) + if($expires !== null) { - throw new InvalidArgumentException('The expiration time is in the past'); - } + if($expires === 0) + { + $expires = null; + } + else + { + if($expires < time()) + { + throw new InvalidArgumentException('The expiration time is in the past'); + } - // At least more than 1 hour - if($expires !== null && $expires < time() + 3600) - { - throw new InvalidArgumentException('The expiration time is too soon, must be at least 1 hour in the future'); - } + if($expires < time() + 3600) + { + throw new InvalidArgumentException('The expiration time is too soon, must be at least 1 hour in the future'); + } + $expires = (new DateTime())->setTimestamp($expires)->format('Y-m-d H:i:s'); + } + } $uuid = UuidV4::v4()->toRfc4122(); @@ -139,19 +111,21 @@ /** * Updates the state of a signing key in the database identified by its UUID. * + * @param string $peerUuid The UUID of the peer associated with the signing key. * @param string $uuid The unique identifier of the signing key to update. * @param SigningKeyState $state The new state to set for the signing key. * @return void * @throws DatabaseOperationException */ - public static function updateSigningKeyState(string $uuid, SigningKeyState $state): void + public static function updateSigningKeyState(string $peerUuid, string $uuid, SigningKeyState $state): void { $state = $state->value; try { - $statement = Database::getConnection()->prepare("UPDATE signing_keys SET state=:state WHERE uuid=:uuid"); + $statement = Database::getConnection()->prepare("UPDATE signing_keys SET state=:state WHERE peer_uuid=:peer_uuid AND uuid=:uuid"); $statement->bindParam(':state', $state); + $statement->bindParam(':peer_uuid', $peerUuid); $statement->bindParam(':uuid', $uuid); $statement->execute(); } @@ -164,16 +138,18 @@ /** * Retrieves a signing key from the database using the provided UUID. * + * @param string $peerUuid The UUID of the peer associated with the signing key. * @param string $uuid The UUID of the signing key to retrieve. * @return SigningKeyRecord|null The signing key record if found, or null if no record exists. * @throws DatabaseOperationException If a database error occurs during the operation. */ - public static function getSigningKey(string $uuid): ?SigningKeyRecord + public static function getSigningKey(string $peerUuid, string $uuid): ?SigningKeyRecord { try { - $statement = Database::getConnection()->prepare("SELECT * FROM signing_keys WHERE uuid=:uuid"); + $statement = Database::getConnection()->prepare("SELECT * FROM signing_keys WHERE uuid=:uuid AND peer_uuid=:peer_uuid"); $statement->bindParam(':uuid', $uuid); + $statement->bindParam(':peer_uuid', $peerUuid); $statement->execute(); if($row = $statement->fetch()) @@ -218,6 +194,29 @@ } } + /** + * Deletes a signing key from the database using the provided UUID. + * + * @param string $peerUuid The UUID of the peer associated with the signing key. + * @param string $uuid The UUID of the signing key to delete. + * @return void + * @throws DatabaseOperationException If a database error occurs during the operation. + */ + public static function deleteSigningKey(string $peerUuid, string $uuid): void + { + try + { + $statement = Database::getConnection()->prepare("DELETE FROM signing_keys WHERE uuid=:uuid AND peer_uuid=:peer_uuid"); + $statement->bindParam(':uuid', $uuid); + $statement->bindParam(':peer_uuid', $peerUuid); + $statement->execute(); + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to delete the signing key from the database', $e); + } + } + /** * Verifies the digital signature of a message using the signing key associated with a specific UUID. * From f073b8d0049176c8f267642d5fd0b8bee87d94d9 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 29 Jan 2025 15:41:14 -0500 Subject: [PATCH 266/420] Added TODO --- src/Socialbox/Managers/SigningKeysManager.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Socialbox/Managers/SigningKeysManager.php b/src/Socialbox/Managers/SigningKeysManager.php index f55a482..4d30e1c 100644 --- a/src/Socialbox/Managers/SigningKeysManager.php +++ b/src/Socialbox/Managers/SigningKeysManager.php @@ -229,6 +229,7 @@ */ public static function verifySignature(string $message, string $signature, string $uuid): bool { + // TODO: Complete this $signingKey = self::getSigningKey($uuid); if($signingKey === null) { From 7d5071747b8348047f6ace18b8f369bd8e12f085 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 29 Jan 2025 15:41:25 -0500 Subject: [PATCH 267/420] Added SigningKeyState --- src/Socialbox/Enums/SigningKeyState.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Socialbox/Enums/SigningKeyState.php b/src/Socialbox/Enums/SigningKeyState.php index 403d930..6e55ce7 100644 --- a/src/Socialbox/Enums/SigningKeyState.php +++ b/src/Socialbox/Enums/SigningKeyState.php @@ -4,7 +4,6 @@ enum SigningKeyState : string { - case ACTIVE = 'active'; - case EXPIRED = 'expired'; - case NOT_FOUND = 'not_found'; + case ACTIVE = 'ACTIVE'; + case EXPIRED = 'EXPIRED'; } From 59d08b88b1a7b59328ce3647c85dba5d21cfb186 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 29 Jan 2025 15:41:49 -0500 Subject: [PATCH 268/420] Added decentralized method resolvePeerSignature --- src/Socialbox/Socialbox.php | 83 +++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index f6bfb88..f77a878 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -32,11 +32,13 @@ use Socialbox\Managers\PeerInformationManager; use Socialbox\Managers\RegisteredPeerManager; use Socialbox\Managers\SessionManager; + use Socialbox\Managers\SigningKeysManager; use Socialbox\Objects\ClientRequest; use Socialbox\Objects\PeerAddress; use Socialbox\Objects\Standard\InformationField; use Socialbox\Objects\Standard\Peer; use Socialbox\Objects\Standard\ServerInformation; + use Socialbox\Objects\Standard\SigningKey; use Throwable; class Socialbox @@ -747,6 +749,87 @@ } } + /** + * Resolves a peer signature key based on the given peer address or string identifier. + * + * @param PeerAddress|string $peerAddress The peer address or string identifier to be resolved. + * @param string $signatureUuid The UUID of the signature key to be resolved. + * @return SigningKey The resolved signing key for the peer. + * @throws StandardException If there was an error while resolving the peer signature key. + */ + public static function resolvePeerSignature(PeerAddress|string $peerAddress, string $signatureUuid): SigningKey + { + // Convert string peer address to object PeerAddress + if(is_string($peerAddress)) + { + try + { + $peerAddress = PeerAddress::fromAddress($peerAddress); + } + catch(InvalidArgumentException $e) + { + throw new StandardException($e->getMessage(), StandardError::RPC_INVALID_ARGUMENTS, $e); + } + } + + // Prevent resolutions against any host + if($peerAddress->getUsername() == ReservedUsernames::HOST) + { + throw new StandardException('Cannot resolve signature for a host peer', StandardError::FORBIDDEN); + } + + // If the peer is registered within this server + if($peerAddress->getDomain() === Configuration::getInstanceConfiguration()->getDomain()) + { + + try + { + $peer = RegisteredPeerManager::getPeerByAddress($peerAddress); + if($peer === null || !$peer?->isEnabled()) + { + // Fail if the peer is not found or enabled + throw new StandardException(sprintf('The peer %s does not exist', $peerAddress), StandardError::PEER_NOT_FOUND); + } + + $signingKey = SigningKeysManager::getSigningKey($peer->getUuid(), $signatureUuid); + if($signingKey === null) + { + throw new StandardException(sprintf('The requested signing key %s was not found', $signatureUuid), StandardError::NOT_FOUND); + } + } + catch(StandardException $e) + { + throw $e; + } + catch(Exception $e) + { + throw new StandardException('There was an error while trying to resolve the signature key for the peer locally', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $signingKey->toStandard(); + } + + // The requested peer is coming from an external server + try + { + $client = self::getExternalSession($peerAddress->getDomain()); + } + catch(Exception $e) + { + throw new StandardException(sprintf('There was an error while trying to communicate with %s', $peerAddress->getDomain()), StandardError::RESOLUTION_FAILED, $e); + } + + try + { + return $client->resolvePeerSignature($peerAddress, $signatureUuid); + } + catch(RpcException $e) + { + // Reflect the server error to the client + throw new StandardException($e->getMessage(), StandardError::tryFrom((int)$e->getCode()) ?? StandardError::UNKNOWN, $e); + } + } + /** * Resolves an external peer based on the given peer address or string identifier. * From 6f9c2ef5178c0e9f302091d196d3abf0d4f4c01b Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 29 Jan 2025 15:42:11 -0500 Subject: [PATCH 269/420] Added additional methods and updated the return type for some of the existing methods --- src/Socialbox/SocialClient.php | 246 +++++++++++++++++++++++++++------ 1 file changed, 201 insertions(+), 45 deletions(-) diff --git a/src/Socialbox/SocialClient.php b/src/Socialbox/SocialClient.php index cf6cb70..0295c0b 100644 --- a/src/Socialbox/SocialClient.php +++ b/src/Socialbox/SocialClient.php @@ -14,13 +14,18 @@ use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\ResolutionException; use Socialbox\Exceptions\RpcException; - use Socialbox\Objects\ExportedSession; + use Socialbox\Objects\Client\ExportedSession; + use Socialbox\Objects\Client\SignatureKeyPair; use Socialbox\Objects\PeerAddress; use Socialbox\Objects\RpcRequest; + use Socialbox\Objects\Standard\ExternalUrlVerification; + use Socialbox\Objects\Standard\ImageCaptchaVerification; use Socialbox\Objects\Standard\InformationField; use Socialbox\Objects\Standard\Peer; use Socialbox\Objects\Standard\ServerDocument; use Socialbox\Objects\Standard\SessionState; + use Socialbox\Objects\Standard\SigningKey; + use Socialbox\Objects\Standard\TextCaptchaVerification; class SocialClient extends RpcClient { @@ -160,16 +165,13 @@ /** * Sends a verification email to the specified email address by making a remote procedure call request. * - * @param string $emailAddress The email address to which the verification email will be sent. * @return true Indicates the successful initiation of the verification process. * @throws RpcException Thrown if the RPC request fails. */ - public function verificationEmail(string $emailAddress): true + public function verificationEmail(): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_EMAIL, Utilities::randomCrc32(), [ - 'email_address' => $emailAddress - ]) + new RpcRequest(StandardMethods::VERIFICATION_EMAIL, Utilities::randomCrc32()) )->getResponse()->getResult(); } @@ -192,16 +194,13 @@ /** * Sends a verification SMS to the specified phone number by initiating a remote procedure call. * - * @param string $phoneNumber The phone number to which the verification SMS should be sent. * @return true True if the SMS was sent successfully. * @throws RpcException Thrown if the RPC request fails. */ - public function verificationSms(string $phoneNumber): true + public function verificationSms(): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_SMS, Utilities::randomCrc32(), [ - 'phone_number' => $phoneNumber - ]) + new RpcRequest(StandardMethods::VERIFICATION_SMS, Utilities::randomCrc32()) )->getResponse()->getResult(); } @@ -224,16 +223,13 @@ /** * Initiates a phone verification process by sending a remote procedure call request. * - * @param string $phoneNumber The phone number to be verified. * @return bool True if the phone verification request was successful. * @throws RpcException Thrown if the RPC request fails. */ - public function verificationPhone(string $phoneNumber): true + public function verificationPhone(): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_PHONE_CALL, Utilities::randomCrc32(), [ - 'phone_number' => $phoneNumber - ]) + new RpcRequest(StandardMethods::VERIFICATION_PHONE_CALL, Utilities::randomCrc32()) )->getResponse()->getResult(); } @@ -256,14 +252,14 @@ /** * Retrieves the image captcha for verification purposes by sending a remote procedure call request. * - * @return string The result of the image captcha request. + * @return ImageCaptchaVerification The result of the image captcha request. * @throws RpcException Thrown if the RPC request fails. */ - public function verificationGetImageCaptcha(): string + public function verificationGetImageCaptcha(): ImageCaptchaVerification { - return (bool)$this->sendRequest( + return ImageCaptchaVerification::fromArray($this->sendRequest( new RpcRequest(StandardMethods::VERIFICATION_GET_IMAGE_CAPTCHA, Utilities::randomCrc32()) - )->getResponse()->getResult(); + )->getResponse()->getResult()); } /** @@ -285,14 +281,14 @@ /** * Retrieves the text captcha verification response. * - * @return string The result of the text captcha verification request. + * @return TextCaptchaVerification The result of the text captcha verification request. * @throws RpcException Thrown if the RPC request fails. */ - public function verificationGetTextCaptcha(): string + public function verificationGetTextCaptcha(): TextCaptchaVerification { - return (bool)$this->sendRequest( + return TextCaptchaVerification::fromArray($this->sendRequest( new RpcRequest(StandardMethods::VERIFICATION_GET_TEXT_CAPTCHA, Utilities::randomCrc32()) - )->getResponse()->getResult(); + )->getResponse()->getResult()); } /** @@ -314,14 +310,14 @@ /** * Retrieves the external URL for verification purposes by sending a remote procedure call request. * - * @return string The result of the verification URL request. + * @return ExternalUrlVerification The result of the verification URL request. * @throws RpcException Thrown if the RPC request fails. */ - public function verificationGetExternalUrl(): string + public function verificationGetExternalUrl(): ExternalUrlVerification { - return (bool)$this->sendRequest( + return ExternalUrlVerification::fromArray($this->sendRequest( new RpcRequest(StandardMethods::VERIFICATION_GET_EXTERNAL_URL, Utilities::randomCrc32()) - )->getResponse()->getResult(); + )->getResponse()->getResult()); } /** @@ -414,14 +410,15 @@ * Deletes the user's password settings by sending a remote procedure call request. * * @param string $password The password to be deleted. + * @param bool $hash Indicates whether to hash the password before sending the request. Defaults to true. * @return true Indicates successful deletion of the password. * @throws RpcException Thrown if the RPC request fails. */ - public function settingsDeletePassword(string $password): true + public function settingsDeletePassword(string $password, bool $hash=true): true { return (bool)$this->sendRequest( new RpcRequest(StandardMethods::SETTINGS_DELETE_PASSWORD, Utilities::randomCrc32(), [ - 'password' => $password + 'password' => $hash ? hash('sha512', $password) : $password ]) )->getResponse()->getResult(); } @@ -431,11 +428,23 @@ * * @param string $password The new password to be set. * @param string $existingPassword The current password for authentication. + * @param bool $hash * @return bool True if the password was successfully updated, false otherwise. + * @throws CryptographyException * @throws RpcException Thrown if the RPC request fails. */ - public function settingsUpdatePassword(string $password, string $existingPassword): bool + public function settingsUpdatePassword(string $password, string $existingPassword, bool $hash=true): bool { + if($hash) + { + $password = Cryptography::hashPassword($password); + $existingPassword = hash('sha512', $existingPassword); + } + elseif(!Cryptography::validatePasswordHash($password)) + { + throw new CryptographyException('Invalid password hash provided'); + } + return (bool)$this->sendRequest( new RpcRequest(StandardMethods::SETTINGS_UPDATE_PASSWORD, Utilities::randomCrc32(), [ 'password' => $password, @@ -447,14 +456,19 @@ /** * Updates the OTP setting by sending a remote procedure call request with the provided OTP. * - * @param string $otp The OTP to be set. If hashing is enabled, it will be hashed using SHA-512. + * @return string The result of the OTP URI request. * @throws RpcException Thrown if the RPC request fails. */ - public function settingsSetOtp(string $otp, bool $hash=true): true + public function settingsSetOtp(?string $password=null, bool $hash=true): string { + if($hash && $password !== null) + { + $password = hash('sha512', $password); + } + return (bool)$this->sendRequest( new RpcRequest(StandardMethods::SETTINGS_SET_OTP, Utilities::randomCrc32(), [ - 'otp' => $hash ? hash('sha512', $otp) : $otp + 'password' => $password ]) )->getResponse()->getResult(); } @@ -506,47 +520,162 @@ )->getResponse()->getResult(); } + /** + * Retrieves an information field by sending a remote procedure call request. + * + * @param InformationFieldName $field The field to be retrieved. + * @return InformationField The information field retrieved from the server. + * @throws RpcException Thrown if the RPC request fails. + */ + public function settingsGetInformationField(InformationFieldName $field): InformationField + { + return InformationField::fromArray($this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_GET_INFORMATION_FIELD, Utilities::randomCrc32(), [ + 'field' => $field->value + ]) + )->getResponse()->getResult()); + } + /** * Deletes an information field by sending a remote procedure call request. * - * @param InformationField $field The field to be deleted. + * @param InformationFieldName $field The field to be deleted. * @return bool True if the field was successfully deleted, false otherwise. * @throws RpcException Thrown if the RPC request fails. */ - public function settingsDeleteInformationField(InformationField $field): true + public function settingsDeleteInformationField(InformationFieldName $field): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_DELETE_INFORMATION_FIELD, Utilities::randomCrc32()) + new RpcRequest(StandardMethods::SETTINGS_DELETE_INFORMATION_FIELD, Utilities::randomCrc32(), [ + 'field' => $field->value + ]) )->getResponse()->getResult(); } /** * Updates an information field by sending a remote procedure call request. * - * @param InformationField $field The field to be updated. + * @param InformationFieldName $field The field to be updated. * @param string $value The value to be set. * @return bool True if the field was successfully updated, false otherwise. * @throws RpcException Thrown if the RPC request fails. */ - public function settingsUpdateInformationField(InformationField $field, string $value): true + public function settingsUpdateInformationField(InformationFieldName $field, string $value): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_UPDATE_INFORMATION_FIELD, Utilities::randomCrc32()) + new RpcRequest(StandardMethods::SETTINGS_UPDATE_INFORMATION_FIELD, Utilities::randomCrc32(), [ + 'field' => $field->value, + 'value' => $value + ]) )->getResponse()->getResult(); } /** * Updates the privacy of an information field by sending a remote procedure call request. * - * @param InformationField $field The field to be updated. + * @param InformationFieldName $field The field to be updated. * @param PrivacyState $privacy The privacy state to be set. * @return bool True if the privacy was successfully updated, false otherwise. * @throws RpcException Thrown if the RPC request fails. */ - public function settingsUpdateInformationPrivacy(InformationField $field, PrivacyState $privacy): true + public function settingsUpdateInformationPrivacy(InformationFieldName $field, PrivacyState $privacy): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_UPDATE_INFORMATION_PRIVACY, Utilities::randomCrc32()) + new RpcRequest(StandardMethods::SETTINGS_UPDATE_INFORMATION_PRIVACY, Utilities::randomCrc32(), [ + 'field' => $field->value, + 'privacy' => $privacy->value + ]) + )->getResponse()->getResult(); + } + + /** + * Adds a signing key to the server associated with the peer by sending a remote procedure call request. + * + * @param string $publicKey The public key to be added. + * @param string|null $name The name of the signing key. + * @param int|null $expires The expiration date of the signing key. + * @return string The UUID of the signing key. + * @throws RpcException Thrown if the RPC request fails. + */ + public function settingsAddSignature(string $publicKey, ?string $name=null, ?int $expires=null): string + { + return $this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_ADD_SIGNATURE, Utilities::randomCrc32(), [ + 'public_key' => $publicKey, + 'name' => $name, + 'expires' => $expires + ]) + )->getResponse()->getResult(); + } + + /** + * Creates a signing key pair by generating a new key pair and sending a remote procedure call request to add it. + * The generated key pair is returned, this is similar to settingsAddSignature but generates the key pair. + * + * @param string|null $name The name of the signing key. + * @param int|null $expires The expiration date of the signing key. + * @return string The UUID of the signing key. + * @throws CryptographyException Thrown if the key generation fails. + * @throws RpcException Thrown if the RPC request fails. + */ + public function settingsCreateSignature(?string $name=null, ?int $expires=null): string + { + $signingKeypair = Cryptography::generateSigningKeyPair(); + $uuid = $this->settingsAddSignature($signingKeypair->getPublicKey(), $name, $expires); + $signatureKeypair = new SignatureKeyPair([ + 'uuid' => $uuid, + 'name' => $name, + 'public_key' => $signingKeypair->getPublicKey(), + 'private_key' => $signingKeypair->getPrivateKey(), + 'expires' => $expires + ]); + + $this->addSigningKey($signatureKeypair); + return $uuid; + } + + /** + * Retrieves a signing key by sending a remote procedure call request. + * + * @param string $uuid The UUID of the signing key to be retrieved. + * @return SigningKey The signing key retrieved from the server. + * @throws RpcException Thrown if the RPC request fails. + */ + public function settingsGetSigningKey(string $uuid): SigningKey + { + return SigningKey::fromArray($this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_GET_SIGNATURE, Utilities::randomCrc32(), [ + 'uuid' => $uuid + ]) + )->getResponse()->getResult()); + } + + /** + * Retrieves the list of signing keys associated with the peer by sending a remote procedure call request. + * + * @return SigningKey[] The list of signing keys retrieved from the server. + * @throws RpcException Thrown if the RPC request fails. + */ + public function settingsGetSigningKeys(): array + { + return array_map(fn($key) => SigningKey::fromArray($key), $this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_GET_SIGNATURES, Utilities::randomCrc32()) + )->getResponse()->getResult()); + } + + /** + * Deletes a signing key by sending a remote procedure call request. + * + * @param string $uuid The UUID of the signing key to be deleted. + * @return bool True if the signing key was successfully deleted, false otherwise. + * @throws RpcException Thrown if the RPC request fails. + */ + public function settingsDeleteSigningKey(string $uuid): true + { + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_DELETE_SIGNATURE, Utilities::randomCrc32(), [ + 'uuid' => $uuid + ]) )->getResponse()->getResult(); } @@ -561,12 +690,14 @@ public function authenticate(): true { return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::AUTHENTICATE, Utilities::randomCrc32()) + new RpcRequest(StandardMethods::VERIFICATION_AUTHENTICATE, Utilities::randomCrc32()) )->getResponse()->getResult(); } /** - * Resolves a peer by its address or a PeerAddress instance through a remote procedure call. + * Resolves a peer by its address or a PeerAddress and returns information about the peer. Note that this is a + * decentralized method call, so passing on a peer that does not belong to the host server will result in the + * host server resolving the peer externally on its end. * * @param string|PeerAddress $peerAddress The peer address as a string or an instance of PeerAddress. * @return Peer The resolved peer object. @@ -590,4 +721,29 @@ ]), true, $identifiedAs )->getResponse()->getResult()); } + + /** + * Resolves the signing key of a peer. Note that this is a decentralized method call, so passing on a peer + * that does not belong to the host server will result in the host server resolving the key externally on + * its end. + * + * @param string|PeerAddress $peerAddress The peer address as a string or an instance of PeerAddress. + * @param string $signatureUuid The UUID of the signature to resolve. + * @return SigningKey The resolved signing key. + * @throws RpcException Thrown if the RPC request fails. + */ + public function resolvePeerSignature(string|PeerAddress $peerAddress, string $signatureUuid): SigningKey + { + if($peerAddress instanceof PeerAddress) + { + $peerAddress = $peerAddress->getAddress(); + } + + return SigningKey::fromArray($this->sendRequest( + new RpcRequest(StandardMethods::RESOLVE_PEER_SIGNATURE, Utilities::randomCrc32(), [ + 'peer' => $peerAddress, + 'uuid' => $signatureUuid + ]) + )->getResponse()->getResult()); + } } \ No newline at end of file From f76109347a8d2dae498dfc11502f3d311dbb125e Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 29 Jan 2025 15:42:19 -0500 Subject: [PATCH 270/420] Updated error codes --- src/Socialbox/Enums/StandardError.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Socialbox/Enums/StandardError.php b/src/Socialbox/Enums/StandardError.php index 3c0fbc6..804f396 100644 --- a/src/Socialbox/Enums/StandardError.php +++ b/src/Socialbox/Enums/StandardError.php @@ -13,8 +13,9 @@ enum StandardError : int case BAD_REQUEST = -102; case FORBIDDEN = -103; case UNAUTHORIZED = -104; - case RESOLUTION_FAILED = -105; - case CRYPTOGRAPHIC_ERROR = -106; + case NOT_FOUND = -105; + case RESOLUTION_FAILED = -106; + case CRYPTOGRAPHIC_ERROR = -107; // RPC Errors case RPC_METHOD_NOT_FOUND = -1000; From d6e397247ace00ab0987248ebbf06fd6a516ffbe Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 29 Jan 2025 15:42:28 -0500 Subject: [PATCH 271/420] Formatting update --- src/Socialbox/Enums/StandardError.php | 140 +++++++++++++------------- 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/src/Socialbox/Enums/StandardError.php b/src/Socialbox/Enums/StandardError.php index 804f396..8824d54 100644 --- a/src/Socialbox/Enums/StandardError.php +++ b/src/Socialbox/Enums/StandardError.php @@ -1,80 +1,80 @@ 'Unknown Error', + return match ($this) + { + default => 'Unknown Error', - self::RPC_METHOD_NOT_FOUND => 'The request method was not found', - self::RPC_INVALID_ARGUMENTS => 'The request method contains one or more invalid arguments', + self::RPC_METHOD_NOT_FOUND => 'The request method was not found', + self::RPC_INVALID_ARGUMENTS => 'The request method contains one or more invalid arguments', - self::INTERNAL_SERVER_ERROR => 'Internal server error', - self::SERVER_UNAVAILABLE => 'Server temporarily unavailable', + self::INTERNAL_SERVER_ERROR => 'Internal server error', + self::SERVER_UNAVAILABLE => 'Server temporarily unavailable', - self::INVALID_PUBLIC_KEY => 'The given public key is not valid', - self::UNSUPPORTED_AUTHENTICATION_TYPE => 'The requested authentication type is not supported by the server', - self::ALREADY_AUTHENTICATED => 'The action cannot be preformed while authenticated', - self::AUTHENTICATION_REQUIRED => 'Authentication is required to preform this action', - self::SESSION_NOT_FOUND => 'The requested session UUID was not found', - self::REGISTRATION_DISABLED => 'Registration is disabled on the server', - self::CAPTCHA_NOT_AVAILABLE => 'Captcha is not available', - self::INCORRECT_CAPTCHA_ANSWER => 'The Captcha answer is incorrect', - self::CAPTCHA_EXPIRED => 'The captcha has expired and a new captcha needs to be requested', + self::INVALID_PUBLIC_KEY => 'The given public key is not valid', + self::UNSUPPORTED_AUTHENTICATION_TYPE => 'The requested authentication type is not supported by the server', + self::ALREADY_AUTHENTICATED => 'The action cannot be preformed while authenticated', + self::AUTHENTICATION_REQUIRED => 'Authentication is required to preform this action', + self::SESSION_NOT_FOUND => 'The requested session UUID was not found', + self::REGISTRATION_DISABLED => 'Registration is disabled on the server', + self::CAPTCHA_NOT_AVAILABLE => 'Captcha is not available', + self::INCORRECT_CAPTCHA_ANSWER => 'The Captcha answer is incorrect', + self::CAPTCHA_EXPIRED => 'The captcha has expired and a new captcha needs to be requested', - self::PEER_NOT_FOUND => 'The requested peer was not found', - self::INVALID_USERNAME => 'The given username is invalid, it must be Alphanumeric with a minimum of 3 character but no greater than 255 characters', - self::USERNAME_ALREADY_EXISTS => 'The given username already exists on the network', - self::NOT_REGISTERED => 'The given username is not registered on the server', - self::METHOD_NOT_ALLOWED => 'The requested method is not allowed', - }; + self::PEER_NOT_FOUND => 'The requested peer was not found', + self::INVALID_USERNAME => 'The given username is invalid, it must be Alphanumeric with a minimum of 3 character but no greater than 255 characters', + self::USERNAME_ALREADY_EXISTS => 'The given username already exists on the network', + self::NOT_REGISTERED => 'The given username is not registered on the server', + self::METHOD_NOT_ALLOWED => 'The requested method is not allowed', + }; - } -} \ No newline at end of file + } + } \ No newline at end of file From 1e10e761dbdf4d2e5051c7fed9e38ef01f05e454 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 29 Jan 2025 15:52:38 -0500 Subject: [PATCH 272/420] Restructured StandardMethod Namespaces --- .../AddressBookAddContact.php | 2 +- .../AddressBookDeleteContact.php | 4 +- .../AddressBookGetContact.php | 2 +- .../AddressBookGetContacts.php | 2 +- .../AddressBookTrustSignature.php | 3 +- .../AddressBookUpdateRelationship.php | 2 +- .../{ => Core}/GetAllowedMethods.php | 2 +- .../{ => Core}/GetSessionState.php | 2 +- .../StandardMethods/{ => Core}/Ping.php | 2 +- .../{ => Core}/ResolvePeer.php | 6 +- .../{ => Core}/ResolvePeerSignature.php | 2 +- .../AcceptCommunityGuidelines.php | 2 +- .../AcceptPrivacyPolicy.php | 2 +- .../AcceptTermsOfService.php | 2 +- .../GetCommunityGuidelines.php | 2 +- .../GetPrivacyPolicy.php | 2 +- .../GetTermsOfService.php | 2 +- .../SettingsAddInformationField.php | 2 +- .../{ => Settings}/SettingsAddSignature.php | 2 +- .../SettingsDeleteInformationField.php | 2 +- .../{ => Settings}/SettingsDeleteOtp.php | 4 +- .../{ => Settings}/SettingsDeletePassword.php | 2 +- .../SettingsDeleteSignature.php | 3 +- .../SettingsGetInformationField.php | 2 +- .../SettingsGetInformationFields.php | 2 +- .../{ => Settings}/SettingsGetSigningKey.php | 2 +- .../{ => Settings}/SettingsGetSigningKeys.php | 2 +- .../{ => Settings}/SettingsSetOtp.php | 2 +- .../{ => Settings}/SettingsSetPassword.php | 2 +- .../SettingsUpdateInformationField.php | 2 +- .../SettingsUpdateInformationPrivacy.php | 2 +- .../{ => Settings}/SettingsUpdatePassword.php | 2 +- .../{ => Verification}/Authenticate.php | 2 +- .../VerificationAnswerImageCaptcha.php | 2 +- .../VerificationGetImageCaptcha.php | 2 +- .../VerificationOtpAuthentication.php | 2 +- .../VerificationPasswordAuthentication.php | 2 +- src/Socialbox/Enums/StandardMethods.php | 70 +++++++++---------- 38 files changed, 72 insertions(+), 82 deletions(-) rename src/Socialbox/Classes/StandardMethods/{ => AddressBook}/AddressBookAddContact.php (97%) rename src/Socialbox/Classes/StandardMethods/{ => AddressBook}/AddressBookDeleteContact.php (93%) rename src/Socialbox/Classes/StandardMethods/{ => AddressBook}/AddressBookGetContact.php (96%) rename src/Socialbox/Classes/StandardMethods/{ => AddressBook}/AddressBookGetContacts.php (97%) rename src/Socialbox/Classes/StandardMethods/{ => AddressBook}/AddressBookTrustSignature.php (95%) rename src/Socialbox/Classes/StandardMethods/{ => AddressBook}/AddressBookUpdateRelationship.php (97%) rename src/Socialbox/Classes/StandardMethods/{ => Core}/GetAllowedMethods.php (95%) rename src/Socialbox/Classes/StandardMethods/{ => Core}/GetSessionState.php (90%) rename src/Socialbox/Classes/StandardMethods/{ => Core}/Ping.php (89%) rename src/Socialbox/Classes/StandardMethods/{ => Core}/ResolvePeer.php (89%) rename src/Socialbox/Classes/StandardMethods/{ => Core}/ResolvePeerSignature.php (97%) rename src/Socialbox/Classes/StandardMethods/{ => ServerDocuments}/AcceptCommunityGuidelines.php (95%) rename src/Socialbox/Classes/StandardMethods/{ => ServerDocuments}/AcceptPrivacyPolicy.php (95%) rename src/Socialbox/Classes/StandardMethods/{ => ServerDocuments}/AcceptTermsOfService.php (95%) rename src/Socialbox/Classes/StandardMethods/{ => ServerDocuments}/GetCommunityGuidelines.php (92%) rename src/Socialbox/Classes/StandardMethods/{ => ServerDocuments}/GetPrivacyPolicy.php (92%) rename src/Socialbox/Classes/StandardMethods/{ => ServerDocuments}/GetTermsOfService.php (92%) rename src/Socialbox/Classes/StandardMethods/{ => Settings}/SettingsAddInformationField.php (99%) rename src/Socialbox/Classes/StandardMethods/{ => Settings}/SettingsAddSignature.php (97%) rename src/Socialbox/Classes/StandardMethods/{ => Settings}/SettingsDeleteInformationField.php (98%) rename src/Socialbox/Classes/StandardMethods/{ => Settings}/SettingsDeleteOtp.php (96%) rename src/Socialbox/Classes/StandardMethods/{ => Settings}/SettingsDeletePassword.php (98%) rename src/Socialbox/Classes/StandardMethods/{ => Settings}/SettingsDeleteSignature.php (94%) rename src/Socialbox/Classes/StandardMethods/{ => Settings}/SettingsGetInformationField.php (96%) rename src/Socialbox/Classes/StandardMethods/{ => Settings}/SettingsGetInformationFields.php (94%) rename src/Socialbox/Classes/StandardMethods/{ => Settings}/SettingsGetSigningKey.php (96%) rename src/Socialbox/Classes/StandardMethods/{ => Settings}/SettingsGetSigningKeys.php (95%) rename src/Socialbox/Classes/StandardMethods/{ => Settings}/SettingsSetOtp.php (98%) rename src/Socialbox/Classes/StandardMethods/{ => Settings}/SettingsSetPassword.php (97%) rename src/Socialbox/Classes/StandardMethods/{ => Settings}/SettingsUpdateInformationField.php (97%) rename src/Socialbox/Classes/StandardMethods/{ => Settings}/SettingsUpdateInformationPrivacy.php (97%) rename src/Socialbox/Classes/StandardMethods/{ => Settings}/SettingsUpdatePassword.php (98%) rename src/Socialbox/Classes/StandardMethods/{ => Verification}/Authenticate.php (95%) rename src/Socialbox/Classes/StandardMethods/{ => Verification}/VerificationAnswerImageCaptcha.php (97%) rename src/Socialbox/Classes/StandardMethods/{ => Verification}/VerificationGetImageCaptcha.php (97%) rename src/Socialbox/Classes/StandardMethods/{ => Verification}/VerificationOtpAuthentication.php (97%) rename src/Socialbox/Classes/StandardMethods/{ => Verification}/VerificationPasswordAuthentication.php (97%) diff --git a/src/Socialbox/Classes/StandardMethods/AddressBookAddContact.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php similarity index 97% rename from src/Socialbox/Classes/StandardMethods/AddressBookAddContact.php rename to src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php index ebad0d1..91558c8 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBookAddContact.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php @@ -1,6 +1,6 @@ Date: Thu, 30 Jan 2025 00:24:11 -0500 Subject: [PATCH 273/420] Added Method AddressBookContactExists --- .../AddressBook/AddressBookContactExists.php | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookContactExists.php diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookContactExists.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookContactExists.php new file mode 100644 index 0000000..c592341 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookContactExists.php @@ -0,0 +1,51 @@ +containsParameter('peer')) + { + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing required peer parameter'); + } + + try + { + $address = PeerAddress::fromAddress($rpcRequest->getParameter('peer')); + } + catch(InvalidArgumentException $e) + { + throw new StandardException('Invalid peer address', StandardError::RPC_INVALID_ARGUMENTS, $e); + } + + try + { + $peer = $request->getPeer(); + return $rpcRequest->produceResponse(ContactManager::isContact($peer, $address)); + } + catch (DatabaseOperationException $e) + { + throw new StandardException('Failed to check if the contact exists', StandardError::INTERNAL_SERVER_ERROR, $e); + } + } + } \ No newline at end of file From b8ef387bb2a5cdbd6479a26a98786824fc7dd422 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 30 Jan 2025 00:26:01 -0500 Subject: [PATCH 274/420] Added PhpDoc --- .../StandardMethods/AddressBook/AddressBookAddContact.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php index 91558c8..ccfcbe0 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php @@ -18,6 +18,8 @@ class AddressBookAddContact extends Method { /** + * Adds a contact to the authenticated peer's address book + * * @inheritDoc */ public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface From efc9905200f0296b87c3cb522173eecdfac3dd2f Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 30 Jan 2025 00:27:06 -0500 Subject: [PATCH 275/420] Moved StandardException to it's own namespace --- src/Socialbox/Abstracts/Method.php | 2 +- .../StandardMethods/AddressBook/AddressBookAddContact.php | 2 +- .../StandardMethods/AddressBook/AddressBookContactExists.php | 4 +--- .../StandardMethods/AddressBook/AddressBookDeleteContact.php | 4 ++-- .../StandardMethods/AddressBook/AddressBookGetContact.php | 2 +- .../StandardMethods/AddressBook/AddressBookGetContacts.php | 2 +- .../AddressBook/AddressBookTrustSignature.php | 2 +- .../AddressBook/AddressBookUpdateRelationship.php | 2 +- .../Classes/StandardMethods/Core/GetAllowedMethods.php | 5 +++-- .../Classes/StandardMethods/Core/GetSessionState.php | 1 - src/Socialbox/Classes/StandardMethods/Core/ResolvePeer.php | 2 +- .../Classes/StandardMethods/Core/ResolvePeerSignature.php | 2 +- .../StandardMethods/Settings/SettingsAddInformationField.php | 2 +- .../StandardMethods/Settings/SettingsAddSignature.php | 2 +- .../Settings/SettingsDeleteInformationField.php | 2 +- .../Classes/StandardMethods/Settings/SettingsDeleteOtp.php | 2 +- .../StandardMethods/Settings/SettingsDeletePassword.php | 2 +- .../StandardMethods/Settings/SettingsDeleteSignature.php | 2 +- .../StandardMethods/Settings/SettingsGetInformationField.php | 2 +- .../Settings/SettingsGetInformationFields.php | 2 +- .../StandardMethods/Settings/SettingsGetSigningKey.php | 2 +- .../StandardMethods/Settings/SettingsGetSigningKeys.php | 2 +- .../Classes/StandardMethods/Settings/SettingsSetOtp.php | 2 +- .../Classes/StandardMethods/Settings/SettingsSetPassword.php | 2 +- .../Settings/SettingsUpdateInformationField.php | 2 +- .../Settings/SettingsUpdateInformationPrivacy.php | 2 +- .../StandardMethods/Settings/SettingsUpdatePassword.php | 2 +- .../Classes/StandardMethods/Verification/Authenticate.php | 2 +- .../Verification/VerificationAnswerImageCaptcha.php | 2 +- .../Verification/VerificationGetImageCaptcha.php | 2 +- .../Verification/VerificationOtpAuthentication.php | 2 +- .../Verification/VerificationPasswordAuthentication.php | 2 +- src/Socialbox/Enums/StandardMethods.php | 2 +- .../Exceptions/{ => Standard}/StandardException.php | 2 +- src/Socialbox/Managers/SessionManager.php | 2 +- src/Socialbox/Objects/RpcRequest.php | 2 +- src/Socialbox/Socialbox.php | 2 +- 37 files changed, 39 insertions(+), 41 deletions(-) rename src/Socialbox/Exceptions/{ => Standard}/StandardException.php (96%) diff --git a/src/Socialbox/Abstracts/Method.php b/src/Socialbox/Abstracts/Method.php index 39d9012..8214bf5 100644 --- a/src/Socialbox/Abstracts/Method.php +++ b/src/Socialbox/Abstracts/Method.php @@ -2,7 +2,7 @@ namespace Socialbox\Abstracts; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\ClientRequest; use Socialbox\Objects\RpcRequest; diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php index ccfcbe0..e331352 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php @@ -7,7 +7,7 @@ use Socialbox\Enums\StandardError; use Socialbox\Enums\Types\ContactRelationshipType; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\ContactManager; use Socialbox\Objects\ClientRequest; diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookContactExists.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookContactExists.php index c592341..7fd13fa 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookContactExists.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookContactExists.php @@ -5,15 +5,13 @@ use InvalidArgumentException; use Socialbox\Abstracts\Method; use Socialbox\Enums\StandardError; - use Socialbox\Enums\Types\ContactRelationshipType; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\ContactManager; use Socialbox\Objects\ClientRequest; use Socialbox\Objects\PeerAddress; use Socialbox\Objects\RpcRequest; - use Socialbox\Socialbox; class AddressBookContactExists extends Method { diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookDeleteContact.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookDeleteContact.php index 0490b40..3f3ccf8 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookDeleteContact.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookDeleteContact.php @@ -6,7 +6,7 @@ use Socialbox\Abstracts\Method; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\ContactManager; use Socialbox\Objects\ClientRequest; @@ -22,7 +22,7 @@ { if(!$rpcRequest->containsParameter('peer')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing required \'peer\' parameter'); + return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing required peer parameter'); } try diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContact.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContact.php index 4da647f..00f61db 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContact.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContact.php @@ -6,7 +6,7 @@ use Socialbox\Abstracts\Method; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\ContactManager; use Socialbox\Objects\ClientRequest; diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContacts.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContacts.php index 135b5ab..8033a84 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContacts.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContacts.php @@ -6,7 +6,7 @@ use Socialbox\Classes\Configuration; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\ContactManager; use Socialbox\Objects\ClientRequest; diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookTrustSignature.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookTrustSignature.php index 3db9592..ca22925 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookTrustSignature.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookTrustSignature.php @@ -6,7 +6,7 @@ use Socialbox\Abstracts\Method; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\ContactManager; use Socialbox\Objects\ClientRequest; diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookUpdateRelationship.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookUpdateRelationship.php index 70197bf..78bd4b5 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookUpdateRelationship.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookUpdateRelationship.php @@ -7,7 +7,7 @@ use Socialbox\Enums\StandardError; use Socialbox\Enums\Types\ContactRelationshipType; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\ContactManager; use Socialbox\Objects\ClientRequest; diff --git a/src/Socialbox/Classes/StandardMethods/Core/GetAllowedMethods.php b/src/Socialbox/Classes/StandardMethods/Core/GetAllowedMethods.php index 7babe2a..338dc8b 100644 --- a/src/Socialbox/Classes/StandardMethods/Core/GetAllowedMethods.php +++ b/src/Socialbox/Classes/StandardMethods/Core/GetAllowedMethods.php @@ -6,15 +6,16 @@ use Socialbox\Enums\StandardError; use Socialbox\Enums\StandardMethods; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\ClientRequest; use Socialbox\Objects\RpcRequest; class GetAllowedMethods extends Method { - /** + * Returns a list of allowed methods for the current session. + * * @inheritDoc */ public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface diff --git a/src/Socialbox/Classes/StandardMethods/Core/GetSessionState.php b/src/Socialbox/Classes/StandardMethods/Core/GetSessionState.php index 0d70b53..b2ed25f 100644 --- a/src/Socialbox/Classes/StandardMethods/Core/GetSessionState.php +++ b/src/Socialbox/Classes/StandardMethods/Core/GetSessionState.php @@ -9,7 +9,6 @@ class GetSessionState extends Method { - /** * @inheritDoc */ diff --git a/src/Socialbox/Classes/StandardMethods/Core/ResolvePeer.php b/src/Socialbox/Classes/StandardMethods/Core/ResolvePeer.php index e0a6fc4..f410772 100644 --- a/src/Socialbox/Classes/StandardMethods/Core/ResolvePeer.php +++ b/src/Socialbox/Classes/StandardMethods/Core/ResolvePeer.php @@ -7,7 +7,7 @@ use Socialbox\Abstracts\Method; use Socialbox\Enums\ReservedUsernames; use Socialbox\Enums\StandardError; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\ClientRequest; use Socialbox\Objects\PeerAddress; diff --git a/src/Socialbox/Classes/StandardMethods/Core/ResolvePeerSignature.php b/src/Socialbox/Classes/StandardMethods/Core/ResolvePeerSignature.php index 56384bf..59ab939 100644 --- a/src/Socialbox/Classes/StandardMethods/Core/ResolvePeerSignature.php +++ b/src/Socialbox/Classes/StandardMethods/Core/ResolvePeerSignature.php @@ -6,7 +6,7 @@ use InvalidArgumentException; use Socialbox\Abstracts\Method; use Socialbox\Enums\StandardError; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\ClientRequest; use Socialbox\Objects\PeerAddress; diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddInformationField.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddInformationField.php index 909b996..8fa6ce0 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddInformationField.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddInformationField.php @@ -9,7 +9,7 @@ use Socialbox\Enums\StandardError; use Socialbox\Enums\Types\InformationFieldName; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PeerInformationManager; use Socialbox\Managers\SessionManager; diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddSignature.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddSignature.php index c731825..cdf184f 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddSignature.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddSignature.php @@ -7,7 +7,7 @@ use Socialbox\Abstracts\Method; use Socialbox\Classes\Configuration; use Socialbox\Enums\StandardError; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\SigningKeysManager; use Socialbox\Objects\ClientRequest; diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteInformationField.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteInformationField.php index 91f5a26..6fbbb8c 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteInformationField.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteInformationField.php @@ -7,7 +7,7 @@ use Socialbox\Enums\StandardError; use Socialbox\Enums\Types\InformationFieldName; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PeerInformationManager; use Socialbox\Objects\ClientRequest; diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteOtp.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteOtp.php index 9668547..2a8fe02 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteOtp.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteOtp.php @@ -8,7 +8,7 @@ use Socialbox\Classes\Cryptography; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\OneTimePasswordManager; use Socialbox\Managers\PasswordManager; diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeletePassword.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeletePassword.php index a771879..cb02dd3 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeletePassword.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeletePassword.php @@ -8,7 +8,7 @@ use Socialbox\Classes\Cryptography; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\CryptographyException; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PasswordManager; use Socialbox\Objects\ClientRequest; diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteSignature.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteSignature.php index 8a2a1ab..856db73 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteSignature.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteSignature.php @@ -7,7 +7,7 @@ use ncc\ThirdParty\Symfony\Uid\Uuid; use Socialbox\Abstracts\Method; use Socialbox\Enums\StandardError; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\SigningKeysManager; use Socialbox\Objects\ClientRequest; diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetInformationField.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetInformationField.php index ae63224..2333d71 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetInformationField.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetInformationField.php @@ -6,7 +6,7 @@ use Socialbox\Enums\StandardError; use Socialbox\Enums\Types\InformationFieldName; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PeerInformationManager; use Socialbox\Objects\ClientRequest; diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetInformationFields.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetInformationFields.php index 375ba5f..a702193 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetInformationFields.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetInformationFields.php @@ -5,7 +5,7 @@ use Socialbox\Abstracts\Method; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PeerInformationManager; use Socialbox\Objects\ClientRequest; diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSigningKey.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSigningKey.php index 358ccad..01ddfd0 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSigningKey.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSigningKey.php @@ -5,7 +5,7 @@ use Socialbox\Abstracts\Method; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\SigningKeysManager; use Socialbox\Objects\ClientRequest; diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSigningKeys.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSigningKeys.php index 57e755b..e4b729c 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSigningKeys.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSigningKeys.php @@ -5,7 +5,7 @@ use Socialbox\Abstracts\Method; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\SigningKeysManager; use Socialbox\Objects\ClientRequest; diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsSetOtp.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsSetOtp.php index 7da0714..7ecc46c 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsSetOtp.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsSetOtp.php @@ -8,7 +8,7 @@ use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\OneTimePasswordManager; use Socialbox\Managers\PasswordManager; diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsSetPassword.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsSetPassword.php index 21c4b10..4a6000d 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsSetPassword.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsSetPassword.php @@ -8,7 +8,7 @@ use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PasswordManager; use Socialbox\Managers\SessionManager; diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdateInformationField.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdateInformationField.php index 3b3de31..6e14a3c 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdateInformationField.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdateInformationField.php @@ -6,7 +6,7 @@ use Socialbox\Enums\StandardError; use Socialbox\Enums\Types\InformationFieldName; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PeerInformationManager; use Socialbox\Objects\ClientRequest; diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdateInformationPrivacy.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdateInformationPrivacy.php index 5e80cda..ce08b2a 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdateInformationPrivacy.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdateInformationPrivacy.php @@ -7,7 +7,7 @@ use Socialbox\Enums\StandardError; use Socialbox\Enums\Types\InformationFieldName; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PeerInformationManager; use Socialbox\Objects\ClientRequest; diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdatePassword.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdatePassword.php index 4668144..e098769 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdatePassword.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdatePassword.php @@ -7,7 +7,7 @@ use Socialbox\Classes\Cryptography; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PasswordManager; use Socialbox\Objects\ClientRequest; diff --git a/src/Socialbox/Classes/StandardMethods/Verification/Authenticate.php b/src/Socialbox/Classes/StandardMethods/Verification/Authenticate.php index 2fb452a..15dce3f 100644 --- a/src/Socialbox/Classes/StandardMethods/Verification/Authenticate.php +++ b/src/Socialbox/Classes/StandardMethods/Verification/Authenticate.php @@ -6,7 +6,7 @@ use Socialbox\Abstracts\Method; use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\SessionManager; use Socialbox\Objects\ClientRequest; diff --git a/src/Socialbox/Classes/StandardMethods/Verification/VerificationAnswerImageCaptcha.php b/src/Socialbox/Classes/StandardMethods/Verification/VerificationAnswerImageCaptcha.php index 27c0425..1c5c922 100644 --- a/src/Socialbox/Classes/StandardMethods/Verification/VerificationAnswerImageCaptcha.php +++ b/src/Socialbox/Classes/StandardMethods/Verification/VerificationAnswerImageCaptcha.php @@ -6,7 +6,7 @@ use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\CaptchaManager; use Socialbox\Managers\SessionManager; diff --git a/src/Socialbox/Classes/StandardMethods/Verification/VerificationGetImageCaptcha.php b/src/Socialbox/Classes/StandardMethods/Verification/VerificationGetImageCaptcha.php index e0debbf..ddc4459 100644 --- a/src/Socialbox/Classes/StandardMethods/Verification/VerificationGetImageCaptcha.php +++ b/src/Socialbox/Classes/StandardMethods/Verification/VerificationGetImageCaptcha.php @@ -7,7 +7,7 @@ use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\CaptchaManager; use Socialbox\Objects\ClientRequest; diff --git a/src/Socialbox/Classes/StandardMethods/Verification/VerificationOtpAuthentication.php b/src/Socialbox/Classes/StandardMethods/Verification/VerificationOtpAuthentication.php index af61e65..b85dc6d 100644 --- a/src/Socialbox/Classes/StandardMethods/Verification/VerificationOtpAuthentication.php +++ b/src/Socialbox/Classes/StandardMethods/Verification/VerificationOtpAuthentication.php @@ -8,7 +8,7 @@ use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\CryptographyException; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\OneTimePasswordManager; use Socialbox\Managers\SessionManager; diff --git a/src/Socialbox/Classes/StandardMethods/Verification/VerificationPasswordAuthentication.php b/src/Socialbox/Classes/StandardMethods/Verification/VerificationPasswordAuthentication.php index f1d56c4..9b1f321 100644 --- a/src/Socialbox/Classes/StandardMethods/Verification/VerificationPasswordAuthentication.php +++ b/src/Socialbox/Classes/StandardMethods/Verification/VerificationPasswordAuthentication.php @@ -8,7 +8,7 @@ use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\CryptographyException; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PasswordManager; use Socialbox\Managers\SessionManager; diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index 11f5f2a..d62221a 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -40,7 +40,7 @@ use Socialbox\Classes\StandardMethods\Verification\VerificationPasswordAuthentication; use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\StandardException; + use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\OneTimePasswordManager; use Socialbox\Managers\PasswordManager; diff --git a/src/Socialbox/Exceptions/StandardException.php b/src/Socialbox/Exceptions/Standard/StandardException.php similarity index 96% rename from src/Socialbox/Exceptions/StandardException.php rename to src/Socialbox/Exceptions/Standard/StandardException.php index 8028440..8cd7382 100644 --- a/src/Socialbox/Exceptions/StandardException.php +++ b/src/Socialbox/Exceptions/Standard/StandardException.php @@ -1,6 +1,6 @@ Date: Thu, 30 Jan 2025 00:28:33 -0500 Subject: [PATCH 276/420] Added MissingArgumentException that extends StandardException --- .../Standard/MissingArgumentException.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/Socialbox/Exceptions/Standard/MissingArgumentException.php diff --git a/src/Socialbox/Exceptions/Standard/MissingArgumentException.php b/src/Socialbox/Exceptions/Standard/MissingArgumentException.php new file mode 100644 index 0000000..2552c85 --- /dev/null +++ b/src/Socialbox/Exceptions/Standard/MissingArgumentException.php @@ -0,0 +1,18 @@ + Date: Thu, 30 Jan 2025 00:30:25 -0500 Subject: [PATCH 277/420] Added InvalidRpcArgumentException which extends StandardException & Renamed MissingArgumentException to MissingRpcArgumentException for clarification and to avoid conflict with existing \InvalidArgumentException (base php) --- .../Standard/InvalidRpcArgumentException.php | 19 +++++++++++++++++++ ...on.php => MissingRpcArgumentException.php} | 4 ++-- 2 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 src/Socialbox/Exceptions/Standard/InvalidRpcArgumentException.php rename src/Socialbox/Exceptions/Standard/{MissingArgumentException.php => MissingRpcArgumentException.php} (74%) diff --git a/src/Socialbox/Exceptions/Standard/InvalidRpcArgumentException.php b/src/Socialbox/Exceptions/Standard/InvalidRpcArgumentException.php new file mode 100644 index 0000000..fb1abd3 --- /dev/null +++ b/src/Socialbox/Exceptions/Standard/InvalidRpcArgumentException.php @@ -0,0 +1,19 @@ + Date: Thu, 30 Jan 2025 00:31:41 -0500 Subject: [PATCH 278/420] Updated message formatting in exception --- .../Exceptions/Standard/InvalidRpcArgumentException.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Socialbox/Exceptions/Standard/InvalidRpcArgumentException.php b/src/Socialbox/Exceptions/Standard/InvalidRpcArgumentException.php index fb1abd3..fe167a9 100644 --- a/src/Socialbox/Exceptions/Standard/InvalidRpcArgumentException.php +++ b/src/Socialbox/Exceptions/Standard/InvalidRpcArgumentException.php @@ -14,6 +14,6 @@ */ public function __construct(string $parameterName, string $reason) { - parent::__construct(sprintf('The parameter %s is invalid: %s', $parameterName, $reason), StandardError::RPC_INVALID_ARGUMENTS); + parent::__construct(sprintf('Invalid parameter %s: %s', $parameterName, $reason), StandardError::RPC_INVALID_ARGUMENTS); } } \ No newline at end of file From 06b55a65b6f73d472575d684c1e78296aedd46a9 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 30 Jan 2025 00:32:41 -0500 Subject: [PATCH 279/420] Updated exception handling in AddressBookAddContact --- .../AddressBook/AddressBookAddContact.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php index e331352..30538d2 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php @@ -7,6 +7,8 @@ use Socialbox\Enums\StandardError; use Socialbox\Enums\Types\ContactRelationshipType; use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; + use Socialbox\Exceptions\Standard\MissingRpcArgumentException; use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\ContactManager; @@ -18,7 +20,7 @@ class AddressBookAddContact extends Method { /** - * Adds a contact to the authenticated peer's address book + * Adds a contact to the authenticated peer's address book * * @inheritDoc */ @@ -26,7 +28,7 @@ { if(!$rpcRequest->containsParameter('peer')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing required peer parameter'); + throw new MissingRpcArgumentException('peer'); } try @@ -35,7 +37,7 @@ } catch(InvalidArgumentException $e) { - throw new StandardException('Invalid peer address', StandardError::RPC_INVALID_ARGUMENTS, $e); + throw new InvalidRpcArgumentException('peer', $e->getMessage()); } if($rpcRequest->containsParameter('relationship')) @@ -43,7 +45,7 @@ $relationship = ContactRelationshipType::tryFrom(strtoupper($rpcRequest->getParameter('relationship'))); if($relationship === null) { - throw new StandardException('Invalid relationship type', StandardError::RPC_INVALID_ARGUMENTS); + throw new InvalidRpcArgumentException('peer', 'Invalid relationship type'); } } else From 831f6cc8e2f6955055ed00fb1e44c867cb908ddd Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 30 Jan 2025 00:34:05 -0500 Subject: [PATCH 280/420] Updated logic --- .../StandardMethods/AddressBook/AddressBookAddContact.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php index 30538d2..3bf7671 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php @@ -20,7 +20,8 @@ class AddressBookAddContact extends Method { /** - * Adds a contact to the authenticated peer's address book + * Adds a contact to the authenticated peer's address book, returns True if the contact was added + * false if the contact already exists. * * @inheritDoc */ @@ -67,7 +68,7 @@ // Check if the contact already exists if(ContactManager::isContact($peer, $address)) { - return $rpcRequest->produceError(StandardError::FORBIDDEN, 'Contact already exists'); + return $rpcRequest->produceResponse(false); } // Create the contact From 6e4c7bcc044dedb9f90d0d893a85cc1188bbce9e Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 30 Jan 2025 00:34:22 -0500 Subject: [PATCH 281/420] Updated exception handling in AddressBookContactExists --- .../AddressBook/AddressBookContactExists.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookContactExists.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookContactExists.php index 7fd13fa..c12efd0 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookContactExists.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookContactExists.php @@ -6,6 +6,8 @@ use Socialbox\Abstracts\Method; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; + use Socialbox\Exceptions\Standard\MissingRpcArgumentException; use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\ContactManager; @@ -24,7 +26,7 @@ { if(!$rpcRequest->containsParameter('peer')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing required peer parameter'); + throw new MissingRpcArgumentException('peer'); } try @@ -33,7 +35,7 @@ } catch(InvalidArgumentException $e) { - throw new StandardException('Invalid peer address', StandardError::RPC_INVALID_ARGUMENTS, $e); + throw new InvalidRpcArgumentException('peer', $e->getMessage()); } try From 4856dacb9d32a09460c4fbd800c47b36853f5d73 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 30 Jan 2025 00:35:19 -0500 Subject: [PATCH 282/420] Updated exception handling and logic in AddressBookDeleteContact --- .../AddressBook/AddressBookDeleteContact.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookDeleteContact.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookDeleteContact.php index 3f3ccf8..2ab7463 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookDeleteContact.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookDeleteContact.php @@ -6,6 +6,8 @@ use Socialbox\Abstracts\Method; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; + use Socialbox\Exceptions\Standard\MissingRpcArgumentException; use Socialbox\Exceptions\Standard\StandardException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\ContactManager; @@ -16,13 +18,16 @@ class AddressBookDeleteContact extends Method { /** + * Deletes a contact from the authenticated peer's address book, returns True if the contact was deleted + * false if the contact does not exist. + * * @inheritDoc */ public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { if(!$rpcRequest->containsParameter('peer')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing required peer parameter'); + throw new MissingRpcArgumentException('peer'); } try @@ -31,7 +36,7 @@ } catch(InvalidArgumentException $e) { - throw new StandardException('Invalid peer address', StandardError::RPC_INVALID_ARGUMENTS, $e); + throw new InvalidRpcArgumentException('peer', $e->getMessage()); } try @@ -40,7 +45,7 @@ $peer = $request->getPeer(); if(!ContactManager::isContact($peer, $address)) { - return $rpcRequest->produceError(StandardError::FORBIDDEN, 'Contact does not exist'); + return $rpcRequest->produceResponse(false); } // Create the contact From b8346e139cec4e38ead686814b9518379bf2d135 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 30 Jan 2025 00:37:37 -0500 Subject: [PATCH 283/420] Renamed StandardException to StandardRpcException --- src/Socialbox/Abstracts/Method.php | 4 +- .../AddressBook/AddressBookAddContact.php | 4 +- .../AddressBook/AddressBookContactExists.php | 4 +- .../AddressBook/AddressBookDeleteContact.php | 4 +- .../AddressBook/AddressBookGetContact.php | 6 +-- .../AddressBook/AddressBookGetContacts.php | 4 +- .../AddressBook/AddressBookTrustSignature.php | 8 +-- .../AddressBookUpdateRelationship.php | 8 +-- .../Core/GetAllowedMethods.php | 4 +- .../StandardMethods/Core/ResolvePeer.php | 6 +-- .../Core/ResolvePeerSignature.php | 10 ++-- .../Settings/SettingsAddInformationField.php | 12 ++--- .../Settings/SettingsAddSignature.php | 4 +- .../SettingsDeleteInformationField.php | 6 +-- .../Settings/SettingsDeleteOtp.php | 10 ++-- .../Settings/SettingsDeletePassword.php | 4 +- .../Settings/SettingsDeleteSignature.php | 4 +- .../Settings/SettingsGetInformationField.php | 4 +- .../Settings/SettingsGetInformationFields.php | 4 +- .../Settings/SettingsGetSigningKey.php | 4 +- .../Settings/SettingsGetSigningKeys.php | 4 +- .../Settings/SettingsSetOtp.php | 10 ++-- .../Settings/SettingsSetPassword.php | 6 +-- .../SettingsUpdateInformationField.php | 4 +- .../SettingsUpdateInformationPrivacy.php | 4 +- .../Settings/SettingsUpdatePassword.php | 8 +-- .../Verification/Authenticate.php | 4 +- .../VerificationAnswerImageCaptcha.php | 6 +-- .../VerificationGetImageCaptcha.php | 4 +- .../VerificationOtpAuthentication.php | 4 +- .../VerificationPasswordAuthentication.php | 4 +- src/Socialbox/Enums/StandardMethods.php | 8 +-- .../Standard/InvalidRpcArgumentException.php | 2 +- .../Standard/MissingRpcArgumentException.php | 2 +- ...Exception.php => StandardRpcException.php} | 2 +- src/Socialbox/Managers/SessionManager.php | 16 +++--- src/Socialbox/Objects/RpcRequest.php | 6 +-- src/Socialbox/Socialbox.php | 50 +++++++++---------- 38 files changed, 129 insertions(+), 129 deletions(-) rename src/Socialbox/Exceptions/Standard/{StandardException.php => StandardRpcException.php} (96%) diff --git a/src/Socialbox/Abstracts/Method.php b/src/Socialbox/Abstracts/Method.php index 8214bf5..748f076 100644 --- a/src/Socialbox/Abstracts/Method.php +++ b/src/Socialbox/Abstracts/Method.php @@ -2,7 +2,7 @@ namespace Socialbox\Abstracts; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\ClientRequest; use Socialbox\Objects\RpcRequest; @@ -15,7 +15,7 @@ * @param ClientRequest $request The full client request object, used to identify the client & it's requests * @param RpcRequest $rpcRequest The selected RPC request for the method to handle * @return SerializableInterface|null Returns RpcResponse/RpcError on success, null if the request is a notification - * @throws StandardException If a standard exception is thrown, it will be handled by the engine. + * @throws StandardRpcException If a standard exception is thrown, it will be handled by the engine. */ public static abstract function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface; } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php index 3bf7671..2f4a88e 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php @@ -9,7 +9,7 @@ use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; use Socialbox\Exceptions\Standard\MissingRpcArgumentException; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\ContactManager; use Socialbox\Objects\ClientRequest; @@ -76,7 +76,7 @@ } catch (DatabaseOperationException $e) { - throw new StandardException('Failed to add contact', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to add contact', StandardError::INTERNAL_SERVER_ERROR, $e); } // Return success diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookContactExists.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookContactExists.php index c12efd0..087baa2 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookContactExists.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookContactExists.php @@ -8,7 +8,7 @@ use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; use Socialbox\Exceptions\Standard\MissingRpcArgumentException; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\ContactManager; use Socialbox\Objects\ClientRequest; @@ -45,7 +45,7 @@ } catch (DatabaseOperationException $e) { - throw new StandardException('Failed to check if the contact exists', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to check if the contact exists', StandardError::INTERNAL_SERVER_ERROR, $e); } } } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookDeleteContact.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookDeleteContact.php index 2ab7463..1f27565 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookDeleteContact.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookDeleteContact.php @@ -8,7 +8,7 @@ use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; use Socialbox\Exceptions\Standard\MissingRpcArgumentException; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\ContactManager; use Socialbox\Objects\ClientRequest; @@ -53,7 +53,7 @@ } catch (DatabaseOperationException $e) { - throw new StandardException('Failed to remove contact', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to remove contact', StandardError::INTERNAL_SERVER_ERROR, $e); } // Return success diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContact.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContact.php index 00f61db..201f9f7 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContact.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContact.php @@ -6,7 +6,7 @@ use Socialbox\Abstracts\Method; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\ContactManager; use Socialbox\Objects\ClientRequest; @@ -31,7 +31,7 @@ } catch(InvalidArgumentException $e) { - throw new StandardException('Invalid peer address', StandardError::RPC_INVALID_ARGUMENTS, $e); + throw new StandardRpcException('Invalid peer address', StandardError::RPC_INVALID_ARGUMENTS, $e); } try @@ -45,7 +45,7 @@ } catch(DatabaseOperationException $e) { - throw new StandardException('Failed to get contact', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to get contact', StandardError::INTERNAL_SERVER_ERROR, $e); } return $rpcRequest->produceResponse($contact->toStandard()); diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContacts.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContacts.php index 8033a84..5f9775e 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContacts.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContacts.php @@ -6,7 +6,7 @@ use Socialbox\Classes\Configuration; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\ContactManager; use Socialbox\Objects\ClientRequest; @@ -49,7 +49,7 @@ } catch(DatabaseOperationException $e) { - throw new StandardException('Failed to get contacts', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to get contacts', StandardError::INTERNAL_SERVER_ERROR, $e); } diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookTrustSignature.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookTrustSignature.php index ca22925..1c6ce68 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookTrustSignature.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookTrustSignature.php @@ -6,7 +6,7 @@ use Socialbox\Abstracts\Method; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\ContactManager; use Socialbox\Objects\ClientRequest; @@ -32,7 +32,7 @@ } catch(InvalidArgumentException $e) { - throw new StandardException('Invalid peer address', StandardError::RPC_INVALID_ARGUMENTS, $e); + throw new StandardRpcException('Invalid peer address', StandardError::RPC_INVALID_ARGUMENTS, $e); } if(!$rpcRequest->containsParameter('uuid')) @@ -46,7 +46,7 @@ } catch(InvalidArgumentException $e) { - throw new StandardException('Invalid UUID', StandardError::RPC_INVALID_ARGUMENTS, $e); + throw new StandardRpcException('Invalid UUID', StandardError::RPC_INVALID_ARGUMENTS, $e); } try @@ -63,7 +63,7 @@ } catch (DatabaseOperationException $e) { - throw new StandardException('Failed to update contact relationship', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to update contact relationship', StandardError::INTERNAL_SERVER_ERROR, $e); } // Return success diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookUpdateRelationship.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookUpdateRelationship.php index 78bd4b5..e30669a 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookUpdateRelationship.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookUpdateRelationship.php @@ -7,7 +7,7 @@ use Socialbox\Enums\StandardError; use Socialbox\Enums\Types\ContactRelationshipType; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\ContactManager; use Socialbox\Objects\ClientRequest; @@ -32,7 +32,7 @@ } catch(InvalidArgumentException $e) { - throw new StandardException('Invalid peer address', StandardError::RPC_INVALID_ARGUMENTS, $e); + throw new StandardRpcException('Invalid peer address', StandardError::RPC_INVALID_ARGUMENTS, $e); } if(!$rpcRequest->containsParameter('relationship')) @@ -42,7 +42,7 @@ $relationship = ContactRelationshipType::tryFrom(strtoupper($rpcRequest->getParameter('relationship'))); if($relationship === null) { - throw new StandardException('Invalid relationship type', StandardError::RPC_INVALID_ARGUMENTS); + throw new StandardRpcException('Invalid relationship type', StandardError::RPC_INVALID_ARGUMENTS); } try @@ -59,7 +59,7 @@ } catch (DatabaseOperationException $e) { - throw new StandardException('Failed to update contact relationship', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to update contact relationship', StandardError::INTERNAL_SERVER_ERROR, $e); } // Return success diff --git a/src/Socialbox/Classes/StandardMethods/Core/GetAllowedMethods.php b/src/Socialbox/Classes/StandardMethods/Core/GetAllowedMethods.php index 338dc8b..49ca7c1 100644 --- a/src/Socialbox/Classes/StandardMethods/Core/GetAllowedMethods.php +++ b/src/Socialbox/Classes/StandardMethods/Core/GetAllowedMethods.php @@ -6,7 +6,7 @@ use Socialbox\Enums\StandardError; use Socialbox\Enums\StandardMethods; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\ClientRequest; use Socialbox\Objects\RpcRequest; @@ -31,7 +31,7 @@ } catch(DatabaseOperationException $e) { - throw new StandardException('Failed to retrieve allowed methods due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to retrieve allowed methods due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } return $rpcRequest->produceResponse($allowedMethods); diff --git a/src/Socialbox/Classes/StandardMethods/Core/ResolvePeer.php b/src/Socialbox/Classes/StandardMethods/Core/ResolvePeer.php index f410772..a13e0f8 100644 --- a/src/Socialbox/Classes/StandardMethods/Core/ResolvePeer.php +++ b/src/Socialbox/Classes/StandardMethods/Core/ResolvePeer.php @@ -7,7 +7,7 @@ use Socialbox\Abstracts\Method; use Socialbox\Enums\ReservedUsernames; use Socialbox\Enums\StandardError; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\ClientRequest; use Socialbox\Objects\PeerAddress; @@ -35,7 +35,7 @@ } catch(InvalidArgumentException $e) { - throw new StandardException('Peer Address Error: ' . $e->getMessage(), StandardError::RPC_INVALID_ARGUMENTS, $e); + throw new StandardRpcException('Peer Address Error: ' . $e->getMessage(), StandardError::RPC_INVALID_ARGUMENTS, $e); } // Check if host is making the request & the identifier is not empty @@ -52,7 +52,7 @@ } catch(Exception $e) { - throw new StandardException(sprintf('There was an error while trying to resolve the peer %s: %s', $peerAddress, $e->getMessage()), StandardError::RESOLUTION_FAILED, $e); + throw new StandardRpcException(sprintf('There was an error while trying to resolve the peer %s: %s', $peerAddress, $e->getMessage()), StandardError::RESOLUTION_FAILED, $e); } } } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/Core/ResolvePeerSignature.php b/src/Socialbox/Classes/StandardMethods/Core/ResolvePeerSignature.php index 59ab939..10b483a 100644 --- a/src/Socialbox/Classes/StandardMethods/Core/ResolvePeerSignature.php +++ b/src/Socialbox/Classes/StandardMethods/Core/ResolvePeerSignature.php @@ -6,7 +6,7 @@ use InvalidArgumentException; use Socialbox\Abstracts\Method; use Socialbox\Enums\StandardError; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\ClientRequest; use Socialbox\Objects\PeerAddress; @@ -39,7 +39,7 @@ } catch(InvalidArgumentException $e) { - throw new StandardException('Invalid UUID', StandardError::RPC_INVALID_ARGUMENTS, $e); + throw new StandardRpcException('Invalid UUID', StandardError::RPC_INVALID_ARGUMENTS, $e); } // Parse the peer address @@ -49,20 +49,20 @@ } catch(InvalidArgumentException $e) { - throw new StandardException('Peer Address Error: ' . $e->getMessage(), StandardError::RPC_INVALID_ARGUMENTS, $e); + throw new StandardRpcException('Peer Address Error: ' . $e->getMessage(), StandardError::RPC_INVALID_ARGUMENTS, $e); } try { return $rpcRequest->produceResponse(Socialbox::resolvePeerSignature($peerAddress, $uuid->toRfc4122())); } - catch(StandardException $e) + catch(StandardRpcException $e) { throw $e; } catch (Exception $e) { - throw new StandardException('Failed to resolve peer signature', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to resolve peer signature', StandardError::INTERNAL_SERVER_ERROR, $e); } } } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddInformationField.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddInformationField.php index 8fa6ce0..88145ca 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddInformationField.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddInformationField.php @@ -9,7 +9,7 @@ use Socialbox\Enums\StandardError; use Socialbox\Enums\Types\InformationFieldName; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PeerInformationManager; use Socialbox\Managers\SessionManager; @@ -62,7 +62,7 @@ } catch (DatabaseOperationException $e) { - throw new StandardException('Failed to retrieve current peer information', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to retrieve current peer information', StandardError::INTERNAL_SERVER_ERROR, $e); } try @@ -76,7 +76,7 @@ } catch (DatabaseOperationException $e) { - throw new StandardException('Failed to add the information field', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to add the information field', StandardError::INTERNAL_SERVER_ERROR, $e); } // Update the session flow if necessary @@ -130,15 +130,15 @@ catch (DatabaseOperationException $e) { // Something is seriously wrong if we can't roll back the information field - throw new StandardException('Failed to rollback the information field', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to rollback the information field', StandardError::INTERNAL_SERVER_ERROR, $e); } - if($e instanceof StandardException) + if($e instanceof StandardRpcException) { throw $e; } - throw new StandardException('Failed to update the session flow', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to update the session flow', StandardError::INTERNAL_SERVER_ERROR, $e); } diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddSignature.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddSignature.php index cdf184f..796f6a2 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddSignature.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddSignature.php @@ -7,7 +7,7 @@ use Socialbox\Abstracts\Method; use Socialbox\Classes\Configuration; use Socialbox\Enums\StandardError; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\SigningKeysManager; use Socialbox\Objects\ClientRequest; @@ -54,7 +54,7 @@ } catch(Exception $e) { - throw new StandardException('Failed to add the signing key', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to add the signing key', StandardError::INTERNAL_SERVER_ERROR, $e); } return $rpcRequest->produceResponse($uuid); diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteInformationField.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteInformationField.php index 6fbbb8c..32e095e 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteInformationField.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteInformationField.php @@ -7,7 +7,7 @@ use Socialbox\Enums\StandardError; use Socialbox\Enums\Types\InformationFieldName; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PeerInformationManager; use Socialbox\Objects\ClientRequest; @@ -40,7 +40,7 @@ } catch(DatabaseOperationException $e) { - throw new StandardException('Failed to check if the information field exists', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to check if the information field exists', StandardError::INTERNAL_SERVER_ERROR, $e); } switch($fieldName) @@ -111,7 +111,7 @@ } catch(DatabaseOperationException $e) { - throw new StandardException('Failed to delete the information field', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to delete the information field', StandardError::INTERNAL_SERVER_ERROR, $e); } return $rpcRequest->produceResponse(true); diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteOtp.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteOtp.php index 2a8fe02..c6d504c 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteOtp.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteOtp.php @@ -8,7 +8,7 @@ use Socialbox\Classes\Cryptography; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\OneTimePasswordManager; use Socialbox\Managers\PasswordManager; @@ -38,7 +38,7 @@ } catch (DatabaseOperationException $e) { - throw new StandardException('Failed to check One Time Password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to check One Time Password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } try @@ -47,7 +47,7 @@ } catch (DatabaseOperationException $e) { - throw new StandardException('Failed to check password usage due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to check password usage due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } // Password verification is required to set an OTP if a password is set @@ -72,7 +72,7 @@ } catch(Exception $e) { - throw new StandardException('Failed to verify password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to verify password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } } @@ -83,7 +83,7 @@ } catch(Exception $e) { - throw new StandardException('Failed to set password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to set password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } return $rpcRequest->produceResponse(true); diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeletePassword.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeletePassword.php index cb02dd3..caec050 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeletePassword.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeletePassword.php @@ -8,7 +8,7 @@ use Socialbox\Classes\Cryptography; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\CryptographyException; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PasswordManager; use Socialbox\Objects\ClientRequest; @@ -67,7 +67,7 @@ } catch (Exception $e) { - throw new StandardException('Failed to check password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to check password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } // Success diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteSignature.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteSignature.php index 856db73..0211037 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteSignature.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteSignature.php @@ -7,7 +7,7 @@ use ncc\ThirdParty\Symfony\Uid\Uuid; use Socialbox\Abstracts\Method; use Socialbox\Enums\StandardError; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\SigningKeysManager; use Socialbox\Objects\ClientRequest; @@ -40,7 +40,7 @@ } catch(Exception $e) { - throw new StandardException('Failed to delete the signing key', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to delete the signing key', StandardError::INTERNAL_SERVER_ERROR, $e); } return $rpcRequest->produceResponse(true); diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetInformationField.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetInformationField.php index 2333d71..61447e9 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetInformationField.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetInformationField.php @@ -6,7 +6,7 @@ use Socialbox\Enums\StandardError; use Socialbox\Enums\Types\InformationFieldName; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PeerInformationManager; use Socialbox\Objects\ClientRequest; @@ -35,7 +35,7 @@ } catch(DatabaseOperationException $e) { - throw new StandardException('Failed to retrieve existing information fields', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to retrieve existing information fields', StandardError::INTERNAL_SERVER_ERROR, $e); } if($fieldRecord === null) diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetInformationFields.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetInformationFields.php index a702193..30e609f 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetInformationFields.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetInformationFields.php @@ -5,7 +5,7 @@ use Socialbox\Abstracts\Method; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PeerInformationManager; use Socialbox\Objects\ClientRequest; @@ -24,7 +24,7 @@ } catch(DatabaseOperationException $e) { - throw new StandardException('Failed to retrieve existing information fields', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to retrieve existing information fields', StandardError::INTERNAL_SERVER_ERROR, $e); } return $rpcRequest->produceResponse(array_map(fn($result) => $result->toInformationFieldState(), $fieldRecords)); diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSigningKey.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSigningKey.php index 01ddfd0..d58923b 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSigningKey.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSigningKey.php @@ -5,7 +5,7 @@ use Socialbox\Abstracts\Method; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\SigningKeysManager; use Socialbox\Objects\ClientRequest; @@ -29,7 +29,7 @@ } catch (DatabaseOperationException $e) { - throw new StandardException('Failed to get the signing keys', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to get the signing keys', StandardError::INTERNAL_SERVER_ERROR, $e); } if($key === null) diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSigningKeys.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSigningKeys.php index e4b729c..ee4dfb1 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSigningKeys.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSigningKeys.php @@ -5,7 +5,7 @@ use Socialbox\Abstracts\Method; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\SigningKeysManager; use Socialbox\Objects\ClientRequest; @@ -24,7 +24,7 @@ } catch (DatabaseOperationException $e) { - throw new StandardException('Failed to get the signing keys', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to get the signing keys', StandardError::INTERNAL_SERVER_ERROR, $e); } if(empty($keys)) diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsSetOtp.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsSetOtp.php index 7ecc46c..34e933d 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsSetOtp.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsSetOtp.php @@ -8,7 +8,7 @@ use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\OneTimePasswordManager; use Socialbox\Managers\PasswordManager; @@ -34,7 +34,7 @@ } catch (DatabaseOperationException $e) { - throw new StandardException('Failed to check One Time Password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to check One Time Password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } if($peer->isEnabled()) @@ -46,7 +46,7 @@ } catch (DatabaseOperationException $e) { - throw new StandardException('Failed to check password usage due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to check password usage due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } } else @@ -76,7 +76,7 @@ } catch(Exception $e) { - throw new StandardException('Failed to verify password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to verify password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } } @@ -90,7 +90,7 @@ } catch(Exception $e) { - throw new StandardException('Failed to set password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to set password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } return $rpcRequest->produceResponse($totpUri); diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsSetPassword.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsSetPassword.php index 4a6000d..91c688b 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsSetPassword.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsSetPassword.php @@ -8,7 +8,7 @@ use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PasswordManager; use Socialbox\Managers\SessionManager; @@ -41,7 +41,7 @@ } catch (DatabaseOperationException $e) { - throw new StandardException('Failed to check password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to check password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } try @@ -54,7 +54,7 @@ } catch(Exception $e) { - throw new StandardException('Failed to set password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to set password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } return $rpcRequest->produceResponse(true); diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdateInformationField.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdateInformationField.php index 6e14a3c..01dc98d 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdateInformationField.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdateInformationField.php @@ -6,7 +6,7 @@ use Socialbox\Enums\StandardError; use Socialbox\Enums\Types\InformationFieldName; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PeerInformationManager; use Socialbox\Objects\ClientRequest; @@ -53,7 +53,7 @@ } catch(DatabaseOperationException $e) { - throw new StandardException('Failed to update the information field', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to update the information field', StandardError::INTERNAL_SERVER_ERROR, $e); } diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdateInformationPrivacy.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdateInformationPrivacy.php index ce08b2a..9c8e449 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdateInformationPrivacy.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdateInformationPrivacy.php @@ -7,7 +7,7 @@ use Socialbox\Enums\StandardError; use Socialbox\Enums\Types\InformationFieldName; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PeerInformationManager; use Socialbox\Objects\ClientRequest; @@ -55,7 +55,7 @@ } catch(DatabaseOperationException $e) { - throw new StandardException('Failed to update the information field', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to update the information field', StandardError::INTERNAL_SERVER_ERROR, $e); } diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdatePassword.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdatePassword.php index e098769..a644788 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdatePassword.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdatePassword.php @@ -7,7 +7,7 @@ use Socialbox\Classes\Cryptography; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PasswordManager; use Socialbox\Objects\ClientRequest; @@ -49,7 +49,7 @@ } catch (DatabaseOperationException $e) { - throw new StandardException('Failed to check password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to check password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } try @@ -61,7 +61,7 @@ } catch (Exception $e) { - throw new StandardException('Failed to verify existing password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to verify existing password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } try @@ -71,7 +71,7 @@ } catch(Exception $e) { - throw new StandardException('Failed to set password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to set password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } return $rpcRequest->produceResponse(true); diff --git a/src/Socialbox/Classes/StandardMethods/Verification/Authenticate.php b/src/Socialbox/Classes/StandardMethods/Verification/Authenticate.php index 15dce3f..2fe5438 100644 --- a/src/Socialbox/Classes/StandardMethods/Verification/Authenticate.php +++ b/src/Socialbox/Classes/StandardMethods/Verification/Authenticate.php @@ -6,7 +6,7 @@ use Socialbox\Abstracts\Method; use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\SessionManager; use Socialbox\Objects\ClientRequest; @@ -35,7 +35,7 @@ } catch(Exception $e) { - throw new StandardException('An error occurred while authenticating the peer', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('An error occurred while authenticating the peer', StandardError::INTERNAL_SERVER_ERROR, $e); } diff --git a/src/Socialbox/Classes/StandardMethods/Verification/VerificationAnswerImageCaptcha.php b/src/Socialbox/Classes/StandardMethods/Verification/VerificationAnswerImageCaptcha.php index 1c5c922..79af8fd 100644 --- a/src/Socialbox/Classes/StandardMethods/Verification/VerificationAnswerImageCaptcha.php +++ b/src/Socialbox/Classes/StandardMethods/Verification/VerificationAnswerImageCaptcha.php @@ -6,7 +6,7 @@ use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\CaptchaManager; use Socialbox\Managers\SessionManager; @@ -37,7 +37,7 @@ } catch(DatabaseOperationException $e) { - throw new StandardException("There was an unexpected error while trying to get the captcha", StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException("There was an unexpected error while trying to get the captcha", StandardError::INTERNAL_SERVER_ERROR, $e); } try @@ -51,7 +51,7 @@ } catch (DatabaseOperationException $e) { - throw new StandardException("There was an unexpected error while trying to answer the captcha", StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException("There was an unexpected error while trying to answer the captcha", StandardError::INTERNAL_SERVER_ERROR, $e); } return $rpcRequest->produceResponse($result); diff --git a/src/Socialbox/Classes/StandardMethods/Verification/VerificationGetImageCaptcha.php b/src/Socialbox/Classes/StandardMethods/Verification/VerificationGetImageCaptcha.php index ddc4459..21066e9 100644 --- a/src/Socialbox/Classes/StandardMethods/Verification/VerificationGetImageCaptcha.php +++ b/src/Socialbox/Classes/StandardMethods/Verification/VerificationGetImageCaptcha.php @@ -7,7 +7,7 @@ use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\CaptchaManager; use Socialbox\Objects\ClientRequest; @@ -53,7 +53,7 @@ } catch (DatabaseOperationException $e) { - throw new StandardException("There was an unexpected error while trying create the captcha", StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException("There was an unexpected error while trying create the captcha", StandardError::INTERNAL_SERVER_ERROR, $e); } // Build the captcha diff --git a/src/Socialbox/Classes/StandardMethods/Verification/VerificationOtpAuthentication.php b/src/Socialbox/Classes/StandardMethods/Verification/VerificationOtpAuthentication.php index b85dc6d..3d5c3c1 100644 --- a/src/Socialbox/Classes/StandardMethods/Verification/VerificationOtpAuthentication.php +++ b/src/Socialbox/Classes/StandardMethods/Verification/VerificationOtpAuthentication.php @@ -8,7 +8,7 @@ use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\CryptographyException; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\OneTimePasswordManager; use Socialbox\Managers\SessionManager; @@ -55,7 +55,7 @@ } catch (Exception $e) { - throw new StandardException('Failed to verify password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to verify password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } return $rpcRequest->produceResponse($result); diff --git a/src/Socialbox/Classes/StandardMethods/Verification/VerificationPasswordAuthentication.php b/src/Socialbox/Classes/StandardMethods/Verification/VerificationPasswordAuthentication.php index 9b1f321..f2cc87b 100644 --- a/src/Socialbox/Classes/StandardMethods/Verification/VerificationPasswordAuthentication.php +++ b/src/Socialbox/Classes/StandardMethods/Verification/VerificationPasswordAuthentication.php @@ -8,7 +8,7 @@ use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\CryptographyException; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PasswordManager; use Socialbox\Managers\SessionManager; @@ -54,7 +54,7 @@ } catch (Exception $e) { - throw new StandardException('Failed to verify password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to verify password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } return $rpcRequest->produceResponse($result); diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index d62221a..ee14a03 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -40,7 +40,7 @@ use Socialbox\Classes\StandardMethods\Verification\VerificationPasswordAuthentication; use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\OneTimePasswordManager; use Socialbox\Managers\PasswordManager; @@ -145,7 +145,7 @@ * @param ClientRequest $request The client request object containing necessary data for execution. * @param RpcRequest $rpcRequest The RPC request object providing additional parameters for execution. * @return SerializableInterface|null The result of the operation as a serializable interface or null if no operation matches. - * @throws StandardException If an error occurs during execution + * @throws StandardRpcException If an error occurs during execution */ public function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { @@ -206,7 +206,7 @@ * @param ClientRequest $clientRequest The client request instance to check access against. * @return void * @throws DatabaseOperationException If an error occurs while checking the database for session information. - * @throws StandardException If the method is not allowed for the given client request. + * @throws StandardRpcException If the method is not allowed for the given client request. */ public function checkAccess(ClientRequest $clientRequest): void { @@ -215,7 +215,7 @@ return; } - throw new StandardException(StandardError::METHOD_NOT_ALLOWED->getMessage(), StandardError::METHOD_NOT_ALLOWED); + throw new StandardRpcException(StandardError::METHOD_NOT_ALLOWED->getMessage(), StandardError::METHOD_NOT_ALLOWED); } /** diff --git a/src/Socialbox/Exceptions/Standard/InvalidRpcArgumentException.php b/src/Socialbox/Exceptions/Standard/InvalidRpcArgumentException.php index fe167a9..45b8673 100644 --- a/src/Socialbox/Exceptions/Standard/InvalidRpcArgumentException.php +++ b/src/Socialbox/Exceptions/Standard/InvalidRpcArgumentException.php @@ -4,7 +4,7 @@ use Socialbox\Enums\StandardError; - class InvalidRpcArgumentException extends StandardException + class InvalidRpcArgumentException extends StandardRpcException { /** * Thrown when a required parameter is missing diff --git a/src/Socialbox/Exceptions/Standard/MissingRpcArgumentException.php b/src/Socialbox/Exceptions/Standard/MissingRpcArgumentException.php index ed8d459..6ac23a0 100644 --- a/src/Socialbox/Exceptions/Standard/MissingRpcArgumentException.php +++ b/src/Socialbox/Exceptions/Standard/MissingRpcArgumentException.php @@ -4,7 +4,7 @@ use Socialbox\Enums\StandardError; - class MissingRpcArgumentException extends StandardException + class MissingRpcArgumentException extends StandardRpcException { /** * Thrown when a required parameter is missing diff --git a/src/Socialbox/Exceptions/Standard/StandardException.php b/src/Socialbox/Exceptions/Standard/StandardRpcException.php similarity index 96% rename from src/Socialbox/Exceptions/Standard/StandardException.php rename to src/Socialbox/Exceptions/Standard/StandardRpcException.php index 8cd7382..9b3a002 100644 --- a/src/Socialbox/Exceptions/Standard/StandardException.php +++ b/src/Socialbox/Exceptions/Standard/StandardRpcException.php @@ -10,7 +10,7 @@ use Socialbox\Objects\RpcRequest; use Throwable; - class StandardException extends Exception + class StandardRpcException extends Exception { /** * Thrown as a standard error, with a message and a code diff --git a/src/Socialbox/Managers/SessionManager.php b/src/Socialbox/Managers/SessionManager.php index 683e266..2b4d49c 100644 --- a/src/Socialbox/Managers/SessionManager.php +++ b/src/Socialbox/Managers/SessionManager.php @@ -15,7 +15,7 @@ use Socialbox\Enums\SessionState; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Objects\Database\PeerRecord; use Socialbox\Objects\Database\SessionRecord; use Socialbox\Objects\KeyPair; @@ -181,7 +181,7 @@ * @param string $uuid The unique identifier of the session. * @return SessionRecord The session record corresponding to the given UUID. * @throws DatabaseOperationException If the session record cannot be found or if there is an error during retrieval. - * @throws StandardException + * @throws StandardRpcException */ public static function getSession(string $uuid): SessionRecord { @@ -196,7 +196,7 @@ if ($data === false) { - throw new StandardException(sprintf("The requested session '%s' does not exist", $uuid), StandardError::SESSION_NOT_FOUND); + throw new StandardRpcException(sprintf("The requested session '%s' does not exist", $uuid), StandardError::SESSION_NOT_FOUND); } // Convert the timestamp fields to DateTime objects @@ -308,7 +308,7 @@ * * @param string $uuid The UUID of the session to retrieve flags for. * @return SessionFlags[] An array of flags associated with the specified session. - * @throws StandardException If the specified session does not exist. + * @throws StandardRpcException If the specified session does not exist. * @throws DatabaseOperationException If there */ private static function getFlags(string $uuid): array @@ -324,7 +324,7 @@ if ($data === false) { - throw new StandardException(sprintf("The requested session '%s' does not exist", $uuid), StandardError::SESSION_NOT_FOUND); + throw new StandardRpcException(sprintf("The requested session '%s' does not exist", $uuid), StandardError::SESSION_NOT_FOUND); } return SessionFlags::fromString($data['flags']); @@ -341,7 +341,7 @@ * @param string $uuid The unique identifier of the session to which the flags will be added. * @param array $flags The flags to add to the session. * @return void - * @throws DatabaseOperationException|StandardException If there is an error while updating the session in the database. + * @throws DatabaseOperationException|StandardRpcException If there is an error while updating the session in the database. */ public static function addFlags(string $uuid, array $flags): void { @@ -372,7 +372,7 @@ * @param string $uuid The UUID of the session from which the flags will be removed. * @param SessionFlags[] $flags An array of flags to be removed from the session. * @return void - * @throws DatabaseOperationException|StandardException If there is an error while updating the session in the database. + * @throws DatabaseOperationException|StandardRpcException If there is an error while updating the session in the database. */ public static function removeFlags(string $uuid, array $flags): void { @@ -429,7 +429,7 @@ * @param array $flagsToRemove An array of flags to remove from the session if it is marked as complete. * @return void * @throws DatabaseOperationException If there is an error while updating the session in the database. - * @throws StandardException If the session record cannot be found or if there is an error during retrieval. + * @throws StandardRpcException If the session record cannot be found or if there is an error during retrieval. */ public static function updateFlow(SessionRecord $session, array $flagsToRemove=[]): void { diff --git a/src/Socialbox/Objects/RpcRequest.php b/src/Socialbox/Objects/RpcRequest.php index 4e9f438..1efe1d8 100644 --- a/src/Socialbox/Objects/RpcRequest.php +++ b/src/Socialbox/Objects/RpcRequest.php @@ -6,7 +6,7 @@ use Socialbox\Classes\Logger; use Socialbox\Enums\StandardError; use Socialbox\Enums\StandardMethods; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; class RpcRequest implements SerializableInterface @@ -162,10 +162,10 @@ } /** - * @param StandardException $e + * @param StandardRpcException $e * @return RpcError|null */ - public function handleStandardException(StandardException $e): ?RpcError + public function handleStandardException(StandardRpcException $e): ?RpcError { return $this->produceError($e->getStandardError(), $e->getMessage()); } diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index 6fc2685..1b4d0b3 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -26,7 +26,7 @@ use Socialbox\Exceptions\RequestException; use Socialbox\Exceptions\ResolutionException; use Socialbox\Exceptions\RpcException; - use Socialbox\Exceptions\Standard\StandardException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Managers\ContactManager; use Socialbox\Managers\ExternalSessionManager; use Socialbox\Managers\PeerInformationManager; @@ -567,7 +567,7 @@ { $method->checkAccess($clientRequest); } - catch (StandardException $e) + catch (StandardRpcException $e) { $response = $e->produceError($rpcRequest); $results[] = $response->toArray(); @@ -587,7 +587,7 @@ $response = $method->execute($clientRequest, $rpcRequest); Logger::getLogger()->debug(sprintf('%s method executed successfully', $rpcRequest->getMethod())); } - catch(StandardException $e) + catch(StandardRpcException $e) { Logger::getLogger()->error('An error occurred while processing the RPC request', $e); $response = $e->produceError($rpcRequest); @@ -755,7 +755,7 @@ * @param PeerAddress|string $peerAddress The peer address or string identifier to be resolved. * @param string $signatureUuid The UUID of the signature key to be resolved. * @return SigningKey The resolved signing key for the peer. - * @throws StandardException If there was an error while resolving the peer signature key. + * @throws StandardRpcException If there was an error while resolving the peer signature key. */ public static function resolvePeerSignature(PeerAddress|string $peerAddress, string $signatureUuid): SigningKey { @@ -768,14 +768,14 @@ } catch(InvalidArgumentException $e) { - throw new StandardException($e->getMessage(), StandardError::RPC_INVALID_ARGUMENTS, $e); + throw new StandardRpcException($e->getMessage(), StandardError::RPC_INVALID_ARGUMENTS, $e); } } // Prevent resolutions against any host if($peerAddress->getUsername() == ReservedUsernames::HOST) { - throw new StandardException('Cannot resolve signature for a host peer', StandardError::FORBIDDEN); + throw new StandardRpcException('Cannot resolve signature for a host peer', StandardError::FORBIDDEN); } // If the peer is registered within this server @@ -788,22 +788,22 @@ if($peer === null || !$peer?->isEnabled()) { // Fail if the peer is not found or enabled - throw new StandardException(sprintf('The peer %s does not exist', $peerAddress), StandardError::PEER_NOT_FOUND); + throw new StandardRpcException(sprintf('The peer %s does not exist', $peerAddress), StandardError::PEER_NOT_FOUND); } $signingKey = SigningKeysManager::getSigningKey($peer->getUuid(), $signatureUuid); if($signingKey === null) { - throw new StandardException(sprintf('The requested signing key %s was not found', $signatureUuid), StandardError::NOT_FOUND); + throw new StandardRpcException(sprintf('The requested signing key %s was not found', $signatureUuid), StandardError::NOT_FOUND); } } - catch(StandardException $e) + catch(StandardRpcException $e) { throw $e; } catch(Exception $e) { - throw new StandardException('There was an error while trying to resolve the signature key for the peer locally', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('There was an error while trying to resolve the signature key for the peer locally', StandardError::INTERNAL_SERVER_ERROR, $e); } return $signingKey->toStandard(); @@ -816,7 +816,7 @@ } catch(Exception $e) { - throw new StandardException(sprintf('There was an error while trying to communicate with %s', $peerAddress->getDomain()), StandardError::RESOLUTION_FAILED, $e); + throw new StandardRpcException(sprintf('There was an error while trying to communicate with %s', $peerAddress->getDomain()), StandardError::RESOLUTION_FAILED, $e); } try @@ -826,7 +826,7 @@ catch(RpcException $e) { // Reflect the server error to the client - throw new StandardException($e->getMessage(), StandardError::tryFrom((int)$e->getCode()) ?? StandardError::UNKNOWN, $e); + throw new StandardRpcException($e->getMessage(), StandardError::tryFrom((int)$e->getCode()) ?? StandardError::UNKNOWN, $e); } } @@ -836,7 +836,7 @@ * @param PeerAddress|string $peerAddress The external peer address or string identifier to be resolved. * @param PeerAddress|string|null $identifiedAs Optional. The peer address or string identifier by which the caller is identified * @return Peer The resolved external peer after synchronization. - * @throws StandardException Thrown if there was an error with the resolution process + * @throws StandardRpcException Thrown if there was an error with the resolution process */ public static function resolvePeer(PeerAddress|string $peerAddress, null|PeerAddress|string $identifiedAs=null): Peer { @@ -870,7 +870,7 @@ * @param PeerAddress|string $peerAddress The peer address or string identifier to be resolved. * @param PeerAddress|string|null $identifiedAs Optional. The peer address or string identifier by which the caller is identified * @return Peer The resolved peer after synchronization. - * @throws StandardException Thrown if there was an error with the resolution process + * @throws StandardRpcException Thrown if there was an error with the resolution process */ private static function resolveExternalPeer(PeerAddress|string $peerAddress, null|PeerAddress|string $identifiedAs=null): Peer { @@ -891,7 +891,7 @@ } catch(DatabaseOperationException $e) { - throw new StandardException('Failed to resolve the peer: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to resolve the peer: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); } if($existingPeer === null) @@ -904,7 +904,7 @@ } catch(Exception $e) { - throw new StandardException('Failed to resolve the peer: ' . $e->getMessage(), StandardError::RESOLUTION_FAILED, $e); + throw new StandardRpcException('Failed to resolve the peer: ' . $e->getMessage(), StandardError::RESOLUTION_FAILED, $e); } try @@ -913,7 +913,7 @@ } catch(DatabaseOperationException $e) { - throw new StandardException('Failed to synchronize the external peer: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to synchronize the external peer: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); } return $peer; @@ -928,7 +928,7 @@ } catch(Exception $e) { - throw new StandardException('Failed to resolve the peer: ' . $e->getMessage(), StandardError::RESOLUTION_FAILED, $e); + throw new StandardRpcException('Failed to resolve the peer: ' . $e->getMessage(), StandardError::RESOLUTION_FAILED, $e); } try @@ -937,7 +937,7 @@ } catch(DatabaseOperationException $e) { - throw new StandardException('Failed to synchronize the external peer: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to synchronize the external peer: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); } return $peer; @@ -953,7 +953,7 @@ * @param PeerAddress|string $peerAddress The peer address or string identifier to be resolved. * @param PeerAddress|string|null $identifiedAs Optional. The peer address or string identifier by which the caller is identified * @return Peer The resolved peer after synchronization. - * @throws StandardException Thrown if there was an error with the resolution process + * @throws StandardRpcException Thrown if there was an error with the resolution process */ private static function resolveLocalPeer(PeerAddress|string $peerAddress, null|PeerAddress|string $identifiedAs=null): Peer { @@ -974,12 +974,12 @@ if($peer === null) { - throw new StandardException('The requested peer was not found', StandardError::PEER_NOT_FOUND); + throw new StandardRpcException('The requested peer was not found', StandardError::PEER_NOT_FOUND); } } catch(DatabaseOperationException $e) { - throw new StandardException('Failed to resolve the peer: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to resolve the peer: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); } try @@ -989,7 +989,7 @@ } catch (DatabaseOperationException $e) { - throw new StandardException('Failed to resolve peer information: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to resolve peer information: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); } // If there's an identifier, we can resolve more information fields if the target peer has added the caller @@ -1002,7 +1002,7 @@ } catch (DatabaseOperationException $e) { - throw new StandardException('Failed to resolve peer because there was an error while trying to retrieve contact information for the peer', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to resolve peer because there was an error while trying to retrieve contact information for the peer', StandardError::INTERNAL_SERVER_ERROR, $e); } // If it is a contact, what sort of contact? retrieve depending on the contact type @@ -1023,7 +1023,7 @@ } catch (DatabaseOperationException $e) { - throw new StandardException('Failed to resolve peer information: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to resolve peer information: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); } } } From 4f8f43d10b85589411e71da9d3da04640a15490d Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 30 Jan 2025 12:30:20 -0500 Subject: [PATCH 284/420] Updated Exception Handling to AddressBookGetContact --- .../StandardMethods/AddressBook/AddressBookGetContact.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContact.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContact.php index 201f9f7..756fc37 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContact.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContact.php @@ -6,6 +6,8 @@ use Socialbox\Abstracts\Method; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; + use Socialbox\Exceptions\Standard\MissingRpcArgumentException; use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\ContactManager; @@ -22,7 +24,7 @@ { if(!$rpcRequest->containsParameter('peer')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing required peer parameter'); + throw new MissingRpcArgumentException('peer'); } try @@ -31,14 +33,14 @@ } catch(InvalidArgumentException $e) { - throw new StandardRpcException('Invalid peer address', StandardError::RPC_INVALID_ARGUMENTS, $e); + throw new InvalidRpcArgumentException('peer', $e->getMessage()); } try { if(!ContactManager::isContact($request->getPeer(), $address)) { - return $rpcRequest->produceError(StandardError::FORBIDDEN, 'Contact does not exist'); + return $rpcRequest->produceError(StandardError::NOT_FOUND, 'Contact does not exist'); } $contact = ContactManager::getContact($request->getPeer(), $address); From 95d7a8b021d5400ba19a662e526666d22f7e6046 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 30 Jan 2025 12:31:59 -0500 Subject: [PATCH 285/420] Updated Exception Handling to AddressBookGetContacts --- .../StandardMethods/AddressBook/AddressBookGetContacts.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContacts.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContacts.php index 5f9775e..7b583b6 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContacts.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContacts.php @@ -6,6 +6,7 @@ use Socialbox\Classes\Configuration; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\ContactManager; @@ -23,9 +24,9 @@ if($rpcRequest->containsParameter('limit')) { $limit = (int)$rpcRequest->getParameter('limit'); - if($limit < 1) + if($limit <= 0) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Invalid limit, must be greater than 0'); + throw new InvalidRpcArgumentException('limit', 'Invalid limit, must be greater than 0'); } $limit = min($limit, Configuration::getPoliciesConfiguration()->getGetContactsLimit()); @@ -37,7 +38,7 @@ $page = (int)$rpcRequest->getParameter('page'); if($page < 0) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Invalid page, must be greater than or equal to 0'); + throw new InvalidRpcArgumentException('page', 'Invalid page, must be greater than or equal to 0'); } $page = max($page, 0); From a934049ea552d197228a205c45982edfd670547a Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 30 Jan 2025 12:41:04 -0500 Subject: [PATCH 286/420] Changed Format --- src/Socialbox/Managers/CaptchaManager.php | 324 +++++++++++----------- 1 file changed, 162 insertions(+), 162 deletions(-) diff --git a/src/Socialbox/Managers/CaptchaManager.php b/src/Socialbox/Managers/CaptchaManager.php index 2ccba7e..fbb9bec 100644 --- a/src/Socialbox/Managers/CaptchaManager.php +++ b/src/Socialbox/Managers/CaptchaManager.php @@ -1,44 +1,62 @@ getUuid(); - } + // If the peer_uuid is a RegisteredPeerRecord, get the UUID + if($peer_uuid instanceof PeerRecord) + { + $peer_uuid = $peer_uuid->getUuid(); + } - $answer = Utilities::randomString(6, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'); - $current_time = (new DateTime())->setTimestamp(time())->format('Y-m-d H:i:s'); + $answer = Utilities::randomString(6, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'); + $current_time = (new DateTime())->setTimestamp(time())->format('Y-m-d H:i:s'); - if(!self::captchaExists($peer_uuid)) - { - Logger::getLogger()->debug('Creating a new captcha record for peer ' . $peer_uuid); - $statement = Database::getConnection()->prepare("INSERT INTO captcha_images (peer_uuid, created, answer) VALUES (?, ?, ?)"); - $statement->bindParam(1, $peer_uuid); + if(!self::captchaExists($peer_uuid)) + { + Logger::getLogger()->debug('Creating a new captcha record for peer ' . $peer_uuid); + $statement = Database::getConnection()->prepare("INSERT INTO captcha_images (peer_uuid, created, answer) VALUES (?, ?, ?)"); + $statement->bindParam(1, $peer_uuid); + $statement->bindParam(2, $current_time); + $statement->bindParam(3, $answer); + + try + { + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to create a captcha in the database', $e); + } + + return $answer; + } + + Logger::getLogger()->debug('Updating an existing captcha record for peer ' . $peer_uuid); + $statement = Database::getConnection()->prepare("UPDATE captcha_images SET answer=?, status='UNSOLVED', created=? WHERE peer_uuid=?"); + $statement->bindParam(1, $answer); $statement->bindParam(2, $current_time); - $statement->bindParam(3, $answer); + $statement->bindParam(3, $peer_uuid); try { @@ -46,152 +64,134 @@ class CaptchaManager } catch(PDOException $e) { - throw new DatabaseOperationException('Failed to create a captcha in the database', $e); + throw new DatabaseOperationException('Failed to update a captcha in the database', $e); } return $answer; } - Logger::getLogger()->debug('Updating an existing captcha record for peer ' . $peer_uuid); - $statement = Database::getConnection()->prepare("UPDATE captcha_images SET answer=?, status='UNSOLVED', created=? WHERE peer_uuid=?"); - $statement->bindParam(1, $answer); - $statement->bindParam(2, $current_time); - $statement->bindParam(3, $peer_uuid); - - try + /** + * Answers a captcha for the given peer UUID. + * + * @param string|PeerRecord $peer_uuid The UUID of the peer to answer the captcha for. + * @param string $answer The answer to the captcha. + * @return bool True if the answer is correct, false otherwise. + * @throws DatabaseOperationException If the operation fails. + */ + public static function answerCaptcha(string|PeerRecord $peer_uuid, string $answer): bool { - $statement->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to update a captcha in the database', $e); - } + if($peer_uuid instanceof PeerRecord) + { + $peer_uuid = $peer_uuid->getUuid(); + } - return $answer; - } + // Return false if the captcha does not exist + if(!self::captchaExists($peer_uuid)) + { + return false; + } - /** - * Answers a captcha for the given peer UUID. - * - * @param string|PeerRecord $peer_uuid The UUID of the peer to answer the captcha for. - * @param string $answer The answer to the captcha. - * @return bool True if the answer is correct, false otherwise. - * @throws DatabaseOperationException If the operation fails. - */ - public static function answerCaptcha(string|PeerRecord $peer_uuid, string $answer): bool - { - if($peer_uuid instanceof PeerRecord) - { - $peer_uuid = $peer_uuid->getUuid(); - } + $captcha = self::getCaptcha($peer_uuid); - // Return false if the captcha does not exist - if(!self::captchaExists($peer_uuid)) - { - return false; - } + // Return false if the captcha has already been solved + if($captcha->getStatus() === CaptchaStatus::SOLVED) + { + return false; + } - $captcha = self::getCaptcha($peer_uuid); + // Return false if the captcha is older than 5 minutes + if ($captcha->isExpired()) + { + return false; + } - // Return false if the captcha has already been solved - if($captcha->getStatus() === CaptchaStatus::SOLVED) - { - return false; - } + // Verify the answer + if($captcha->getAnswer() !== $answer) + { + return false; + } - // Return false if the captcha is older than 5 minutes - if ($captcha->isExpired()) - { - return false; - } - - // Verify the answer - if($captcha->getAnswer() !== $answer) - { - return false; - } - - $statement = Database::getConnection()->prepare("UPDATE captcha_images SET status='SOLVED', answered=NOW() WHERE peer_uuid=?"); - $statement->bindParam(1, $peer_uuid); - - try - { - $statement->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to update a captcha in the database', $e); - } - - return true; - } - - /** - * Retrieves the captcha record for the given peer UUID. - * - * @param string|PeerRecord $peer_uuid The UUID of the peer to retrieve the captcha for. - * @return CaptchaRecord|null The captcha record. - * @throws DatabaseOperationException If the operation fails. - */ - public static function getCaptcha(string|PeerRecord $peer_uuid): ?CaptchaRecord - { - // If the peer_uuid is a RegisteredPeerRecord, get the UUID - if($peer_uuid instanceof PeerRecord) - { - $peer_uuid = $peer_uuid->getUuid(); - } - - Logger::getLogger()->debug('Getting the captcha record for peer ' . $peer_uuid); - - try - { - $statement = Database::getConnection()->prepare("SELECT * FROM captcha_images WHERE peer_uuid=? LIMIT 1"); + $statement = Database::getConnection()->prepare("UPDATE captcha_images SET status='SOLVED', answered=NOW() WHERE peer_uuid=?"); $statement->bindParam(1, $peer_uuid); - $statement->execute(); - $result = $statement->fetch(); + + try + { + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to update a captcha in the database', $e); + } + + return true; } - catch(PDOException $e) + + /** + * Retrieves the captcha record for the given peer UUID. + * + * @param string|PeerRecord $peer_uuid The UUID of the peer to retrieve the captcha for. + * @return CaptchaRecord|null The captcha record. + * @throws DatabaseOperationException If the operation fails. + */ + public static function getCaptcha(string|PeerRecord $peer_uuid): ?CaptchaRecord { - throw new DatabaseOperationException('Failed to get a captcha from the database', $e); + // If the peer_uuid is a RegisteredPeerRecord, get the UUID + if($peer_uuid instanceof PeerRecord) + { + $peer_uuid = $peer_uuid->getUuid(); + } + + Logger::getLogger()->debug('Getting the captcha record for peer ' . $peer_uuid); + + try + { + $statement = Database::getConnection()->prepare("SELECT * FROM captcha_images WHERE peer_uuid=? LIMIT 1"); + $statement->bindParam(1, $peer_uuid); + $statement->execute(); + $result = $statement->fetch(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to get a captcha from the database', $e); + } + + if($result === false) + { + return null; + } + + return CaptchaRecord::fromArray($result); } - if($result === false) + /** + * Checks if a captcha exists for the given peer UUID. + * + * @param string|PeerRecord $peer_uuid The UUID of the peer to check for a captcha. + * @return bool True if a captcha exists, false otherwise. + * @throws DatabaseOperationException If the operation fails. + */ + public static function captchaExists(string|PeerRecord $peer_uuid): bool { - return null; + // If the peer_uuid is a RegisteredPeerRecord, get the UUID + if($peer_uuid instanceof PeerRecord) + { + $peer_uuid = $peer_uuid->getUuid(); + } + + Logger::getLogger()->debug('Checking if a captcha exists for peer ' . $peer_uuid); + + try + { + $statement = Database::getConnection()->prepare("SELECT COUNT(*) FROM captcha_images WHERE peer_uuid=?"); + $statement->bindParam(1, $peer_uuid); + $statement->execute(); + $result = $statement->fetchColumn(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to check if a captcha exists in the database', $e); + } + + return $result > 0; } - - return CaptchaRecord::fromArray($result); - } - - /** - * Checks if a captcha exists for the given peer UUID. - * - * @param string|PeerRecord $peer_uuid The UUID of the peer to check for a captcha. - * @return bool True if a captcha exists, false otherwise. - * @throws DatabaseOperationException If the operation fails. - */ - public static function captchaExists(string|PeerRecord $peer_uuid): bool - { - // If the peer_uuid is a RegisteredPeerRecord, get the UUID - if($peer_uuid instanceof PeerRecord) - { - $peer_uuid = $peer_uuid->getUuid(); - } - - Logger::getLogger()->debug('Checking if a captcha exists for peer ' . $peer_uuid); - - try - { - $statement = Database::getConnection()->prepare("SELECT COUNT(*) FROM captcha_images WHERE peer_uuid=?"); - $statement->bindParam(1, $peer_uuid); - $statement->execute(); - $result = $statement->fetchColumn(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to check if a captcha exists in the database', $e); - } - - return $result > 0; - } -} \ No newline at end of file + } \ No newline at end of file From 4e22a8bacd8d9e5127ffb3151425341aa0b424f2 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 30 Jan 2025 12:42:02 -0500 Subject: [PATCH 287/420] Updated PhpDoc --- src/Socialbox/Managers/ContactManager.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Socialbox/Managers/ContactManager.php b/src/Socialbox/Managers/ContactManager.php index 39339dc..c8ad1a0 100644 --- a/src/Socialbox/Managers/ContactManager.php +++ b/src/Socialbox/Managers/ContactManager.php @@ -19,6 +19,7 @@ * @param string $peerUuid The unique identifier of the peer. * @param string|PeerAddress $contactAddress The contact's address, either as a string or a PeerAddress instance. * @return bool Returns true if the contact exists in the database; otherwise, returns false. + * @throws DatabaseOperationException If the operation fails. */ public static function isContact(string $peerUuid, string|PeerAddress $contactAddress): bool { From 330e7f876b02aed52cbb87935b2f4415a87d6eca Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 30 Jan 2025 15:20:11 -0500 Subject: [PATCH 288/420] Added the ability to trust signing keys & resolve signing keys for peers, minor improvements and added new standard error "CONFLICT" --- src/Socialbox/Classes/Configuration.php | 1 + .../Configuration/PoliciesConfiguration.php | 7 + .../Resources/database/signing_keys.sql | 2 +- .../AddressBook/AddressBookTrustSignature.php | 52 ++++- .../AddressBookUpdateRelationship.php | 10 +- .../Settings/SettingsAddSignature.php | 10 +- src/Socialbox/Enums/StandardError.php | 3 +- src/Socialbox/Managers/ContactManager.php | 198 ++++++++++++++++++ src/Socialbox/Managers/SigningKeysManager.php | 17 +- .../Database/ContactKnownKeyRecord.php | 138 +++++++++++- .../Objects/Database/SigningKeyRecord.php | 8 +- src/Socialbox/Objects/Standard/SigningKey.php | 4 +- src/Socialbox/SocialClient.php | 11 +- src/Socialbox/Socialbox.php | 8 +- 14 files changed, 427 insertions(+), 42 deletions(-) diff --git a/src/Socialbox/Classes/Configuration.php b/src/Socialbox/Classes/Configuration.php index 653ab2e..af43b69 100644 --- a/src/Socialbox/Classes/Configuration.php +++ b/src/Socialbox/Classes/Configuration.php @@ -150,6 +150,7 @@ // Server Policies // The maximum number of signing keys a peer can register onto the server at once $config->setDefault('policies.max_signing_keys', 20); + $config->setDefault('policies.max_contact_signing_keys', 50); // The amount of time in seconds it takes before a session is considered expired due to inactivity // Default: 12hours $config->setDefault('policies.session_inactivity_expires', 43200); diff --git a/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php b/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php index 7a11247..329b92a 100644 --- a/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php +++ b/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php @@ -7,6 +7,7 @@ class PoliciesConfiguration { private int $maxSigningKeys; + private int $maxContactSigningKeys; private int $sessionInactivityExpires; private int $imageCaptchaExpires; private int $peerSyncInterval; @@ -37,6 +38,7 @@ public function __construct(array $data) { $this->maxSigningKeys = $data['max_signing_keys']; + $this->maxContactSigningKeys = $data['max_contact_signing_keys']; $this->sessionInactivityExpires = $data['session_inactivity_expires']; $this->imageCaptchaExpires = $data['image_captcha_expires']; $this->peerSyncInterval = $data['peer_sync_interval']; @@ -61,6 +63,11 @@ return $this->maxSigningKeys; } + public function getMaxContactSigningKeys(): int + { + return $this->maxContactSigningKeys; + } + /** * Returns the maximum amount of seconds before the session is considered expired due to inactivity * diff --git a/src/Socialbox/Classes/Resources/database/signing_keys.sql b/src/Socialbox/Classes/Resources/database/signing_keys.sql index 3853718..9184a57 100644 --- a/src/Socialbox/Classes/Resources/database/signing_keys.sql +++ b/src/Socialbox/Classes/Resources/database/signing_keys.sql @@ -2,7 +2,7 @@ create table signing_keys ( peer_uuid varchar(36) not null comment 'The UUID of the peer', uuid varchar(36) default uuid() not null comment 'The UUID of the key record', - name varchar(64) null comment 'Optional. User provided name for the key', + name varchar(64) not null comment 'Optional. User provided name for the key', public_key varchar(64) not null comment 'The Public Signature Key', state enum ('ACTIVE', 'EXPIRED') default 'ACTIVE' not null comment 'The state of the public key', expires timestamp null comment 'The Timestamp for when this key expires, null = Never expires', diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookTrustSignature.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookTrustSignature.php index 1c6ce68..c04a2a9 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookTrustSignature.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookTrustSignature.php @@ -4,14 +4,18 @@ use InvalidArgumentException; use Socialbox\Abstracts\Method; + use Socialbox\Classes\Configuration; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; + use Socialbox\Exceptions\Standard\MissingRpcArgumentException; use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\ContactManager; use Socialbox\Objects\ClientRequest; use Socialbox\Objects\PeerAddress; use Socialbox\Objects\RpcRequest; + use Socialbox\Socialbox; use Symfony\Component\Uid\Uuid; class AddressBookTrustSignature extends Method @@ -23,7 +27,7 @@ { if(!$rpcRequest->containsParameter('peer')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing required peer parameter'); + throw new MissingRpcArgumentException('peer'); } try @@ -32,12 +36,12 @@ } catch(InvalidArgumentException $e) { - throw new StandardRpcException('Invalid peer address', StandardError::RPC_INVALID_ARGUMENTS, $e); + throw new InvalidRpcArgumentException('peer', $e->getMessage()); } if(!$rpcRequest->containsParameter('uuid')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'uuid' parameter"); + throw new MissingRpcArgumentException('uuid'); } try @@ -46,24 +50,54 @@ } catch(InvalidArgumentException $e) { - throw new StandardRpcException('Invalid UUID', StandardError::RPC_INVALID_ARGUMENTS, $e); + throw new InvalidRpcArgumentException('uuid', $e->getMessage()); } + $signingKey = Socialbox::resolvePeerSignature($address, $uuid); + try { // Check if the contact already exists $peer = $request->getPeer(); - if(ContactManager::isContact($peer, $address)) + if(!ContactManager::isContact($peer, $address)) { - + ContactManager::createContact($peer, $address); } - // Create the contact - ContactManager::updateContactRelationship($peer, $address, $relationship); + $contact = ContactManager::getContact($peer, $address); + + if(ContactManager::contactGetSigningKeysCount($contact) > Configuration::getPoliciesConfiguration()->getMaxContactSigningKeys()) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The contact has exceeded the maximum amount of trusted signatures'); + } } catch (DatabaseOperationException $e) { - throw new StandardRpcException('Failed to update contact relationship', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to check contact state with calling peer', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if($signingKey === null) + { + return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The requested signature key was not found'); + } + + try + { + if(ContactManager::contactSigningKeyUuidExists($contact, $signingKey->getUuid())) + { + return $rpcRequest->produceResponse(false); + } + + if(ContactManager::contactSigningKeyExists($contact, $signingKey->getPublicKey())) + { + return $rpcRequest->produceResponse(false); + } + + ContactManager::addContactSigningKey($contact, $signingKey); + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to trust contact signature', StandardError::INTERNAL_SERVER_ERROR, $e); } // Return success diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookUpdateRelationship.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookUpdateRelationship.php index e30669a..cf217c0 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookUpdateRelationship.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookUpdateRelationship.php @@ -7,6 +7,8 @@ use Socialbox\Enums\StandardError; use Socialbox\Enums\Types\ContactRelationshipType; use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; + use Socialbox\Exceptions\Standard\MissingRpcArgumentException; use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\ContactManager; @@ -23,7 +25,7 @@ { if(!$rpcRequest->containsParameter('peer')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing required peer parameter'); + throw new MissingRpcArgumentException('peer'); } try @@ -32,17 +34,17 @@ } catch(InvalidArgumentException $e) { - throw new StandardRpcException('Invalid peer address', StandardError::RPC_INVALID_ARGUMENTS, $e); + throw new InvalidRpcArgumentException('peer', 'Invalid peer address'); } if(!$rpcRequest->containsParameter('relationship')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing required relationship parameter'); + throw new MissingRpcArgumentException('relationship'); } $relationship = ContactRelationshipType::tryFrom(strtoupper($rpcRequest->getParameter('relationship'))); if($relationship === null) { - throw new StandardRpcException('Invalid relationship type', StandardError::RPC_INVALID_ARGUMENTS); + throw new InvalidRpcArgumentException('relationship', 'Invalid relationship type'); } try diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddSignature.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddSignature.php index 796f6a2..5c7fb3c 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddSignature.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddSignature.php @@ -7,6 +7,7 @@ use Socialbox\Abstracts\Method; use Socialbox\Classes\Configuration; use Socialbox\Enums\StandardError; + use Socialbox\Exceptions\Standard\MissingRpcArgumentException; use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\SigningKeysManager; @@ -22,7 +23,7 @@ { if(!$rpcRequest->containsParameter('public_key')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'public_key' parameter"); + throw new MissingRpcArgumentException('public_key'); } $expires = null; @@ -31,6 +32,11 @@ $expires = (int)$rpcRequest->getParameter('expires'); } + if(!$rpcRequest->containsParameter('name')) + { + throw new MissingRpcArgumentException('name'); + } + $name = null; if($rpcRequest->containsParameter('name') && $rpcRequest->getParameter('name') !== null) { @@ -46,7 +52,7 @@ return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The maximum number of signing keys has been reached'); } - $uuid = SigningKeysManager::addSigningKey($peerUuid, $rpcRequest->getParameter('public_key'), $expires, $name); + $uuid = SigningKeysManager::addSigningKey($peerUuid, $rpcRequest->getParameter('public_key'), $name, $expires); } catch(InvalidArgumentException $e) { diff --git a/src/Socialbox/Enums/StandardError.php b/src/Socialbox/Enums/StandardError.php index 8824d54..ad2721f 100644 --- a/src/Socialbox/Enums/StandardError.php +++ b/src/Socialbox/Enums/StandardError.php @@ -15,7 +15,8 @@ case UNAUTHORIZED = -104; case NOT_FOUND = -105; case RESOLUTION_FAILED = -106; - case CRYPTOGRAPHIC_ERROR = -107; + case CONFLICT = -107; + case CRYPTOGRAPHIC_ERROR = -108; // RPC Errors case RPC_METHOD_NOT_FOUND = -1000; diff --git a/src/Socialbox/Managers/ContactManager.php b/src/Socialbox/Managers/ContactManager.php index c8ad1a0..4bd00e1 100644 --- a/src/Socialbox/Managers/ContactManager.php +++ b/src/Socialbox/Managers/ContactManager.php @@ -9,7 +9,9 @@ use Socialbox\Enums\Types\ContactRelationshipType; use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Objects\Database\ContactDatabaseRecord; + use Socialbox\Objects\Database\ContactKnownKeyRecord; use Socialbox\Objects\PeerAddress; + use Socialbox\Objects\Standard\SigningKey; class ContactManager { @@ -278,4 +280,200 @@ } return $contacts; } + + /** + * Adds a signing key to a contact in the database. + * + * @param string|ContactDatabaseRecord $contactUuid The unique identifier of the contact to add the signing key to. + * @param SigningKey $signingKey The signing key to add to the contact. + * @return void + * @throws DatabaseOperationException If the database query fails. + */ + public static function addContactSigningKey(string|ContactDatabaseRecord $contactUuid, SigningKey $signingKey): void + { + if($contactUuid instanceof ContactDatabaseRecord) + { + $contactUuid = $contactUuid->getUuid(); + } + + try + { + $statement = Database::getConnection()->prepare('INSERT INTO contacts_known_keys (contact_uuid, signature_uuid, signature_name, signature_key, expires, created, trusted_on) VALUES (:contact_uuid, :signature_uuid, :signature_name, :signature_key, :expires, :created, :trusted_on)'); + + $statement->bindParam(':contact_uuid', $contactUuid); + $signatureUuid = $signingKey->getUuid(); + $statement->bindParam(':signature_uuid', $signatureUuid); + $signatureName = $signingKey->getName(); + $statement->bindParam(':signature_name', $signatureName); + $signatureKey = $signingKey->getPublicKey(); + $statement->bindParam(':signature_key', $signatureKey); + $expires = $signingKey->getExpires(); + $statement->bindParam(':expires', $expires); + $created = $signingKey->getCreated(); + $statement->bindParam(':created', $created); + $trustedOn = (new \DateTime())->format('Y-m-d H:i:s'); + $statement->bindParam(':trusted_on', $trustedOn); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to add a signing key to a contact in the database', $e); + } + } + + /** + * Determines if a signing key UUID exists for a contact in the database. + * + * @param string|ContactDatabaseRecord $contactUuid The unique identifier of the contact to check. + * @param string $signatureUuid The UUID of the signing key to check. + * @return bool Returns true if the signing key UUID exists for the contact; otherwise, returns false. + * @throws DatabaseOperationException If the database query fails. + */ + public static function contactSigningKeyUuidExists(string|ContactDatabaseRecord $contactUuid, string $signatureUuid): bool + { + if($contactUuid instanceof ContactDatabaseRecord) + { + $contactUuid = $contactUuid->getUuid(); + } + + try + { + $statement = Database::getConnection()->prepare('SELECT COUNT(*) FROM contacts_known_keys WHERE contact_uuid=:contact_uuid AND signature_uuid=:signature_uuid'); + $statement->bindParam(':contact_uuid', $contactUuid); + $statement->bindParam(':signature_uuid', $signatureUuid); + $statement->execute(); + return $statement->fetchColumn() > 0; + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to check if a signing key UUID exists for a contact in the database', $e); + } + } + + /** + * Determines if a signing key exists for a contact in the database. + * + * @param string|ContactDatabaseRecord $contactUuid The unique identifier of the contact to check. + * @param string $signatureKey The public key of the signing key to check. + * @return bool Returns true if the signing key exists for the contact; otherwise, returns false. + * @throws DatabaseOperationException If the database query fails. + */ + public static function contactSigningKeyExists(string|ContactDatabaseRecord $contactUuid, string $signatureKey): bool + { + if($contactUuid instanceof ContactDatabaseRecord) + { + $contactUuid = $contactUuid->getUuid(); + } + + try + { + $statement = Database::getConnection()->prepare('SELECT COUNT(*) FROM contacts_known_keys WHERE contact_uuid=:contact_uuid AND signature_key=:signature_key'); + $statement->bindParam(':contact_uuid', $contactUuid); + $statement->bindParam(':signature_key', $signatureKey); + $statement->execute(); + return $statement->fetchColumn() > 0; + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to check if a signing key exists for a contact in the database', $e); + } + } + + /** + * Retrieves a signing key for a contact from the database. + * + * @param string|ContactDatabaseRecord $contactUuid The unique identifier of the contact to retrieve the signing key for. + * @param string $signatureUuid The UUID of the signing key to retrieve. + * @return ContactKnownKeyRecord|null The retrieved ContactKnownKeyRecord instance if found, or null if no matching signing key exists. + * @throws DatabaseOperationException If the database query fails. + */ + public static function contactGetSigningKey(string|ContactDatabaseRecord $contactUuid, string $signatureUuid): ?ContactKnownKeyRecord + { + if($contactUuid instanceof ContactDatabaseRecord) + { + $contactUuid = $contactUuid->getUuid(); + } + + try + { + $statement = Database::getConnection()->prepare('SELECT * FROM contacts_known_keys WHERE contact_uuid=:contact_uuid AND signature_uuid=:signature_uuid LIMIT 1'); + $statement->bindParam(':contact_uuid', $contactUuid); + $statement->bindParam(':signature_uuid', $signatureUuid); + $statement->execute(); + $result = $statement->fetch(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to get a signing key for a contact from the database', $e); + } + + if($result === false) + { + return null; + } + + return ContactKnownKeyRecord::fromArray($result); + } + + /** + * Retrieves all signing keys for a contact from the database. + * + * @param string|ContactDatabaseRecord $contactUuid The unique identifier of the contact to retrieve the signing keys for. + * @return ContactKnownKeyRecord[] An array of ContactKnownKeyRecord instances representing the signing keys for the contact. + * @throws DatabaseOperationException If the database query fails. + */ + public static function contactGetSigningKeys(string|ContactDatabaseRecord $contactUuid): array + { + if($contactUuid instanceof ContactDatabaseRecord) + { + $contactUuid = $contactUuid->getUuid(); + } + + $signingKeys = []; + + try + { + $statement = Database::getConnection()->prepare('SELECT * FROM contacts_known_keys WHERE contact_uuid=:contact_uuid'); + $statement->bindParam(':contact_uuid', $contactUuid); + $statement->execute(); + $results = $statement->fetchAll(PDO::FETCH_ASSOC); + + foreach($results as $result) + { + $signingKeys[] = ContactKnownKeyRecord::fromArray($result); + } + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to get signing keys for a contact from the database', $e); + } + + return $signingKeys; + } + + /** + * Retrieves the number of signing keys for a contact from the database. + * + * @param string|ContactDatabaseRecord $contactUuid The unique identifier of the contact to retrieve the signing keys count for. + * @return int The number of signing keys for the contact. + * @throws DatabaseOperationException If the database query fails. + */ + public static function contactGetSigningKeysCount(string|ContactDatabaseRecord $contactUuid): int + { + if($contactUuid instanceof ContactDatabaseRecord) + { + $contactUuid = $contactUuid->getUuid(); + } + + try + { + $statement = Database::getConnection()->prepare('SELECT COUNT(*) FROM contacts_known_keys WHERE contact_uuid=:contact_uuid'); + $statement->bindParam(':contact_uuid', $contactUuid); + $statement->execute(); + return $statement->fetchColumn(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to get the number of signing keys for a contact from the database', $e); + } + } } \ No newline at end of file diff --git a/src/Socialbox/Managers/SigningKeysManager.php b/src/Socialbox/Managers/SigningKeysManager.php index 4d30e1c..2b78fa7 100644 --- a/src/Socialbox/Managers/SigningKeysManager.php +++ b/src/Socialbox/Managers/SigningKeysManager.php @@ -44,28 +44,29 @@ * * @param string $peerUuid The unique identifier of the peer associated with the signing key. * @param string $publicKey The public signing key to be added. Must be valid according to the Cryptography::validatePublicSigningKey method. + * @param string $name Optional name associated with the signing key. Must not exceed 64 characters in length. * @param int|null $expires Optional expiration timestamp for the signing key. Can be null if the key does not expire. - * @param string|null $name Optional name associated with the signing key. Must not exceed 64 characters in length. - * @throws DatabaseOperationException If the operation to add the signing key to the database fails. * @return string The UUID of the newly added signing key. + * @throws DatabaseOperationException If the operation to add the signing key to the database fails. */ - public static function addSigningKey(string $peerUuid, string $publicKey, ?int $expires=null, ?string $name=null): string + public static function addSigningKey(string $peerUuid, string $publicKey, string $name, ?int $expires=null): string { if(!Cryptography::validatePublicSigningKey($publicKey)) { throw new InvalidArgumentException('The public key is invalid'); } + + if(empty($name)) + { + throw new InvalidArgumentException('The name cannot be empty'); + } + if(strlen($name) > 64) { throw new InvalidArgumentException('The name is too long'); } - if($name !== null && empty($name)) - { - throw new InvalidArgumentException('The name cannot be empty'); - } - if($expires !== null) { if($expires === 0) diff --git a/src/Socialbox/Objects/Database/ContactKnownKeyRecord.php b/src/Socialbox/Objects/Database/ContactKnownKeyRecord.php index c553c02..1dd8de4 100644 --- a/src/Socialbox/Objects/Database/ContactKnownKeyRecord.php +++ b/src/Socialbox/Objects/Database/ContactKnownKeyRecord.php @@ -2,17 +2,141 @@ namespace Socialbox\Objects\Database; + use DateTime; + use InvalidArgumentException; use Socialbox\Interfaces\SerializableInterface; class ContactKnownKeyRecord implements SerializableInterface { + private string $contactUuid; + private string $signatureUuid; + private string $signatureName; + private string $signatureKey; + private ?DateTime $expires; + private DateTime $created; + private DateTime $trustedOn; + + public function __construct(array $data) + { + $this->contactUuid = $data['contact_uuid']; + $this->signatureUuid = $data['signature_uuid']; + $this->signatureName = $data['signature_name']; + $this->signatureKey = $data['signature_key']; + + if(!isset($data['expires'])) + { + $this->expires = null; + } + else + { + if(is_string($data['expires'])) + { + $this->expires = new DateTime($data['expires']); + } + elseif(is_int($data['expires'])) + { + $this->expires = (new DateTime())->setTimestamp($data['expires']); + } + elseif($data['expires'] instanceof DateTime) + { + $this->expires = $data['expires']; + } + else + { + throw new InvalidArgumentException('Invalid expires property, got type ' . gettype($data['expires'])); + } + } + + if(!isset($data['created'])) + { + throw new InvalidArgumentException('Missing created property'); + } + else + { + if(is_string($data['created'])) + { + $this->created = new DateTime($data['created']); + } + elseif(is_int($data['created'])) + { + $this->created = (new DateTime())->setTimestamp($data['created']); + } + elseif($data['created'] instanceof DateTime) + { + $this->created = $data['created']; + } + else + { + throw new InvalidArgumentException('Invalid created property, got type ' . gettype($data['created'])); + } + } + + if(!isset($data['trusted_on'])) + { + throw new InvalidArgumentException('Missing trusted_on property'); + } + else + { + if(is_string($data['trusted_on'])) + { + $this->trustedOn = new DateTime($data['trusted_on']); + } + elseif(is_int($data['trusted_on'])) + { + $this->trustedOn = (new DateTime())->setTimestamp($data['trusted_on']); + } + elseif($data['trusted_on'] instanceof DateTime) + { + $this->trustedOn = $data['trusted_on']; + } + else + { + throw new InvalidArgumentException('Invalid trusted_on property, got type ' . gettype($data['trusted_on'])); + } + } + } + + public function getContactUuid(): string + { + return $this->contactUuid; + } + + public function getSignatureUuid(): string + { + return $this->signatureUuid; + } + + public function getSignatureName(): string + { + return $this->signatureName; + } + + public function getSignatureKey(): string + { + return $this->signatureKey; + } + + public function getExpires(): ?DateTime + { + return $this->expires; + } + + public function getCreated(): DateTime + { + return $this->created; + } + + public function getTrustedOn(): DateTime + { + return $this->trustedOn; + } /** * @inheritDoc */ - public static function fromArray(array $data): object + public static function fromArray(array $data): ContactKnownKeyRecord { - // TODO: Implement fromArray() method. + return new self($data); } /** @@ -20,6 +144,14 @@ */ public function toArray(): array { - // TODO: Implement toArray() method. + return [ + 'contact_uuid' => $this->contactUuid, + 'signature_uuid' => $this->signatureUuid, + 'signature_name' => $this->signatureName, + 'signature_key' => $this->signatureKey, + 'expires' => $this->expires?->getTimestamp(), + 'created' => $this->created->getTimestamp(), + 'trusted_on' => $this->trustedOn->getTimestamp() + ]; } } \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/SigningKeyRecord.php b/src/Socialbox/Objects/Database/SigningKeyRecord.php index 62ac8af..2ca95f5 100644 --- a/src/Socialbox/Objects/Database/SigningKeyRecord.php +++ b/src/Socialbox/Objects/Database/SigningKeyRecord.php @@ -12,7 +12,7 @@ { private string $peerUuid; private string $uuid; - private ?string $name; + private string $name; private string $publicKey; private SigningKeyState $state; private int $expires; @@ -35,7 +35,7 @@ { $this->peerUuid = $data['peer_uuid']; $this->uuid = $data['uuid']; - $this->name = $data['name'] ?? null; + $this->name = $data['name']; $this->publicKey = $data['public_key']; $this->state = SigningKeyState::tryFrom($data['state']); @@ -115,9 +115,9 @@ /** * Retrieves the name. * - * @return string|null The name, or null if not set. + * @return string The name, or null if not set. */ - public function getName(): ?string + public function getName(): string { return $this->name; } diff --git a/src/Socialbox/Objects/Standard/SigningKey.php b/src/Socialbox/Objects/Standard/SigningKey.php index fdf2ecb..cfc216e 100644 --- a/src/Socialbox/Objects/Standard/SigningKey.php +++ b/src/Socialbox/Objects/Standard/SigningKey.php @@ -11,7 +11,7 @@ class SigningKey implements SerializableInterface { private string $uuid; - private ?string $name; + private string $name; private string $publicKey; private SigningKeyState $state; private int $expires; @@ -35,7 +35,7 @@ public function __construct(array $data) { $this->uuid = $data['uuid']; - $this->name = $data['name'] ?? null; + $this->name = $data['name']; $this->publicKey = $data['public_key']; $this->state = SigningKeyState::from($data['state']); diff --git a/src/Socialbox/SocialClient.php b/src/Socialbox/SocialClient.php index 0295c0b..57c99e6 100644 --- a/src/Socialbox/SocialClient.php +++ b/src/Socialbox/SocialClient.php @@ -729,21 +729,24 @@ * * @param string|PeerAddress $peerAddress The peer address as a string or an instance of PeerAddress. * @param string $signatureUuid The UUID of the signature to resolve. - * @return SigningKey The resolved signing key. + * @return SigningKey|null The resolved signing key. Null if the resolved key was not found * @throws RpcException Thrown if the RPC request fails. */ - public function resolvePeerSignature(string|PeerAddress $peerAddress, string $signatureUuid): SigningKey + public function resolvePeerSignature(string|PeerAddress $peerAddress, string $signatureUuid): ?SigningKey { if($peerAddress instanceof PeerAddress) { $peerAddress = $peerAddress->getAddress(); } - return SigningKey::fromArray($this->sendRequest( + $result = $this->sendRequest( new RpcRequest(StandardMethods::RESOLVE_PEER_SIGNATURE, Utilities::randomCrc32(), [ 'peer' => $peerAddress, 'uuid' => $signatureUuid ]) - )->getResponse()->getResult()); + )->getResponse()->getResult(); + + // Conditional null-return + return $result ? SigningKey::fromArray($result) : null; } } \ No newline at end of file diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index 1b4d0b3..e5dc7ea 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -754,10 +754,10 @@ * * @param PeerAddress|string $peerAddress The peer address or string identifier to be resolved. * @param string $signatureUuid The UUID of the signature key to be resolved. - * @return SigningKey The resolved signing key for the peer. + * @return SigningKey|null The resolved signing key for the peer. Null if not found * @throws StandardRpcException If there was an error while resolving the peer signature key. */ - public static function resolvePeerSignature(PeerAddress|string $peerAddress, string $signatureUuid): SigningKey + public static function resolvePeerSignature(PeerAddress|string $peerAddress, string $signatureUuid): ?SigningKey { // Convert string peer address to object PeerAddress if(is_string($peerAddress)) @@ -788,13 +788,13 @@ if($peer === null || !$peer?->isEnabled()) { // Fail if the peer is not found or enabled - throw new StandardRpcException(sprintf('The peer %s does not exist', $peerAddress), StandardError::PEER_NOT_FOUND); + return null; } $signingKey = SigningKeysManager::getSigningKey($peer->getUuid(), $signatureUuid); if($signingKey === null) { - throw new StandardRpcException(sprintf('The requested signing key %s was not found', $signatureUuid), StandardError::NOT_FOUND); + return null; } } catch(StandardRpcException $e) From d12b4d1c1dfd26675590891ea1891068e56e071b Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 30 Jan 2025 15:23:19 -0500 Subject: [PATCH 289/420] Updated PhpDocs --- src/Socialbox/Objects/ClientRequest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Socialbox/Objects/ClientRequest.php b/src/Socialbox/Objects/ClientRequest.php index 5d28c4f..0a40634 100644 --- a/src/Socialbox/Objects/ClientRequest.php +++ b/src/Socialbox/Objects/ClientRequest.php @@ -8,6 +8,7 @@ use Socialbox\Exceptions\CryptographyException; use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\RequestException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Managers\RegisteredPeerManager; use Socialbox\Managers\SessionManager; use Socialbox\Objects\Database\PeerRecord; @@ -168,6 +169,8 @@ * Retrieves the current session associated with the session UUID. * * @return SessionRecord|null Returns the associated SessionRecord if the session UUID exists, or null if no session UUID is set. + * @throws DatabaseOperationException Thrown if an error occurs while retrieving the session. + * @throws StandardRpcException Thrown if the session UUID is invalid. */ public function getSession(): ?SessionRecord { @@ -184,6 +187,7 @@ * * @return PeerRecord|null Returns the associated RegisteredPeerRecord if available, or null if no session exists. * @throws DatabaseOperationException Thrown if an error occurs while retrieving the peer. + * @throws StandardRpcException Thrown if the session UUID is invalid. */ public function getPeer(): ?PeerRecord { @@ -212,6 +216,8 @@ * * @param string $decryptedContent The decrypted content to verify the signature against. * @return bool True if the signature is valid, false otherwise. + * @throws DatabaseOperationException Thrown if an error occurs while retrieving the client's public signing key. + * @throws StandardRpcException Thrown if the session UUID is invalid. */ private function verifySignature(string $decryptedContent): bool { From 4dfff8d1a43b9c7d57a92039fe69625bb5319123 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 30 Jan 2025 15:23:41 -0500 Subject: [PATCH 290/420] Added Exception Handling --- .../Classes/StandardMethods/Core/GetSessionState.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Socialbox/Classes/StandardMethods/Core/GetSessionState.php b/src/Socialbox/Classes/StandardMethods/Core/GetSessionState.php index b2ed25f..ec52024 100644 --- a/src/Socialbox/Classes/StandardMethods/Core/GetSessionState.php +++ b/src/Socialbox/Classes/StandardMethods/Core/GetSessionState.php @@ -3,6 +3,9 @@ namespace Socialbox\Classes\StandardMethods\Core; use Socialbox\Abstracts\Method; + use Socialbox\Enums\StandardError; + use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\ClientRequest; use Socialbox\Objects\RpcRequest; @@ -14,6 +17,13 @@ */ public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { - return $rpcRequest->produceResponse($request->getSession()->toStandardSessionState()); + try + { + return $rpcRequest->produceResponse($request->getSession()->toStandardSessionState()); + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to retrieve session state due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + } } } \ No newline at end of file From 8d6675915aadcf0da0fbc32fce8b783b9788cfe2 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 30 Jan 2025 15:24:55 -0500 Subject: [PATCH 291/420] Imported class --- src/Socialbox/Classes/StandardMethods/Core/ResolvePeer.php | 3 ++- src/Socialbox/Enums/StandardMethods.php | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Socialbox/Classes/StandardMethods/Core/ResolvePeer.php b/src/Socialbox/Classes/StandardMethods/Core/ResolvePeer.php index a13e0f8..ce0f60c 100644 --- a/src/Socialbox/Classes/StandardMethods/Core/ResolvePeer.php +++ b/src/Socialbox/Classes/StandardMethods/Core/ResolvePeer.php @@ -7,6 +7,7 @@ use Socialbox\Abstracts\Method; use Socialbox\Enums\ReservedUsernames; use Socialbox\Enums\StandardError; + use Socialbox\Exceptions\Standard\MissingRpcArgumentException; use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\ClientRequest; @@ -25,7 +26,7 @@ // Check if the required 'peer' parameter is set. if(!$rpcRequest->containsParameter('peer')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'peer' parameter"); + throw new MissingRpcArgumentException('peer'); } // Parse the peer address diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index ee14a03..f274fcb 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -6,6 +6,7 @@ use Socialbox\Classes\StandardMethods\AddressBook\AddressBookAddContact; use Socialbox\Classes\StandardMethods\AddressBook\AddressBookDeleteContact; use Socialbox\Classes\StandardMethods\AddressBook\AddressBookGetContacts; + use Socialbox\Classes\StandardMethods\AddressBook\AddressBookTrustSignature; use Socialbox\Classes\StandardMethods\AddressBook\AddressBookUpdateRelationship; use Socialbox\Classes\StandardMethods\Core\GetAllowedMethods; use Socialbox\Classes\StandardMethods\Core\GetSessionState; From 4f1f0ce151f90e1be7c284d4f7484f63077cc38a Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 30 Jan 2025 15:42:18 -0500 Subject: [PATCH 292/420] Added PhpDoc --- src/Socialbox/Enums/StandardMethods.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index f274fcb..e706ab8 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -225,6 +225,7 @@ * @param ClientRequest $clientRequest The client request for which allowed methods are determined. * @return array Returns an array of allowed methods for the provided client request. * @throws DatabaseOperationException If an error occurs while checking the database for session information. + * @throws StandardRpcException */ public static function getAllowedMethods(ClientRequest $clientRequest): array { @@ -285,6 +286,8 @@ * * @param ClientRequest $clientRequest The client request object containing all the request parameters * @return array Returns an array methods that are available for external sessions + * @throws DatabaseOperationException If an error occurs while checking the database for session information. + * @throws StandardRpcException If an error occurs while checking the database for session information. */ private static function getExternalMethods(ClientRequest $clientRequest): array { @@ -410,6 +413,7 @@ * @param ClientRequest $clientRequest The client request for which the authentication methods are determined. * @return array The list of available authentication methods as an array of constants. * @throws DatabaseOperationException If an error occurs while checking the database for authentication methods. + * @throws StandardRpcException */ private static function getAuthenticationMethods(ClientRequest $clientRequest): array { From 83a47ddd5ec77d4e2ce9f789b3e8865174195971 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 30 Jan 2025 19:51:37 -0500 Subject: [PATCH 293/420] Updated method AddressBookGetContact to return a full standard contact, improved standard contact object --- .../AddressBook/AddressBookGetContact.php | 4 +- src/Socialbox/Managers/ContactManager.php | 39 ++++- .../Database/ContactKnownKeyRecord.php | 57 +++++++ .../Objects/Standard/ContactRecord.php | 78 ++++++++- .../Objects/Standard/KnownSigningKey.php | 161 ++++++++++++++++++ 5 files changed, 333 insertions(+), 6 deletions(-) create mode 100644 src/Socialbox/Objects/Standard/KnownSigningKey.php diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContact.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContact.php index 756fc37..a51e971 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContact.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContact.php @@ -43,13 +43,11 @@ return $rpcRequest->produceError(StandardError::NOT_FOUND, 'Contact does not exist'); } - $contact = ContactManager::getContact($request->getPeer(), $address); + $rpcRequest->produceResponse(ContactManager::getStandardContact($request->getPeer(), $address)); } catch(DatabaseOperationException $e) { throw new StandardRpcException('Failed to get contact', StandardError::INTERNAL_SERVER_ERROR, $e); } - - return $rpcRequest->produceResponse($contact->toStandard()); } } \ No newline at end of file diff --git a/src/Socialbox/Managers/ContactManager.php b/src/Socialbox/Managers/ContactManager.php index 4bd00e1..906fc74 100644 --- a/src/Socialbox/Managers/ContactManager.php +++ b/src/Socialbox/Managers/ContactManager.php @@ -8,10 +8,13 @@ use Socialbox\Classes\Database; use Socialbox\Enums\Types\ContactRelationshipType; use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Objects\Database\ContactDatabaseRecord; use Socialbox\Objects\Database\ContactKnownKeyRecord; use Socialbox\Objects\PeerAddress; + use Socialbox\Objects\Standard\ContactRecord; use Socialbox\Objects\Standard\SigningKey; + use Socialbox\Socialbox; class ContactManager { @@ -173,7 +176,7 @@ } /** - * Updates the relationship type of a contact associated with a specific peer. + * Updates the relationship type of contact associated with a specific peer. * * @param string $peerUuid The unique identifier for the peer whose contact relationship is to be updated. * @param string|PeerAddress $contactAddress The address of the contact to update. Can be provided as a string or an instance of PeerAddress. @@ -476,4 +479,38 @@ throw new DatabaseOperationException('Failed to get the number of signing keys for a contact from the database', $e); } } + + /** + * Returns a standard contact record for a given peer UUID and contact address. + * + * @param string $peerUuid The unique identifier of the peer. + * @param string|PeerAddress $contactAddress The contact's address, either as a string or a PeerAddress instance. + * @return ContactRecord|null The standard contact record if found, or null if no matching contact exists. + * @throws DatabaseOperationException If the database query fails. + */ + public static function getStandardContact(string $peerUuid, string|PeerAddress $contactAddress): ?ContactRecord + { + $contact = self::getContact($peerUuid, $contactAddress); + if($contact === null) + { + return null; + } + + try + { + $peer = Socialbox::resolvePeer($contactAddress); + } + catch (StandardRpcException $e) + { + $peer = null; + } + + return new ContactRecord([ + 'address' => $contact->getContactPeerAddress(), + 'peer' => $peer, + 'relationship' => $contact->getRelationship(), + 'known_keys' => self::contactGetSigningKeys($contact), + 'added_timestamp' => $contact->getCreated()->getTimestamp() + ]); + } } \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/ContactKnownKeyRecord.php b/src/Socialbox/Objects/Database/ContactKnownKeyRecord.php index 1dd8de4..ca2d075 100644 --- a/src/Socialbox/Objects/Database/ContactKnownKeyRecord.php +++ b/src/Socialbox/Objects/Database/ContactKnownKeyRecord.php @@ -5,6 +5,7 @@ use DateTime; use InvalidArgumentException; use Socialbox\Interfaces\SerializableInterface; + use Socialbox\Objects\Standard\KnownSigningKey; class ContactKnownKeyRecord implements SerializableInterface { @@ -16,6 +17,12 @@ private DateTime $created; private DateTime $trustedOn; + /** + * Constructs a new instance with the provided parameters. + * + * @param array $data The array of data to use for the object. + * @throws \DateMalformedStringException If the date string is malformed. + */ public function __construct(array $data) { $this->contactUuid = $data['contact_uuid']; @@ -96,36 +103,71 @@ } } + /** + * Gets the contact UUID. + * + * @return string The contact UUID. + */ public function getContactUuid(): string { return $this->contactUuid; } + /** + * Gets the signature UUID. + * + * @return string The signature UUID. + */ public function getSignatureUuid(): string { return $this->signatureUuid; } + /** + * Gets the signature name. + * + * @return string The signature name. + */ public function getSignatureName(): string { return $this->signatureName; } + /** + * Gets the signature key. + * + * @return string The signature key. + */ public function getSignatureKey(): string { return $this->signatureKey; } + /** + * Gets the expiration date. + * + * @return DateTime|null The expiration date. + */ public function getExpires(): ?DateTime { return $this->expires; } + /** + * Gets the creation date. + * + * @return DateTime The creation date. + */ public function getCreated(): DateTime { return $this->created; } + /** + * Gets the trusted on date. + * + * @return DateTime The trusted on date. + */ public function getTrustedOn(): DateTime { return $this->trustedOn; @@ -139,6 +181,21 @@ return new self($data); } + /** + * @inheritDoc + */ + public function toStandard(): KnownSigningKey + { + return new KnownSigningKey([ + 'uuid' => $this->signatureUuid, + 'name' => $this->signatureName, + 'public_key' => $this->signatureKey, + 'expires' => $this->expires?->getTimestamp(), + 'created' => $this->created->getTimestamp(), + 'trusted_on' => $this->trustedOn->getTimestamp() + ]); + } + /** * @inheritDoc */ diff --git a/src/Socialbox/Objects/Standard/ContactRecord.php b/src/Socialbox/Objects/Standard/ContactRecord.php index 919efb0..841ef9f 100644 --- a/src/Socialbox/Objects/Standard/ContactRecord.php +++ b/src/Socialbox/Objects/Standard/ContactRecord.php @@ -2,6 +2,7 @@ namespace Socialbox\Objects\Standard; + use InvalidArgumentException; use Socialbox\Enums\Types\ContactRelationshipType; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\PeerAddress; @@ -9,7 +10,12 @@ class ContactRecord implements SerializableInterface { private PeerAddress $address; + private ?Peer $peer; private ContactRelationshipType $relationship; + /** + * @var KnownSigningKey[] + */ + private array $knownKeys; private int $addedTimestamp; /** @@ -20,7 +26,53 @@ public function __construct(array $data) { $this->address = PeerAddress::fromAddress($data['address']); - $this->relationship = ContactRelationshipType::tryFrom($data['relationship']) ?? ContactRelationshipType::MUTUAL; + if(is_array($data['peer'])) + { + $this->peer = Peer::fromArray($data['peer']); + } + elseif($data['peer'] instanceof Peer) + { + $this->peer = $data['peer']; + } + elseif(is_null($data['peer'])) + { + $this->peer = null; + } + else + { + throw new InvalidArgumentException('Invalid peer data'); + } + + if($data['relationship'] instanceof ContactRelationshipType) + { + $this->relationship = $data['relationship']; + } + elseif(is_string($data['relationship'])) + { + $this->relationship = ContactRelationshipType::tryFrom($data['relationship']) ?? ContactRelationshipType::MUTUAL; + } + else + { + throw new InvalidArgumentException('Invalid relationship data'); + } + + $this->knownKeys = []; + + foreach($data['known_keys'] as $key) + { + if(is_array($key)) + { + $this->knownKeys[] = KnownSigningKey::fromArray($key); + } + elseif($key instanceof KnownSigningKey) + { + $this->knownKeys[] = $key; + } + else + { + throw new InvalidArgumentException('Invalid known key data'); + } + } $this->addedTimestamp = $data['added_timestamp']; } @@ -34,6 +86,16 @@ return $this->address; } + /** + * Retrieves the peer of the contact. + * + * @return Peer|null Returns the peer of the contact. If the peer is not known, null is returned. + */ + public function getPeer(): ?Peer + { + return $this->peer; + } + /** * Retrieves the relationship of the contact. * @@ -44,6 +106,16 @@ return $this->relationship; } + /** + * Retrieves the known keys of the contact. + * + * @return KnownSigningKey[] Returns the known keys of the contact. + */ + public function getKnownKeys(): array + { + return $this->knownKeys; + } + /** * Retrieves the timestamp when the contact was added. * @@ -57,7 +129,7 @@ /** * @inheritDoc */ - public static function fromArray(array $data): object + public static function fromArray(array $data): ContactRecord { return new self($data); } @@ -69,7 +141,9 @@ { return [ 'address' => $this->address->getAddress(), + 'peer' => $this->peer?->toArray(), 'relationship' => $this->relationship->value, + 'known_keys' => array_map(function($key) {return $key->toArray();}, $this->knownKeys), 'added_timestamp' => $this->addedTimestamp ]; } diff --git a/src/Socialbox/Objects/Standard/KnownSigningKey.php b/src/Socialbox/Objects/Standard/KnownSigningKey.php new file mode 100644 index 0000000..b85b6be --- /dev/null +++ b/src/Socialbox/Objects/Standard/KnownSigningKey.php @@ -0,0 +1,161 @@ +uuid = $data['uuid']; + $this->name = $data['name']; + $this->publicKey = $data['public_key']; + + if(is_int($data['expires'])) + { + $this->expires = $data['expires']; + } + elseif($data['expires'] instanceof DateTime) + { + $this->expires = $data['expires']->getTimestamp(); + } + else + { + throw new InvalidArgumentException('Invalid expires value'); + } + + if(is_int($data['created'])) + { + $this->created = $data['created']; + } + elseif($data['created'] instanceof DateTime) + { + $this->created = $data['created']->getTimestamp(); + } + else + { + throw new InvalidArgumentException('Invalid created value'); + } + + if(is_int($data['trusted_on'])) + { + $this->trustedOn = $data['trusted_on']; + } + elseif($data['trusted_on'] instanceof DateTime) + { + $this->trustedOn = $data['trusted_on']->getTimestamp(); + } + else + { + throw new InvalidArgumentException('Invalid trusted_on value'); + } + } + + /** + * Retrieves the UUID associated with this instance. + * + * @return string The UUID as a string. + */ + public function getUuid(): string + { + return $this->uuid; + } + + /** + * Retrieves the name associated with this instance. + * + * @return string|null The name as a string, or null if not set. + */ + public function getName(): ?string + { + return $this->name; + } + + /** + * + * Retrieves the public key. + * + * @return string The public key. + */ + public function getPublicKey(): string + { + return $this->publicKey; + } + + /** + * Retrieves the expiration time. + * + * @return int The expiration time as an integer. + */ + public function getExpires(): int + { + return $this->expires; + } + + /** + * + * @return int The timestamp representing the creation time. + */ + public function getCreated(): int + { + return $this->created; + } + + /** + * Retrieves the timestamp representing the time the key was trusted. + * + * @return int The timestamp representing the time the key was trusted. + */ + public function getTrustedOn(): int + { + return $this->trustedOn; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): KnownSigningKey + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'uuid' => $this->uuid, + 'name' => $this->name, + 'public_key' => $this->publicKey, + 'expires' => $this->expires, + 'created' => $this->created, + 'trusted_on' => $this->trustedOn + ]; + } + } \ No newline at end of file From 57d5528b31140c5c49f2e162765ce4b5e6320f21 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 30 Jan 2025 19:54:27 -0500 Subject: [PATCH 294/420] Updated AddressBookGetContacts to return standard contact objects --- .../AddressBook/AddressBookGetContacts.php | 5 +- src/Socialbox/Managers/ContactManager.php | 48 +++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContacts.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContacts.php index 7b583b6..4849487 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContacts.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContacts.php @@ -46,14 +46,11 @@ try { - $contacts = ContactManager::getContacts($request->getPeer(), $limit, $page); + return $rpcRequest->produceResponse(ContactManager::getStandardContacts($request->getPeer(), $limit, $page)); } catch(DatabaseOperationException $e) { throw new StandardRpcException('Failed to get contacts', StandardError::INTERNAL_SERVER_ERROR, $e); } - - - return $rpcRequest->produceResponse(array_map(function($contact) {return $contact->toStandard();}, $contacts)); } } \ No newline at end of file diff --git a/src/Socialbox/Managers/ContactManager.php b/src/Socialbox/Managers/ContactManager.php index 906fc74..bd183af 100644 --- a/src/Socialbox/Managers/ContactManager.php +++ b/src/Socialbox/Managers/ContactManager.php @@ -284,6 +284,54 @@ return $contacts; } + /** + * Retrieves a list of standard contacts associated with a specific peer. + * + * @param string $peerUuid The unique identifier for the peer whose contacts are to be retrieved. + * @param int $limit The maximum number of contacts to retrieve per page. Defaults to 100. + * @param int $page The page number to retrieve. Defaults to 1. + * @return ContactRecord[] An array of ContactRecord instances representing the contacts for the given peer. + * @throws DatabaseOperationException If the database query fails. + */ + public static function getStandardContacts(string $peerUuid, int $limit=100, int $page=1): array + { + if ($page < 1) + { + $page = 1; + } + + if ($limit < 1) + { + $limit = 1; + } + + $contacts = []; + + try + { + $statement = Database::getConnection()->prepare("SELECT uuid FROM contacts WHERE peer_uuid=:peer ORDER BY created DESC LIMIT :limit OFFSET :offset"); + $offset = ($page - 1) * $limit; + $statement->bindParam(':peer', $peerUuid); + $statement->bindParam(':limit', $limit, PDO::PARAM_INT); + $statement->bindParam(':offset', $offset, PDO::PARAM_INT); + $statement->execute(); + + // Fetch results + $results = $statement->fetchAll(PDO::FETCH_ASSOC); + + // Convert results to ContactRecord instances + foreach ($results as $result) + { + $contacts[] = self::getStandardContact($peerUuid, $result['uuid']); + } + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to get contacts from the database', $e); + } + return $contacts; + } + /** * Adds a signing key to a contact in the database. * From c3129f89d689eb105e267c05391e9897a19324a2 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 30 Jan 2025 19:55:10 -0500 Subject: [PATCH 295/420] Updated return logic for AddressBookGetContact --- .../StandardMethods/AddressBook/AddressBookGetContact.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContact.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContact.php index a51e971..a8984a0 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContact.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContact.php @@ -40,7 +40,8 @@ { if(!ContactManager::isContact($request->getPeer(), $address)) { - return $rpcRequest->produceError(StandardError::NOT_FOUND, 'Contact does not exist'); + // Return empty response if the contact does not exist + return $rpcRequest->produceResponse(); } $rpcRequest->produceResponse(ContactManager::getStandardContact($request->getPeer(), $address)); From caf329903978de70f3d9fd7d0fc49330be9dff21 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 30 Jan 2025 19:56:12 -0500 Subject: [PATCH 296/420] Updated PhpDoc --- .../StandardMethods/AddressBook/AddressBookGetContact.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContact.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContact.php index a8984a0..7054cc8 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContact.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContact.php @@ -18,6 +18,8 @@ class AddressBookGetContact extends Method { /** + * Returns the contact information for the given peer address. + * * @inheritDoc */ public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface From 051ad11aba3611333b6c4acf1d0fb12fb9c49c6a Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 30 Jan 2025 19:56:25 -0500 Subject: [PATCH 297/420] Updated PhpDoc --- .../StandardMethods/AddressBook/AddressBookGetContacts.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContacts.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContacts.php index 4849487..fa3cf84 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContacts.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContacts.php @@ -16,6 +16,8 @@ class AddressBookGetContacts extends Method { /** + * Returns the contacts in the address book. + * * @inheritDoc */ public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface From e63e7eb0d795ab49a62da9ebcd2c621cedb5308c Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 30 Jan 2025 19:59:29 -0500 Subject: [PATCH 298/420] Added method AddressBookRevokeSignature --- .../AddressBookRevokeSignature.php | 89 +++++++++++++++++++ src/Socialbox/Managers/ContactManager.php | 20 +++++ 2 files changed, 109 insertions(+) create mode 100644 src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookRevokeSignature.php diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookRevokeSignature.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookRevokeSignature.php new file mode 100644 index 0000000..6053658 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookRevokeSignature.php @@ -0,0 +1,89 @@ +containsParameter('peer')) + { + throw new MissingRpcArgumentException('peer'); + } + + try + { + $address = PeerAddress::fromAddress($rpcRequest->getParameter('peer')); + } + catch(InvalidArgumentException $e) + { + throw new InvalidRpcArgumentException('peer', $e->getMessage()); + } + + if(!$rpcRequest->containsParameter('uuid')) + { + throw new MissingRpcArgumentException('uuid'); + } + + try + { + $uuid = Uuid::fromString($rpcRequest->getParameter('uuid')); + } + catch(InvalidArgumentException $e) + { + throw new InvalidRpcArgumentException('uuid', $e->getMessage()); + } + + try + { + // Check if the contact already exists + $peer = $request->getPeer(); + $contact = ContactManager::getContact($peer, $address); + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to check contact state with calling peer', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if($contact === null) + { + return $rpcRequest->produceResponse(false); + } + + try + { + if(!ContactManager::contactSigningKeyUuidExists($contact, $uuid)) + { + return $rpcRequest->produceResponse(false); + } + + ContactManager::removeContactSigningKey($contact, $uuid); + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to remove contact signature', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + // Return success + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file diff --git a/src/Socialbox/Managers/ContactManager.php b/src/Socialbox/Managers/ContactManager.php index bd183af..f05c42b 100644 --- a/src/Socialbox/Managers/ContactManager.php +++ b/src/Socialbox/Managers/ContactManager.php @@ -371,6 +371,26 @@ } } + public static function removeContactSigningKey(string|ContactDatabaseRecord $contactUuid, string $signatureUuid): void + { + if($contactUuid instanceof ContactDatabaseRecord) + { + $contactUuid = $contactUuid->getUuid(); + } + + try + { + $statement = Database::getConnection()->prepare('DELETE FROM contacts_known_keys WHERE contact_uuid=:contact_uuid AND signature_uuid=:signature_uuid'); + $statement->bindParam(':contact_uuid', $contactUuid); + $statement->bindParam(':signature_uuid', $signatureUuid); + $statement->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to remove a signing key from a contact in the database', $e); + } + } + /** * Determines if a signing key UUID exists for a contact in the database. * From 28cb7a0a880bd57545f26bae8f3f72c80dbfbb4d Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 30 Jan 2025 20:00:40 -0500 Subject: [PATCH 299/420] Removed unused import --- .../StandardMethods/AddressBook/AddressBookRevokeSignature.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookRevokeSignature.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookRevokeSignature.php index 6053658..3bb44fa 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookRevokeSignature.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookRevokeSignature.php @@ -4,7 +4,6 @@ use InvalidArgumentException; use Socialbox\Abstracts\Method; - use Socialbox\Classes\Configuration; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; From 0f67bb3337d412ba7bd227e3066c4e0f00210437 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 30 Jan 2025 21:37:36 -0500 Subject: [PATCH 300/420] Removed unused import --- .../StandardMethods/AddressBook/AddressBookRevokeSignature.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookRevokeSignature.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookRevokeSignature.php index 3bb44fa..609cda7 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookRevokeSignature.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookRevokeSignature.php @@ -14,7 +14,6 @@ use Socialbox\Objects\ClientRequest; use Socialbox\Objects\PeerAddress; use Socialbox\Objects\RpcRequest; - use Socialbox\Socialbox; use Symfony\Component\Uid\Uuid; class AddressBookRevokeSignature extends Method From 1f55538ad4984b08060d26cf27b126724386a2ec Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 31 Jan 2025 12:56:45 -0500 Subject: [PATCH 301/420] Improved exception handling --- .../StandardMethods/Core/ResolvePeer.php | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/Core/ResolvePeer.php b/src/Socialbox/Classes/StandardMethods/Core/ResolvePeer.php index ce0f60c..5c728e2 100644 --- a/src/Socialbox/Classes/StandardMethods/Core/ResolvePeer.php +++ b/src/Socialbox/Classes/StandardMethods/Core/ResolvePeer.php @@ -2,11 +2,11 @@ namespace Socialbox\Classes\StandardMethods\Core; - use Exception; use InvalidArgumentException; use Socialbox\Abstracts\Method; use Socialbox\Enums\ReservedUsernames; use Socialbox\Enums\StandardError; + use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\Standard\MissingRpcArgumentException; use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; @@ -40,20 +40,20 @@ } // Check if host is making the request & the identifier is not empty - $identifyAs = null; - if($request->getPeer()->getUsername() == ReservedUsernames::HOST && $request->getIdentifyAs() != null) + try { - $identifyAs = $request->getIdentifyAs(); + $identifyAs = null; + if ($request->getPeer()->getUsername() == ReservedUsernames::HOST && $request->getIdentifyAs() != null) + { + $identifyAs = $request->getIdentifyAs(); + } + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to retrieve peer information', StandardError::INTERNAL_SERVER_ERROR, $e); } // Resolve the peer using the server's peer resolver, this will resolve both internal peers and external peers - try - { - return $rpcRequest->produceResponse(Socialbox::resolvePeer($peerAddress, $identifyAs)); - } - catch(Exception $e) - { - throw new StandardRpcException(sprintf('There was an error while trying to resolve the peer %s: %s', $peerAddress, $e->getMessage()), StandardError::RESOLUTION_FAILED, $e); - } + return $rpcRequest->produceResponse(Socialbox::resolvePeer($peerAddress, $identifyAs)); } } \ No newline at end of file From 2ba814f9b495bbf6507325e626070c28ad6dde73 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 31 Jan 2025 13:05:31 -0500 Subject: [PATCH 302/420] Peer resolution for a contact isn't needed, reference is found in 'address' which could be resolved at a later time --- src/Socialbox/Managers/ContactManager.php | 10 ------- .../Objects/Standard/ContactRecord.php | 28 ------------------- 2 files changed, 38 deletions(-) diff --git a/src/Socialbox/Managers/ContactManager.php b/src/Socialbox/Managers/ContactManager.php index f05c42b..a8e1004 100644 --- a/src/Socialbox/Managers/ContactManager.php +++ b/src/Socialbox/Managers/ContactManager.php @@ -564,18 +564,8 @@ return null; } - try - { - $peer = Socialbox::resolvePeer($contactAddress); - } - catch (StandardRpcException $e) - { - $peer = null; - } - return new ContactRecord([ 'address' => $contact->getContactPeerAddress(), - 'peer' => $peer, 'relationship' => $contact->getRelationship(), 'known_keys' => self::contactGetSigningKeys($contact), 'added_timestamp' => $contact->getCreated()->getTimestamp() diff --git a/src/Socialbox/Objects/Standard/ContactRecord.php b/src/Socialbox/Objects/Standard/ContactRecord.php index 841ef9f..8b4cabd 100644 --- a/src/Socialbox/Objects/Standard/ContactRecord.php +++ b/src/Socialbox/Objects/Standard/ContactRecord.php @@ -10,7 +10,6 @@ class ContactRecord implements SerializableInterface { private PeerAddress $address; - private ?Peer $peer; private ContactRelationshipType $relationship; /** * @var KnownSigningKey[] @@ -26,22 +25,6 @@ public function __construct(array $data) { $this->address = PeerAddress::fromAddress($data['address']); - if(is_array($data['peer'])) - { - $this->peer = Peer::fromArray($data['peer']); - } - elseif($data['peer'] instanceof Peer) - { - $this->peer = $data['peer']; - } - elseif(is_null($data['peer'])) - { - $this->peer = null; - } - else - { - throw new InvalidArgumentException('Invalid peer data'); - } if($data['relationship'] instanceof ContactRelationshipType) { @@ -86,16 +69,6 @@ return $this->address; } - /** - * Retrieves the peer of the contact. - * - * @return Peer|null Returns the peer of the contact. If the peer is not known, null is returned. - */ - public function getPeer(): ?Peer - { - return $this->peer; - } - /** * Retrieves the relationship of the contact. * @@ -141,7 +114,6 @@ { return [ 'address' => $this->address->getAddress(), - 'peer' => $this->peer?->toArray(), 'relationship' => $this->relationship->value, 'known_keys' => array_map(function($key) {return $key->toArray();}, $this->knownKeys), 'added_timestamp' => $this->addedTimestamp From 268630226ee5938915e10648e1d2d6a532224670 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 31 Jan 2025 13:05:50 -0500 Subject: [PATCH 303/420] Removed unused imports --- src/Socialbox/Managers/ContactManager.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Socialbox/Managers/ContactManager.php b/src/Socialbox/Managers/ContactManager.php index a8e1004..51b06ae 100644 --- a/src/Socialbox/Managers/ContactManager.php +++ b/src/Socialbox/Managers/ContactManager.php @@ -8,13 +8,11 @@ use Socialbox\Classes\Database; use Socialbox\Enums\Types\ContactRelationshipType; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Objects\Database\ContactDatabaseRecord; use Socialbox\Objects\Database\ContactKnownKeyRecord; use Socialbox\Objects\PeerAddress; use Socialbox\Objects\Standard\ContactRecord; use Socialbox\Objects\Standard\SigningKey; - use Socialbox\Socialbox; class ContactManager { From 55137a93f2bbafcd21a93df1f87b9e46a01c0cb3 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 31 Jan 2025 13:15:34 -0500 Subject: [PATCH 304/420] Added better exception handling --- .../ServerDocuments/AcceptCommunityGuidelines.php | 12 ++++++++++-- .../ServerDocuments/AcceptPrivacyPolicy.php | 13 +++++++++++-- .../ServerDocuments/AcceptTermsOfService.php | 13 +++++++++++-- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/ServerDocuments/AcceptCommunityGuidelines.php b/src/Socialbox/Classes/StandardMethods/ServerDocuments/AcceptCommunityGuidelines.php index bb40cd6..66dbb6e 100644 --- a/src/Socialbox/Classes/StandardMethods/ServerDocuments/AcceptCommunityGuidelines.php +++ b/src/Socialbox/Classes/StandardMethods/ServerDocuments/AcceptCommunityGuidelines.php @@ -6,6 +6,7 @@ use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\SessionManager; use Socialbox\Objects\ClientRequest; @@ -21,7 +22,14 @@ */ public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { - $session = $request->getSession(); + try + { + $session = $request->getSession(); + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to retrieve session', StandardError::INTERNAL_SERVER_ERROR, $e); + } if(!$session->flagExists(SessionFlags::VER_COMMUNITY_GUIDELINES)) { return $rpcRequest->produceError(StandardError::FORBIDDEN, 'Community guidelines has already been accepted'); @@ -34,7 +42,7 @@ } catch (DatabaseOperationException $e) { - return $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to update session flow', StandardError::INTERNAL_SERVER_ERROR, $e); } return $rpcRequest->produceResponse(true); diff --git a/src/Socialbox/Classes/StandardMethods/ServerDocuments/AcceptPrivacyPolicy.php b/src/Socialbox/Classes/StandardMethods/ServerDocuments/AcceptPrivacyPolicy.php index 72b1d86..681b06f 100644 --- a/src/Socialbox/Classes/StandardMethods/ServerDocuments/AcceptPrivacyPolicy.php +++ b/src/Socialbox/Classes/StandardMethods/ServerDocuments/AcceptPrivacyPolicy.php @@ -6,6 +6,7 @@ use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\SessionManager; use Socialbox\Objects\ClientRequest; @@ -20,7 +21,15 @@ */ public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { - $session = $request->getSession(); + try + { + $session = $request->getSession(); + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to retrieve session', StandardError::INTERNAL_SERVER_ERROR, $e); + } + if(!$session->flagExists(SessionFlags::VER_PRIVACY_POLICY)) { return $rpcRequest->produceError(StandardError::FORBIDDEN, 'Privacy policy has already been accepted'); @@ -33,7 +42,7 @@ } catch (DatabaseOperationException $e) { - return $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to update session flow', StandardError::INTERNAL_SERVER_ERROR, $e); } return $rpcRequest->produceResponse(true); diff --git a/src/Socialbox/Classes/StandardMethods/ServerDocuments/AcceptTermsOfService.php b/src/Socialbox/Classes/StandardMethods/ServerDocuments/AcceptTermsOfService.php index 369d427..3c11fa1 100644 --- a/src/Socialbox/Classes/StandardMethods/ServerDocuments/AcceptTermsOfService.php +++ b/src/Socialbox/Classes/StandardMethods/ServerDocuments/AcceptTermsOfService.php @@ -6,6 +6,7 @@ use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\SessionManager; use Socialbox\Objects\ClientRequest; @@ -20,7 +21,15 @@ */ public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { - $session = $request->getSession(); + try + { + $session = $request->getSession(); + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to retrieve session', StandardError::INTERNAL_SERVER_ERROR, $e); + } + if(!$session->flagExists(SessionFlags::VER_TERMS_OF_SERVICE)) { return $rpcRequest->produceError(StandardError::FORBIDDEN, 'Terms of service has already been accepted'); @@ -33,7 +42,7 @@ } catch (DatabaseOperationException $e) { - return $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to update session flow', StandardError::INTERNAL_SERVER_ERROR, $e); } return $rpcRequest->produceResponse(true); From 0ed6a36d50c2084174df213e7c9904537cc1547e Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 31 Jan 2025 13:28:28 -0500 Subject: [PATCH 305/420] Improved InvalidRpcArgumentException handling --- .../AddressBook/AddressBookAddContact.php | 4 ++-- .../AddressBook/AddressBookContactExists.php | 2 +- .../AddressBook/AddressBookDeleteContact.php | 2 +- .../AddressBook/AddressBookGetContact.php | 2 +- .../AddressBook/AddressBookRevokeSignature.php | 4 ++-- .../AddressBook/AddressBookTrustSignature.php | 4 ++-- .../AddressBookUpdateRelationship.php | 4 ++-- .../Settings/SettingsAddInformationField.php | 18 +++++++++++------- .../Settings/SettingsAddSignature.php | 4 +--- .../SettingsDeleteInformationField.php | 6 ++++-- .../Standard/InvalidRpcArgumentException.php | 18 ++++++++++++++++-- 11 files changed, 43 insertions(+), 25 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php index 2f4a88e..7af29fd 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php @@ -38,7 +38,7 @@ } catch(InvalidArgumentException $e) { - throw new InvalidRpcArgumentException('peer', $e->getMessage()); + throw new InvalidRpcArgumentException('peer', $e); } if($rpcRequest->containsParameter('relationship')) @@ -46,7 +46,7 @@ $relationship = ContactRelationshipType::tryFrom(strtoupper($rpcRequest->getParameter('relationship'))); if($relationship === null) { - throw new InvalidRpcArgumentException('peer', 'Invalid relationship type'); + throw new InvalidRpcArgumentException('relationship'); } } else diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookContactExists.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookContactExists.php index 087baa2..9758ce2 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookContactExists.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookContactExists.php @@ -35,7 +35,7 @@ } catch(InvalidArgumentException $e) { - throw new InvalidRpcArgumentException('peer', $e->getMessage()); + throw new InvalidRpcArgumentException('peer', $e); } try diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookDeleteContact.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookDeleteContact.php index 1f27565..b891b92 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookDeleteContact.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookDeleteContact.php @@ -36,7 +36,7 @@ } catch(InvalidArgumentException $e) { - throw new InvalidRpcArgumentException('peer', $e->getMessage()); + throw new InvalidRpcArgumentException('peer', $e); } try diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContact.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContact.php index 7054cc8..5443d29 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContact.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContact.php @@ -35,7 +35,7 @@ } catch(InvalidArgumentException $e) { - throw new InvalidRpcArgumentException('peer', $e->getMessage()); + throw new InvalidRpcArgumentException('peer', $e); } try diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookRevokeSignature.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookRevokeSignature.php index 609cda7..52d00f1 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookRevokeSignature.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookRevokeSignature.php @@ -34,7 +34,7 @@ } catch(InvalidArgumentException $e) { - throw new InvalidRpcArgumentException('peer', $e->getMessage()); + throw new InvalidRpcArgumentException('peer', $e); } if(!$rpcRequest->containsParameter('uuid')) @@ -48,7 +48,7 @@ } catch(InvalidArgumentException $e) { - throw new InvalidRpcArgumentException('uuid', $e->getMessage()); + throw new InvalidRpcArgumentException('uuid', $e); } try diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookTrustSignature.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookTrustSignature.php index c04a2a9..673d5b0 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookTrustSignature.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookTrustSignature.php @@ -36,7 +36,7 @@ } catch(InvalidArgumentException $e) { - throw new InvalidRpcArgumentException('peer', $e->getMessage()); + throw new InvalidRpcArgumentException('peer', $e); } if(!$rpcRequest->containsParameter('uuid')) @@ -50,7 +50,7 @@ } catch(InvalidArgumentException $e) { - throw new InvalidRpcArgumentException('uuid', $e->getMessage()); + throw new InvalidRpcArgumentException('uuid', $e); } $signingKey = Socialbox::resolvePeerSignature($address, $uuid); diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookUpdateRelationship.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookUpdateRelationship.php index cf217c0..7e1898a 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookUpdateRelationship.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookUpdateRelationship.php @@ -34,7 +34,7 @@ } catch(InvalidArgumentException $e) { - throw new InvalidRpcArgumentException('peer', 'Invalid peer address'); + throw new InvalidRpcArgumentException('peer'); } if(!$rpcRequest->containsParameter('relationship')) @@ -44,7 +44,7 @@ $relationship = ContactRelationshipType::tryFrom(strtoupper($rpcRequest->getParameter('relationship'))); if($relationship === null) { - throw new InvalidRpcArgumentException('relationship', 'Invalid relationship type'); + throw new InvalidRpcArgumentException('relationship'); } try diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddInformationField.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddInformationField.php index 88145ca..30b5978 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddInformationField.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddInformationField.php @@ -9,6 +9,8 @@ use Socialbox\Enums\StandardError; use Socialbox\Enums\Types\InformationFieldName; use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; + use Socialbox\Exceptions\Standard\MissingRpcArgumentException; use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PeerInformationManager; @@ -20,29 +22,30 @@ { /** * @inheritDoc + * @noinspection DuplicatedCode */ public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { // Field parameter is required if(!$rpcRequest->containsParameter('field')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The required field parameter is missing'); + throw new MissingRpcArgumentException('field'); } $fieldName = InformationFieldName::tryFrom(strtoupper($rpcRequest->getParameter('field'))); if($fieldName === null) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The provided field parameter is invalid'); + throw new InvalidRpcArgumentException('field'); } // Value parameter is required if(!$rpcRequest->containsParameter('value')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The required value parameter is missing'); + throw new MissingRpcArgumentException('value'); } $value = $rpcRequest->getParameter('value'); if(!$fieldName->validate($value)) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The provided value parameter is invalid'); + throw new InvalidRpcArgumentException('value'); } // Privacy parameter is optional @@ -52,7 +55,7 @@ $privacy = PrivacyState::tryFrom(strtoupper($rpcRequest->getParameter('privacy'))); if($privacy === null) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The provided privacy parameter is invalid'); + throw new InvalidRpcArgumentException('privacy'); } } @@ -62,14 +65,15 @@ } catch (DatabaseOperationException $e) { - throw new StandardRpcException('Failed to retrieve current peer information', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to retrieve peer information', StandardError::INTERNAL_SERVER_ERROR, $e); } try { if (PeerInformationManager::fieldExists($peer, $fieldName)) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The provided field parameter is already registered, use settingsUpdateInformationField or settingsUpdateInformationPrivacy instead'); + // Return False, because the field already exists + return $rpcRequest->produceResponse(false); } PeerInformationManager::addField($peer, $fieldName, $value, $privacy); diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddSignature.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddSignature.php index 5c7fb3c..5b3de2a 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddSignature.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddSignature.php @@ -31,7 +31,6 @@ { $expires = (int)$rpcRequest->getParameter('expires'); } - if(!$rpcRequest->containsParameter('name')) { throw new MissingRpcArgumentException('name'); @@ -43,10 +42,9 @@ $name = $rpcRequest->getParameter('name'); } - $peerUuid = $request->getPeer()->getUuid(); - try { + $peerUuid = $request->getPeer()->getUuid(); if(SigningKeysManager::getSigningKeyCount($peerUuid) >= Configuration::getPoliciesConfiguration()->getMaxSigningKeys()) { return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The maximum number of signing keys has been reached'); diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteInformationField.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteInformationField.php index 32e095e..a337efa 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteInformationField.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteInformationField.php @@ -7,6 +7,8 @@ use Socialbox\Enums\StandardError; use Socialbox\Enums\Types\InformationFieldName; use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; + use Socialbox\Exceptions\Standard\MissingRpcArgumentException; use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PeerInformationManager; @@ -23,12 +25,12 @@ // Field parameter is required if(!$rpcRequest->containsParameter('field')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The required field parameter is missing'); + throw new MissingRpcArgumentException('field'); } $fieldName = InformationFieldName::tryFrom(strtoupper($rpcRequest->getParameter('field'))); if($fieldName === null) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The provided field parameter is invalid'); + throw new InvalidRpcArgumentException('field'); } try diff --git a/src/Socialbox/Exceptions/Standard/InvalidRpcArgumentException.php b/src/Socialbox/Exceptions/Standard/InvalidRpcArgumentException.php index 45b8673..33103fe 100644 --- a/src/Socialbox/Exceptions/Standard/InvalidRpcArgumentException.php +++ b/src/Socialbox/Exceptions/Standard/InvalidRpcArgumentException.php @@ -2,6 +2,8 @@ namespace Socialbox\Exceptions\Standard; + use Exception; + use InvalidArgumentException; use Socialbox\Enums\StandardError; class InvalidRpcArgumentException extends StandardRpcException @@ -10,10 +12,22 @@ * Thrown when a required parameter is missing * * @param string $parameterName The name of the parameter that is missing - * @param string $reason The reason why the parameter is invalid + * @param string|Exception|null $reason The reason why the parameter is invalid can be a string or an exception or null */ - public function __construct(string $parameterName, string $reason) + public function __construct(string $parameterName, null|string|Exception $reason=null) { + if(is_null($reason)) + { + parent::__construct(sprintf('Invalid parameter %s', $parameterName), StandardError::RPC_INVALID_ARGUMENTS); + return; + } + + if($reason instanceof InvalidArgumentException) + { + parent::__construct(sprintf('Invalid parameter %s: %s', $parameterName, $reason->getMessage()), StandardError::RPC_INVALID_ARGUMENTS, $reason); + return; + } + parent::__construct(sprintf('Invalid parameter %s: %s', $parameterName, $reason), StandardError::RPC_INVALID_ARGUMENTS); } } \ No newline at end of file From 4c28112e3bed4f507a83f1ece2f8e8d789437fd4 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 31 Jan 2025 13:29:18 -0500 Subject: [PATCH 306/420] Changed type Exception to Throwable --- .../Exceptions/Standard/InvalidRpcArgumentException.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Socialbox/Exceptions/Standard/InvalidRpcArgumentException.php b/src/Socialbox/Exceptions/Standard/InvalidRpcArgumentException.php index 33103fe..f6274a8 100644 --- a/src/Socialbox/Exceptions/Standard/InvalidRpcArgumentException.php +++ b/src/Socialbox/Exceptions/Standard/InvalidRpcArgumentException.php @@ -2,9 +2,9 @@ namespace Socialbox\Exceptions\Standard; - use Exception; use InvalidArgumentException; use Socialbox\Enums\StandardError; + use Throwable; class InvalidRpcArgumentException extends StandardRpcException { @@ -12,9 +12,9 @@ * Thrown when a required parameter is missing * * @param string $parameterName The name of the parameter that is missing - * @param string|Exception|null $reason The reason why the parameter is invalid can be a string or an exception or null + * @param string|Throwable|null $reason The reason why the parameter is invalid can be a string or an exception or null */ - public function __construct(string $parameterName, null|string|Exception $reason=null) + public function __construct(string $parameterName, null|string|Throwable $reason=null) { if(is_null($reason)) { From aa70a83c2695e1543331c651fb9edf12607528db Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 31 Jan 2025 13:39:32 -0500 Subject: [PATCH 307/420] Improved return logic --- .../Settings/SettingsDeleteInformationField.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteInformationField.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteInformationField.php index a337efa..f5f15b8 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteInformationField.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteInformationField.php @@ -37,7 +37,7 @@ { if(!PeerInformationManager::fieldExists($request->getPeer(), $fieldName)) { - return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The information field does not exist'); + return $rpcRequest->produceResponse(false); } } catch(DatabaseOperationException $e) @@ -45,6 +45,7 @@ throw new StandardRpcException('Failed to check if the information field exists', StandardError::INTERNAL_SERVER_ERROR, $e); } + // Prevent the removal of required fields switch($fieldName) { case InformationFieldName::DISPLAY_NAME: From 8a86bd04a69ab76a7d38c755f0a69d2b5a30e774 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 31 Jan 2025 15:03:52 -0500 Subject: [PATCH 308/420] Improved exception handling --- .../Settings/SettingsDeleteOtp.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteOtp.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteOtp.php index c6d504c..686ae8f 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteOtp.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteOtp.php @@ -8,6 +8,7 @@ use Socialbox\Classes\Cryptography; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\OneTimePasswordManager; @@ -24,16 +25,15 @@ { if(Configuration::getRegistrationConfiguration()->isOtpRequired()) { - return $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, 'One Time Password is required for this server'); + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'One Time Password is required for this server'); } - $peer = $request->getPeer(); - try { + $peer = $request->getPeer(); if (!OneTimePasswordManager::usesOtp($peer->getUuid())) { - return $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, "Cannot delete One Time Password when none is set"); + return $rpcRequest->produceResponse(false); } } catch (DatabaseOperationException $e) @@ -55,12 +55,12 @@ { if(!$rpcRequest->containsParameter('password')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'When a password is set, the current password must be provided to delete an OTP'); + throw new InvalidRpcArgumentException('password', 'When a password is set, the current password must be provided to delete an OTP'); } if(!Cryptography::validateSha512($rpcRequest->getParameter('password'))) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The provided password is not a valid SHA-512 hash'); + throw new InvalidRpcArgumentException('password', 'The provided password is not a valid SHA-512 hash'); } try @@ -70,7 +70,7 @@ return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The provided password is incorrect'); } } - catch(Exception $e) + catch(DatabaseOperationException $e) { throw new StandardRpcException('Failed to verify password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } @@ -81,7 +81,7 @@ // Delete the OTP OneTimePasswordManager::deleteOtp($peer); } - catch(Exception $e) + catch(DatabaseOperationException $e) { throw new StandardRpcException('Failed to set password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } From 3b18077e76982d9c8ba29aed63a92d031e17dd62 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 31 Jan 2025 15:04:22 -0500 Subject: [PATCH 309/420] Improved exception handling --- .../Classes/StandardMethods/Settings/SettingsDeleteOtp.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteOtp.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteOtp.php index 686ae8f..1f9effa 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteOtp.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteOtp.php @@ -7,6 +7,7 @@ use Socialbox\Classes\Configuration; use Socialbox\Classes\Cryptography; use Socialbox\Enums\StandardError; + use Socialbox\Exceptions\CryptographyException; use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; use Socialbox\Exceptions\Standard\StandardRpcException; @@ -70,6 +71,10 @@ return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The provided password is incorrect'); } } + catch(CryptographyException $e) + { + throw new StandardRpcException($e->getMessage(), StandardError::CRYPTOGRAPHIC_ERROR, $e); + } catch(DatabaseOperationException $e) { throw new StandardRpcException('Failed to verify password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); From b813d38ff9f525e972977afa57af5667c3518152 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 31 Jan 2025 15:04:36 -0500 Subject: [PATCH 310/420] Removed unused import --- .../Classes/StandardMethods/Settings/SettingsDeleteOtp.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteOtp.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteOtp.php index 1f9effa..55bef0a 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteOtp.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteOtp.php @@ -2,7 +2,6 @@ namespace Socialbox\Classes\StandardMethods\Settings; - use Exception; use Socialbox\Abstracts\Method; use Socialbox\Classes\Configuration; use Socialbox\Classes\Cryptography; From 148a65c2f785b63715d3c8258f0efc3308dab735 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 31 Jan 2025 15:07:24 -0500 Subject: [PATCH 311/420] Improved exception handling --- .../Settings/SettingsDeletePassword.php | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeletePassword.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeletePassword.php index caec050..06b78e2 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeletePassword.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeletePassword.php @@ -2,12 +2,14 @@ namespace Socialbox\Classes\StandardMethods\Settings; - use Exception; use Socialbox\Abstracts\Method; use Socialbox\Classes\Configuration; use Socialbox\Classes\Cryptography; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\CryptographyException; + use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; + use Socialbox\Exceptions\Standard\MissingRpcArgumentException; use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PasswordManager; @@ -32,20 +34,21 @@ // Check if the password parameter is present if(!$rpcRequest->containsParameter('password')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'password' parameter"); + throw new MissingRpcArgumentException('password'); } // Validate the password parameter if(!Cryptography::validateSha512($rpcRequest->getParameter('password'))) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Invalid 'password' parameter, must be a valid SHA-512 hash"); + throw new InvalidRpcArgumentException('password', 'Must be a valid SHA-512 hash'); } - // Get the peer - $peer = $request->getPeer(); try { + // Get the peer + $peer = $request->getPeer(); + // Check if the password is set if (!PasswordManager::usesPassword($peer->getUuid())) { @@ -55,7 +58,7 @@ // Verify the existing password before deleting it if (!PasswordManager::verifyPassword($peer->getUuid(), $rpcRequest->getParameter('password'))) { - return $rpcRequest->produceError(StandardError::FORBIDDEN, "Failed to delete password due to incorrect existing password"); + return $rpcRequest->produceResponse(false); } // Delete the password @@ -65,9 +68,9 @@ { return $rpcRequest->produceError(StandardError::CRYPTOGRAPHIC_ERROR, 'Failed to delete password due to a cryptographic error'); } - catch (Exception $e) + catch (DatabaseOperationException $e) { - throw new StandardRpcException('Failed to check password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to delete password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } // Success From 7a39b0fd3583b739456b821ad565d8e0010d2f3c Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 31 Jan 2025 15:13:12 -0500 Subject: [PATCH 312/420] Renamed PeerRecord to PeerDatabaseRecord --- src/Socialbox/Managers/CaptchaManager.php | 26 +++++----- .../Managers/OneTimePasswordManager.php | 30 +++++------ src/Socialbox/Managers/PasswordManager.php | 32 ++++++------ .../Managers/PeerInformationManager.php | 50 +++++++++---------- .../Managers/RegisteredPeerManager.php | 46 ++++++++--------- src/Socialbox/Managers/SessionManager.php | 6 +-- src/Socialbox/Objects/ClientRequest.php | 6 +-- ...{PeerRecord.php => PeerDatabaseRecord.php} | 13 +---- 8 files changed, 99 insertions(+), 110 deletions(-) rename src/Socialbox/Objects/Database/{PeerRecord.php => PeerDatabaseRecord.php} (94%) diff --git a/src/Socialbox/Managers/CaptchaManager.php b/src/Socialbox/Managers/CaptchaManager.php index fbb9bec..711c7fd 100644 --- a/src/Socialbox/Managers/CaptchaManager.php +++ b/src/Socialbox/Managers/CaptchaManager.php @@ -10,21 +10,21 @@ use Socialbox\Enums\Status\CaptchaStatus; use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Objects\Database\CaptchaRecord; - use Socialbox\Objects\Database\PeerRecord; + use Socialbox\Objects\Database\PeerDatabaseRecord; class CaptchaManager { /** * Creates a new captcha for the given peer UUID. * - * @param string|PeerRecord $peer_uuid The UUID of the peer to create the captcha for. + * @param string|PeerDatabaseRecord $peer_uuid The UUID of the peer to create the captcha for. * @return string The answer to the captcha. * @throws DatabaseOperationException If the operation fails. */ - public static function createCaptcha(string|PeerRecord $peer_uuid): string + public static function createCaptcha(string|PeerDatabaseRecord $peer_uuid): string { // If the peer_uuid is a RegisteredPeerRecord, get the UUID - if($peer_uuid instanceof PeerRecord) + if($peer_uuid instanceof PeerDatabaseRecord) { $peer_uuid = $peer_uuid->getUuid(); } @@ -73,14 +73,14 @@ /** * Answers a captcha for the given peer UUID. * - * @param string|PeerRecord $peer_uuid The UUID of the peer to answer the captcha for. + * @param string|PeerDatabaseRecord $peer_uuid The UUID of the peer to answer the captcha for. * @param string $answer The answer to the captcha. * @return bool True if the answer is correct, false otherwise. * @throws DatabaseOperationException If the operation fails. */ - public static function answerCaptcha(string|PeerRecord $peer_uuid, string $answer): bool + public static function answerCaptcha(string|PeerDatabaseRecord $peer_uuid, string $answer): bool { - if($peer_uuid instanceof PeerRecord) + if($peer_uuid instanceof PeerDatabaseRecord) { $peer_uuid = $peer_uuid->getUuid(); } @@ -129,14 +129,14 @@ /** * Retrieves the captcha record for the given peer UUID. * - * @param string|PeerRecord $peer_uuid The UUID of the peer to retrieve the captcha for. + * @param string|PeerDatabaseRecord $peer_uuid The UUID of the peer to retrieve the captcha for. * @return CaptchaRecord|null The captcha record. * @throws DatabaseOperationException If the operation fails. */ - public static function getCaptcha(string|PeerRecord $peer_uuid): ?CaptchaRecord + public static function getCaptcha(string|PeerDatabaseRecord $peer_uuid): ?CaptchaRecord { // If the peer_uuid is a RegisteredPeerRecord, get the UUID - if($peer_uuid instanceof PeerRecord) + if($peer_uuid instanceof PeerDatabaseRecord) { $peer_uuid = $peer_uuid->getUuid(); } @@ -166,14 +166,14 @@ /** * Checks if a captcha exists for the given peer UUID. * - * @param string|PeerRecord $peer_uuid The UUID of the peer to check for a captcha. + * @param string|PeerDatabaseRecord $peer_uuid The UUID of the peer to check for a captcha. * @return bool True if a captcha exists, false otherwise. * @throws DatabaseOperationException If the operation fails. */ - public static function captchaExists(string|PeerRecord $peer_uuid): bool + public static function captchaExists(string|PeerDatabaseRecord $peer_uuid): bool { // If the peer_uuid is a RegisteredPeerRecord, get the UUID - if($peer_uuid instanceof PeerRecord) + if($peer_uuid instanceof PeerDatabaseRecord) { $peer_uuid = $peer_uuid->getUuid(); } diff --git a/src/Socialbox/Managers/OneTimePasswordManager.php b/src/Socialbox/Managers/OneTimePasswordManager.php index 5210afa..c9fa89c 100644 --- a/src/Socialbox/Managers/OneTimePasswordManager.php +++ b/src/Socialbox/Managers/OneTimePasswordManager.php @@ -10,20 +10,20 @@ use Socialbox\Classes\OtpCryptography; use Socialbox\Exceptions\CryptographyException; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Objects\Database\PeerRecord; + use Socialbox\Objects\Database\PeerDatabaseRecord; class OneTimePasswordManager { /** * Checks if a given peer uses OTP for authentication. * - * @param string|PeerRecord $peerUuid Either a UUID as a string or a RegisteredPeerRecord object representing the peer. + * @param string|PeerDatabaseRecord $peerUuid Either a UUID as a string or a RegisteredPeerRecord object representing the peer. * @return bool Returns true if the peer uses OTP, otherwise false. * @throws DatabaseOperationException Thrown when a database error occurs. */ - public static function usesOtp(string|PeerRecord $peerUuid): bool + public static function usesOtp(string|PeerDatabaseRecord $peerUuid): bool { - if($peerUuid instanceof PeerRecord) + if($peerUuid instanceof PeerDatabaseRecord) { $peerUuid = $peerUuid->getUuid(); } @@ -45,12 +45,12 @@ /** * Creates and stores a new OTP (One-Time Password) secret for the specified peer, and generates a key URI. * - * @param string|PeerRecord $peer The unique identifier of the peer, either as a string UUID + * @param string|PeerDatabaseRecord $peer The unique identifier of the peer, either as a string UUID * or an instance of RegisteredPeerRecord. * @return string The generated OTP key URI that can be used for applications like authenticator apps. * @throws DatabaseOperationException If there is an error during the database operation. */ - public static function createOtp(string|PeerRecord $peer): string + public static function createOtp(string|PeerDatabaseRecord $peer): string { if(is_string($peer)) { @@ -84,16 +84,16 @@ /** * Verifies the provided OTP (One-Time Password) against the stored secret associated with the specified peer. * - * @param string|PeerRecord $peerUuid The unique identifier of the peer, either as a string UUID + * @param string|PeerDatabaseRecord $peerUuid The unique identifier of the peer, either as a string UUID * or an instance of RegisteredPeerRecord. * @param string $otp The OTP to be verified. * @return bool Returns true if the OTP is valid; otherwise, false. * @throws DatabaseOperationException If there is an error during the database operation. * @throws CryptographyException If there is a failure in decrypting the stored OTP secret. */ - public static function verifyOtp(string|PeerRecord $peerUuid, string $otp): bool + public static function verifyOtp(string|PeerDatabaseRecord $peerUuid, string $otp): bool { - if($peerUuid instanceof PeerRecord) + if($peerUuid instanceof PeerDatabaseRecord) { $peerUuid = $peerUuid->getUuid(); } @@ -145,13 +145,13 @@ /** * Deletes the OTP record associated with the specified peer. * - * @param string|PeerRecord $peerUuid The peer's UUID or an instance of RegisteredPeerRecord whose OTP record needs to be deleted. + * @param string|PeerDatabaseRecord $peerUuid The peer's UUID or an instance of RegisteredPeerRecord whose OTP record needs to be deleted. * @return void * @throws DatabaseOperationException if the database operation fails. */ - public static function deleteOtp(string|PeerRecord $peerUuid): void + public static function deleteOtp(string|PeerDatabaseRecord $peerUuid): void { - if($peerUuid instanceof PeerRecord) + if($peerUuid instanceof PeerDatabaseRecord) { $peerUuid = $peerUuid->getUuid(); } @@ -171,12 +171,12 @@ /** * Retrieves the last updated timestamp for the OTP record of the specified peer. * - * @param string|PeerRecord $peerUuid The peer's UUID or an instance of RegisteredPeerRecord whose OTP record's last updated timestamp needs to be retrieved + * @param string|PeerDatabaseRecord $peerUuid The peer's UUID or an instance of RegisteredPeerRecord whose OTP record's last updated timestamp needs to be retrieved * @return int The last updated timestamp of the OTP record, or 0 if no such record exists */ - public static function getLastUpdated(string|PeerRecord $peerUuid): int + public static function getLastUpdated(string|PeerDatabaseRecord $peerUuid): int { - if($peerUuid instanceof PeerRecord) + if($peerUuid instanceof PeerDatabaseRecord) { $peerUuid = $peerUuid->getUuid(); } diff --git a/src/Socialbox/Managers/PasswordManager.php b/src/Socialbox/Managers/PasswordManager.php index f6a0465..71b848a 100644 --- a/src/Socialbox/Managers/PasswordManager.php +++ b/src/Socialbox/Managers/PasswordManager.php @@ -10,20 +10,20 @@ use Socialbox\Classes\Database; use Socialbox\Exceptions\CryptographyException; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Objects\Database\PeerRecord; + use Socialbox\Objects\Database\PeerDatabaseRecord; class PasswordManager { /** * Checks if the given peer UUID is associated with a password in the database. * - * @param string|PeerRecord $peerUuid The UUID of the peer, or an instance of RegisteredPeerRecord from which the UUID will be retrieved. + * @param string|PeerDatabaseRecord $peerUuid The UUID of the peer, or an instance of RegisteredPeerRecord from which the UUID will be retrieved. * @return bool Returns true if the peer UUID is associated with a password, otherwise false. * @throws DatabaseOperationException If an error occurs while querying the database. */ - public static function usesPassword(string|PeerRecord $peerUuid): bool + public static function usesPassword(string|PeerDatabaseRecord $peerUuid): bool { - if($peerUuid instanceof PeerRecord) + if($peerUuid instanceof PeerDatabaseRecord) { $peerUuid = $peerUuid->getUuid(); } @@ -45,15 +45,15 @@ /** * Sets a secured password for the given peer UUID or registered peer record. * - * @param string|PeerRecord $peerUuid The unique identifier or registered peer record of the user. + * @param string|PeerDatabaseRecord $peerUuid The unique identifier or registered peer record of the user. * @param string $hash The plaintext password to be securely stored. * @return void * @throws DatabaseOperationException If an error occurs while storing the password in the database. * @throws CryptographyException If an error occurs during password encryption or hashing. */ - public static function setPassword(string|PeerRecord $peerUuid, string $hash): void + public static function setPassword(string|PeerDatabaseRecord $peerUuid, string $hash): void { - if($peerUuid instanceof PeerRecord) + if($peerUuid instanceof PeerDatabaseRecord) { $peerUuid = $peerUuid->getUuid(); } @@ -84,15 +84,15 @@ /** * Updates the secured password associated with the given peer UUID. * - * @param string|PeerRecord $peerUuid The unique identifier or registered peer record of the user. + * @param string|PeerDatabaseRecord $peerUuid The unique identifier or registered peer record of the user. * @param string $hash The new password to be stored securely. * @return void * @throws DatabaseOperationException If an error occurs while updating the password in the database. * @throws CryptographyException If an error occurs while encrypting the password or validating the hash. */ - public static function updatePassword(string|PeerRecord $peerUuid, string $hash): void + public static function updatePassword(string|PeerDatabaseRecord $peerUuid, string $hash): void { - if($peerUuid instanceof PeerRecord) + if($peerUuid instanceof PeerDatabaseRecord) { $peerUuid = $peerUuid->getUuid(); } @@ -124,13 +124,13 @@ /** * Deletes the stored password for a specific peer. * - * @param string|PeerRecord $peerUuid The unique identifier of the peer, or an instance of RegisteredPeerRecord. + * @param string|PeerDatabaseRecord $peerUuid The unique identifier of the peer, or an instance of RegisteredPeerRecord. * @return void * @throws DatabaseOperationException If an error occurs during the database operation. */ - public static function deletePassword(string|PeerRecord $peerUuid): void + public static function deletePassword(string|PeerDatabaseRecord $peerUuid): void { - if($peerUuid instanceof PeerRecord) + if($peerUuid instanceof PeerDatabaseRecord) { $peerUuid = $peerUuid->getUuid(); } @@ -150,15 +150,15 @@ /** * Verifies a given password against a stored password hash for a specific peer. * - * @param string|PeerRecord $peerUuid The unique identifier of the peer, or an instance of RegisteredPeerRecord. + * @param string|PeerDatabaseRecord $peerUuid The unique identifier of the peer, or an instance of RegisteredPeerRecord. * @param string $sha512 The SHA-512 hash of the password to be verified. * @return bool Returns true if the password matches the stored hash; false otherwise. * @throws CryptographyException If the password hash is invalid or an error occurs during the cryptographic operation. * @throws DatabaseOperationException If an error occurs during the database operation. */ - public static function verifyPassword(string|PeerRecord $peerUuid, string $sha512): bool + public static function verifyPassword(string|PeerDatabaseRecord $peerUuid, string $sha512): bool { - if($peerUuid instanceof PeerRecord) + if($peerUuid instanceof PeerDatabaseRecord) { $peerUuid = $peerUuid->getUuid(); } diff --git a/src/Socialbox/Managers/PeerInformationManager.php b/src/Socialbox/Managers/PeerInformationManager.php index 1cbe88f..a849732 100644 --- a/src/Socialbox/Managers/PeerInformationManager.php +++ b/src/Socialbox/Managers/PeerInformationManager.php @@ -9,23 +9,23 @@ use Socialbox\Enums\Types\InformationFieldName; use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Objects\Database\PeerInformationFieldRecord; - use Socialbox\Objects\Database\PeerRecord; + use Socialbox\Objects\Database\PeerDatabaseRecord; class PeerInformationManager { /** * Adds a property to a peer's information record. * - * @param string|PeerRecord $peerUuid The UUID of the peer to add the property to. + * @param string|PeerDatabaseRecord $peerUuid The UUID of the peer to add the property to. * @param InformationFieldName $property The name of the property to add. * @param string $value The value of the property to add. * @param PrivacyState|null $privacyState The privacy state of the property to add. * @return void * @throws DatabaseOperationException Thrown if the operation fails. */ - public static function addField(string|PeerRecord $peerUuid, InformationFieldName $property, string $value, ?PrivacyState $privacyState=null): void + public static function addField(string|PeerDatabaseRecord $peerUuid, InformationFieldName $property, string $value, ?PrivacyState $privacyState=null): void { - if($peerUuid instanceof PeerRecord) + if($peerUuid instanceof PeerDatabaseRecord) { $peerUuid = $peerUuid->getUuid(); } @@ -65,15 +65,15 @@ /** * Updates a property for a peer's information record. * - * @param string|PeerRecord $peerUuid The UUID of the peer to update the property for. + * @param string|PeerDatabaseRecord $peerUuid The UUID of the peer to update the property for. * @param InformationFieldName $property The name of the property to update. * @param string $value The new value of the property. * @return void * @throws DatabaseOperationException Thrown if the operation fails. */ - public static function updateField(string|PeerRecord $peerUuid, InformationFieldName $property, string $value): void + public static function updateField(string|PeerDatabaseRecord $peerUuid, InformationFieldName $property, string $value): void { - if($peerUuid instanceof PeerRecord) + if($peerUuid instanceof PeerDatabaseRecord) { $peerUuid = $peerUuid->getUuid(); } @@ -101,15 +101,15 @@ /** * Updates the privacy state for a property in a peer's information record. * - * @param string|PeerRecord $peerUuid The UUID of the peer to update the privacy state for. + * @param string|PeerDatabaseRecord $peerUuid The UUID of the peer to update the privacy state for. * @param InformationFieldName $property The name of the property to update the privacy state for. * @param PrivacyState $privacyState The new privacy state of the property. * @return void * @throws DatabaseOperationException Thrown if the operation fails. */ - public static function updatePrivacyState(string|PeerRecord $peerUuid, InformationFieldName $property, PrivacyState $privacyState): void + public static function updatePrivacyState(string|PeerDatabaseRecord $peerUuid, InformationFieldName $property, PrivacyState $privacyState): void { - if($peerUuid instanceof PeerRecord) + if($peerUuid instanceof PeerDatabaseRecord) { $peerUuid = $peerUuid->getUuid(); } @@ -138,14 +138,14 @@ /** * Checks if a property exists for a peer. * - * @param string|PeerRecord $peerUuid The UUID of the peer to check for the property. + * @param string|PeerDatabaseRecord $peerUuid The UUID of the peer to check for the property. * @param InformationFieldName $property The name of the property to check for. * @return bool * @throws DatabaseOperationException Thrown if the operation fails. */ - public static function fieldExists(string|PeerRecord $peerUuid, InformationFieldName $property): bool + public static function fieldExists(string|PeerDatabaseRecord $peerUuid, InformationFieldName $property): bool { - if($peerUuid instanceof PeerRecord) + if($peerUuid instanceof PeerDatabaseRecord) { $peerUuid = $peerUuid->getUuid(); } @@ -169,14 +169,14 @@ /** * Gets a property from a peer's information record. * - * @param string|PeerRecord $peerUuid The UUID of the peer to get the property from. + * @param string|PeerDatabaseRecord $peerUuid The UUID of the peer to get the property from. * @param InformationFieldName $property The name of the property to get. * @return PeerInformationFieldRecord|null The property record, or null if it does not exist. * @throws DatabaseOperationException Thrown if the operation fails. */ - public static function getField(string|PeerRecord $peerUuid, InformationFieldName $property): ?PeerInformationFieldRecord + public static function getField(string|PeerDatabaseRecord $peerUuid, InformationFieldName $property): ?PeerInformationFieldRecord { - if($peerUuid instanceof PeerRecord) + if($peerUuid instanceof PeerDatabaseRecord) { $peerUuid = $peerUuid->getUuid(); } @@ -206,13 +206,13 @@ /** * Gets all properties from a peer's information record. * - * @param string|PeerRecord $peerUuid The UUID of the peer to get the properties from. + * @param string|PeerDatabaseRecord $peerUuid The UUID of the peer to get the properties from. * @return PeerInformationFieldRecord[] * @throws DatabaseOperationException Thrown if the operation fails. */ - public static function getFields(string|PeerRecord $peerUuid): array + public static function getFields(string|PeerDatabaseRecord $peerUuid): array { - if($peerUuid instanceof PeerRecord) + if($peerUuid instanceof PeerDatabaseRecord) { $peerUuid = $peerUuid->getUuid(); } @@ -240,14 +240,14 @@ /** * Gets all properties from a peer's information record that match the provided privacy filters. * - * @param string|PeerRecord $peerUuid The UUID of the peer to get the properties from. + * @param string|PeerDatabaseRecord $peerUuid The UUID of the peer to get the properties from. * @param PrivacyState[] $privacyFilters The privacy filters to apply. * @return PeerInformationFieldRecord[] The filtered properties. * @throws DatabaseOperationException Thrown if the operation fails. */ - public static function getFilteredFields(string|PeerRecord $peerUuid, array $privacyFilters): array + public static function getFilteredFields(string|PeerDatabaseRecord $peerUuid, array $privacyFilters): array { - if($peerUuid instanceof PeerRecord) + if($peerUuid instanceof PeerDatabaseRecord) { $peerUuid = $peerUuid->getUuid(); } @@ -281,14 +281,14 @@ /** * Deletes a property from a peer's information record. * - * @param string|PeerRecord $peerUuid The UUID of the peer to delete the property from. + * @param string|PeerDatabaseRecord $peerUuid The UUID of the peer to delete the property from. * @param InformationFieldName $property The name of the property to delete. * @return void * @throws DatabaseOperationException Thrown if the operation fails. */ - public static function deleteField(string|PeerRecord $peerUuid, InformationFieldName $property): void + public static function deleteField(string|PeerDatabaseRecord $peerUuid, InformationFieldName $property): void { - if($peerUuid instanceof PeerRecord) + if($peerUuid instanceof PeerDatabaseRecord) { $peerUuid = $peerUuid->getUuid(); } diff --git a/src/Socialbox/Managers/RegisteredPeerManager.php b/src/Socialbox/Managers/RegisteredPeerManager.php index 66f6ebe..55e6192 100644 --- a/src/Socialbox/Managers/RegisteredPeerManager.php +++ b/src/Socialbox/Managers/RegisteredPeerManager.php @@ -14,7 +14,7 @@ use Socialbox\Enums\PrivacyState; use Socialbox\Enums\ReservedUsernames; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Objects\Database\PeerRecord; + use Socialbox\Objects\Database\PeerDatabaseRecord; use Socialbox\Objects\PeerAddress; use Socialbox\Objects\Standard\Peer; use Symfony\Component\Uid\Uuid; @@ -88,13 +88,13 @@ * Deletes a peer from the database based on the given UUID or RegisteredPeerRecord. * WARNING: This operation is cascading and will delete all associated data. * - * @param string|PeerRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer to be deleted. + * @param string|PeerDatabaseRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer to be deleted. * @return void * @throws DatabaseOperationException If the operation fails. */ - public static function deletePeer(string|PeerRecord $uuid): void + public static function deletePeer(string|PeerDatabaseRecord $uuid): void { - if($uuid instanceof PeerRecord) + if($uuid instanceof PeerDatabaseRecord) { $uuid = $uuid->getUuid(); } @@ -116,13 +116,13 @@ /** * Retrieves a registered peer record based on the given unique identifier or RegisteredPeerRecord object. * - * @param string|PeerRecord $uuid The unique identifier of the registered peer, or an instance of RegisteredPeerRecord. - * @return PeerRecord Returns a RegisteredPeerRecord object containing the peer's information. + * @param string|PeerDatabaseRecord $uuid The unique identifier of the registered peer, or an instance of RegisteredPeerRecord. + * @return PeerDatabaseRecord Returns a RegisteredPeerRecord object containing the peer's information. * @throws DatabaseOperationException If there is an error during the database operation. */ - public static function getPeer(string|PeerRecord $uuid): PeerRecord + public static function getPeer(string|PeerDatabaseRecord $uuid): PeerDatabaseRecord { - if($uuid instanceof PeerRecord) + if($uuid instanceof PeerDatabaseRecord) { $uuid = $uuid->getUuid(); } @@ -142,7 +142,7 @@ throw new DatabaseOperationException(sprintf("The requested peer '%s' does not exist", $uuid)); } - return new PeerRecord($result); + return new PeerDatabaseRecord($result); } catch(Exception $e) { @@ -154,10 +154,10 @@ * Retrieves a peer record by the given username. * * @param PeerAddress $address The address of the peer to be retrieved. - * @return PeerRecord|null The record of the peer associated with the given username. + * @return PeerDatabaseRecord|null The record of the peer associated with the given username. * @throws DatabaseOperationException If there is an error while querying the database. */ - public static function getPeerByAddress(PeerAddress $address): ?PeerRecord + public static function getPeerByAddress(PeerAddress $address): ?PeerDatabaseRecord { Logger::getLogger()->verbose(sprintf("Retrieving peer %s from the database", $address->getAddress())); @@ -184,7 +184,7 @@ return null; } - return new PeerRecord($result); + return new PeerDatabaseRecord($result); } catch(Exception $e) { @@ -291,13 +291,13 @@ /** * Enables a peer identified by the given UUID or RegisteredPeerRecord. * - * @param string|PeerRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer to be enabled. + * @param string|PeerDatabaseRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer to be enabled. * @return void * @throws DatabaseOperationException If there is an error while updating the database. */ - public static function enablePeer(string|PeerRecord $uuid): void + public static function enablePeer(string|PeerDatabaseRecord $uuid): void { - if($uuid instanceof PeerRecord) + if($uuid instanceof PeerDatabaseRecord) { $uuid = $uuid->getUuid(); } @@ -319,13 +319,13 @@ /** * Disables the peer identified by the given UUID or RegisteredPeerRecord. * - * @param string|PeerRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer. + * @param string|PeerDatabaseRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer. * @return void * @throws DatabaseOperationException If there is an error while updating the peer's status in the database. */ - public static function disablePeer(string|PeerRecord $uuid): void + public static function disablePeer(string|PeerDatabaseRecord $uuid): void { - if($uuid instanceof PeerRecord) + if($uuid instanceof PeerDatabaseRecord) { $uuid = $uuid->getUuid(); } @@ -347,14 +347,14 @@ /** * Adds a specific flag to the peer identified by the given UUID or RegisteredPeerRecord. * - * @param string|PeerRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer. + * @param string|PeerDatabaseRecord $uuid The UUID or RegisteredPeerRecord instance representing the peer. * @param PeerFlags|array $flags The flag or array of flags to be added to the peer. * @return void * @throws DatabaseOperationException If there is an error while updating the database. */ - public static function addFlag(string|PeerRecord $uuid, PeerFlags|array $flags): void + public static function addFlag(string|PeerDatabaseRecord $uuid, PeerFlags|array $flags): void { - if($uuid instanceof PeerRecord) + if($uuid instanceof PeerDatabaseRecord) { $uuid = $uuid->getUuid(); } @@ -390,12 +390,12 @@ /** * Removes a specific flag from the peer identified by the given UUID or RegisteredPeerRecord. * - * @param string|PeerRecord $peer + * @param string|PeerDatabaseRecord $peer * @param PeerFlags $flag The flag to be removed from the peer. * @return void * @throws DatabaseOperationException If there is an error while updating the database. */ - public static function removeFlag(string|PeerRecord $peer, PeerFlags $flag): void + public static function removeFlag(string|PeerDatabaseRecord $peer, PeerFlags $flag): void { if(is_string($peer)) { diff --git a/src/Socialbox/Managers/SessionManager.php b/src/Socialbox/Managers/SessionManager.php index 2b4d49c..7d2c980 100644 --- a/src/Socialbox/Managers/SessionManager.php +++ b/src/Socialbox/Managers/SessionManager.php @@ -16,7 +16,7 @@ use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\Standard\StandardRpcException; - use Socialbox\Objects\Database\PeerRecord; + use Socialbox\Objects\Database\PeerDatabaseRecord; use Socialbox\Objects\Database\SessionRecord; use Socialbox\Objects\KeyPair; use Symfony\Component\Uid\Uuid; @@ -26,7 +26,7 @@ /** * Creates a new session for a given peer and client details, and stores it in the database. * - * @param PeerRecord $peer The peer record for which the session is being created. + * @param PeerDatabaseRecord $peer The peer record for which the session is being created. * @param string $clientName The name of the client application. * @param string $clientVersion The version of the client application. * @param string $clientPublicSigningKey The client's public signing key, which must be a valid Ed25519 key. @@ -36,7 +36,7 @@ * @throws InvalidArgumentException If the provided public signing key or encryption key is invalid. * @throws DatabaseOperationException If there is an error during the session creation in the database. */ - public static function createSession(PeerRecord $peer, string $clientName, string $clientVersion, string $clientPublicSigningKey, string $clientPublicEncryptionKey, KeyPair $serverEncryptionKeyPair): string + public static function createSession(PeerDatabaseRecord $peer, string $clientName, string $clientVersion, string $clientPublicSigningKey, string $clientPublicEncryptionKey, KeyPair $serverEncryptionKeyPair): string { if($clientPublicSigningKey === '' || Cryptography::validatePublicSigningKey($clientPublicSigningKey) === false) { diff --git a/src/Socialbox/Objects/ClientRequest.php b/src/Socialbox/Objects/ClientRequest.php index 0a40634..157feba 100644 --- a/src/Socialbox/Objects/ClientRequest.php +++ b/src/Socialbox/Objects/ClientRequest.php @@ -11,7 +11,7 @@ use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Managers\RegisteredPeerManager; use Socialbox\Managers\SessionManager; - use Socialbox\Objects\Database\PeerRecord; + use Socialbox\Objects\Database\PeerDatabaseRecord; use Socialbox\Objects\Database\SessionRecord; class ClientRequest @@ -185,11 +185,11 @@ /** * Retrieves the associated peer for the current session. * - * @return PeerRecord|null Returns the associated RegisteredPeerRecord if available, or null if no session exists. + * @return PeerDatabaseRecord|null Returns the associated RegisteredPeerRecord if available, or null if no session exists. * @throws DatabaseOperationException Thrown if an error occurs while retrieving the peer. * @throws StandardRpcException Thrown if the session UUID is invalid. */ - public function getPeer(): ?PeerRecord + public function getPeer(): ?PeerDatabaseRecord { $session = $this->getSession(); diff --git a/src/Socialbox/Objects/Database/PeerRecord.php b/src/Socialbox/Objects/Database/PeerDatabaseRecord.php similarity index 94% rename from src/Socialbox/Objects/Database/PeerRecord.php rename to src/Socialbox/Objects/Database/PeerDatabaseRecord.php index 6143295..7bb77d4 100644 --- a/src/Socialbox/Objects/Database/PeerRecord.php +++ b/src/Socialbox/Objects/Database/PeerDatabaseRecord.php @@ -8,9 +8,8 @@ use Socialbox\Enums\Flags\PeerFlags; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\Standard\Peer; - use Socialbox\Objects\Standard\SelfUser; - class PeerRecord implements SerializableInterface + class PeerDatabaseRecord implements SerializableInterface { private string $uuid; private string $username; @@ -195,16 +194,6 @@ return $this->username === 'host' && $this->server !== Configuration::getInstanceConfiguration()->getDomain(); } - /** - * Converts the current instance to a SelfUser object. - * - * @return SelfUser The SelfUser object. - */ - public function toSelfUser(): SelfUser - { - return new SelfUser($this); - } - /** * Converts the current instance to a Peer object. * From 8c2cbf48d513d158bbb55afd3e76b5a71e8d2d07 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 31 Jan 2025 15:31:07 -0500 Subject: [PATCH 313/420] Improved Peer Resolution and fixed minor bugs and issues, completed todo --- .../Classes/StandardMethods/Core/GetSelf.php | 28 ++ .../Standard/StandardRpcException.php | 6 + .../Objects/Database/PeerDatabaseRecord.php | 6 +- src/Socialbox/Objects/Standard/SelfUser.php | 272 +++++++++--------- src/Socialbox/Socialbox.php | 52 +++- 5 files changed, 209 insertions(+), 155 deletions(-) create mode 100644 src/Socialbox/Classes/StandardMethods/Core/GetSelf.php diff --git a/src/Socialbox/Classes/StandardMethods/Core/GetSelf.php b/src/Socialbox/Classes/StandardMethods/Core/GetSelf.php new file mode 100644 index 0000000..d5c4a0b --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/Core/GetSelf.php @@ -0,0 +1,28 @@ +produceError(StandardError::from($this->code), $this->message); } + + public static function fromRpcException(RpcException $e): StandardRpcException + { + return new self($e->getMessage(), StandardError::tryFrom($e->getCode()) ?? StandardError::UNKNOWN, $e); + } } \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/PeerDatabaseRecord.php b/src/Socialbox/Objects/Database/PeerDatabaseRecord.php index 7bb77d4..4611b59 100644 --- a/src/Socialbox/Objects/Database/PeerDatabaseRecord.php +++ b/src/Socialbox/Objects/Database/PeerDatabaseRecord.php @@ -7,6 +7,7 @@ use Socialbox\Classes\Configuration; use Socialbox\Enums\Flags\PeerFlags; use Socialbox\Interfaces\SerializableInterface; + use Socialbox\Objects\Standard\InformationFieldState; use Socialbox\Objects\Standard\Peer; class PeerDatabaseRecord implements SerializableInterface @@ -197,13 +198,14 @@ /** * Converts the current instance to a Peer object. * + * @param PeerInformationFieldRecord[]|InformationFieldState[]|array $informationFields * @return Peer The Peer representation of the current instance. */ - public function toStandardPeer(): Peer + public function toStandardPeer(array $informationFields): Peer { - // TODO: TO be updated return Peer::fromArray([ 'address' => $this->getAddress(), + 'information_fields' => $informationFields, 'flags' => array_map(fn(PeerFlags $flag) => $flag->value, $this->flags), 'registered' => $this->created->getTimestamp() ]); diff --git a/src/Socialbox/Objects/Standard/SelfUser.php b/src/Socialbox/Objects/Standard/SelfUser.php index 1d862bd..bf9a3c0 100644 --- a/src/Socialbox/Objects/Standard/SelfUser.php +++ b/src/Socialbox/Objects/Standard/SelfUser.php @@ -1,159 +1,151 @@ uuid = $data->getUuid(); - $this->enabled = $data->isEnabled(); - $this->username = $data->getUsername(); - $this->address = $data->getAddress(); - $this->displayName = $data->getDisplayName(); - $this->flags = $data->getFlags(); - $this->created = $data->getCreated()->getTimestamp(); + $this->uuid = $data['uuid']; + $this->enabled = $data['enabled']; + $this->username = $data['username']; + $this->address = $data['address']; + $this->displayName = $data['display_name'] ?? null; + + if(is_string($data['flags'])) + { + $this->flags = PeerFlags::fromString($data['flags']); + } + elseif(is_array($data['flags'])) + { + $this->flags = $data['flags']; + } + else + { + $this->flags = []; + } + + if($data['created'] instanceof DateTime) + { + $this->registered = $data['created']->getTimestamp(); + } + else + { + $this->registered = $data['created']; + } return; } - $this->uuid = $data['uuid']; - $this->enabled = $data['enabled']; - $this->username = $data['username']; - $this->address = $data['address']; - $this->displayName = $data['display_name'] ?? null; - - if(is_string($data['flags'])) + /** + * Retrieves the UUID of the object. + * + * @return string The UUID of the object. + */ + public function getUuid(): string { - $this->flags = PeerFlags::fromString($data['flags']); - } - elseif(is_array($data['flags'])) - { - $this->flags = $data['flags']; - } - else - { - $this->flags = []; + return $this->uuid; } - if($data['created'] instanceof DateTime) + public function isEnabled(): bool { - $this->created = $data['created']->getTimestamp(); - } - else - { - $this->created = $data['created']; + return $this->enabled; } - return; - } - - /** - * Retrieves the UUID of the object. - * - * @return string The UUID of the object. - */ - public function getUuid(): string - { - return $this->uuid; - } - - public function isEnabled(): bool - { - return $this->enabled; - } - - /** - * - * @return string The username of the user. - */ - public function getUsername(): string - { - return $this->username; - } - - public function getAddress(): string - { - return $this->address; - } - - /** - * - * @return string|null The display name. - */ - public function getDisplayName(): ?string - { - return $this->displayName; - } - - /** - * - * @return array - */ - public function getFlags(): array - { - return $this->flags; - } - - /** - * - * @return int The timestamp when the object was created. - */ - public function getCreated(): int - { - return $this->created; - } - - /** - * @inheritDoc - */ - public static function fromArray(array $data): SelfUser - { - return new self($data); - } - - /** - * @inheritDoc - */ - public function toArray(): array - { - $flags = []; - foreach($this->flags as $flag) + /** + * + * @return string The username of the user. + */ + public function getUsername(): string { - $flags[] = $flag->value; + return $this->username; } - return [ - 'uuid' => $this->uuid, - 'enabled' => $this->enabled, - 'username' => $this->username, - 'address' => $this->address, - 'display_name' => $this->displayName, - 'flags' => $flags, - 'created' => $this->created - ]; - } -} \ No newline at end of file + public function getAddress(): string + { + return $this->address; + } + + /** + * + * @return string|null The display name. + */ + public function getDisplayName(): ?string + { + return $this->displayName; + } + + /** + * + * @return array + */ + public function getFlags(): array + { + return $this->flags; + } + + /** + * + * @return int The timestamp when the object was created. + */ + public function getRegistered(): int + { + return $this->registered; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): SelfUser + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + $flags = []; + foreach($this->flags as $flag) + { + $flags[] = $flag->value; + } + + return [ + 'uuid' => $this->uuid, + 'enabled' => $this->enabled, + 'username' => $this->username, + 'address' => $this->address, + 'display_name' => $this->displayName, + 'flags' => $flags, + 'created' => $this->registered + ]; + } + } \ No newline at end of file diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index e5dc7ea..77754ad 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -891,41 +891,53 @@ } catch(DatabaseOperationException $e) { - throw new StandardRpcException('Failed to resolve the peer: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to resolve the peer due to an internal server error', StandardError::INTERNAL_SERVER_ERROR, $e); } if($existingPeer === null) { // if the peer doesn't exist, resolve it externally and synchronize it - try { $peer = self::getExternalSession($peerAddress->getDomain())->resolvePeer($peerAddress, $identifiedAs); } + catch(RpcException $e) + { + throw StandardRpcException::fromRpcException($e); + } catch(Exception $e) { throw new StandardRpcException('Failed to resolve the peer: ' . $e->getMessage(), StandardError::RESOLUTION_FAILED, $e); } - try + // Do not synchronize if this is a personal request, there may be information fields that + // the peer does not want to share with the server + if($identifiedAs !== null) { - RegisteredPeerManager::synchronizeExternalPeer($peer); - } - catch(DatabaseOperationException $e) - { - throw new StandardRpcException('Failed to synchronize the external peer: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); + try + { + RegisteredPeerManager::synchronizeExternalPeer($peer); + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to synchronize the external peer due to an internal server error', StandardError::INTERNAL_SERVER_ERROR, $e); + } } return $peer; } - // If the peer exists, but it's outdated, synchronize it - if($existingPeer->getUpdated()->getTimestamp() < time() - Configuration::getPoliciesConfiguration()->getPeerSyncInterval()) + // if we're not identifying as a personal peer and If the peer exists, but it's outdated, synchronize it + if($identifiedAs === null && $existingPeer->getUpdated()->getTimestamp() < time() - Configuration::getPoliciesConfiguration()->getPeerSyncInterval()) { try { $peer = self::getExternalSession($peerAddress->getDomain())->resolvePeer($peerAddress, $identifiedAs); } + catch(RpcException $e) + { + throw StandardRpcException::fromRpcException($e); + } catch(Exception $e) { throw new StandardRpcException('Failed to resolve the peer: ' . $e->getMessage(), StandardError::RESOLUTION_FAILED, $e); @@ -937,14 +949,28 @@ } catch(DatabaseOperationException $e) { - throw new StandardRpcException('Failed to synchronize the external peer: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to synchronize the external peer due to an internal server error', StandardError::INTERNAL_SERVER_ERROR, $e); } return $peer; } - // If the peer exists and is up to date, return it - return $existingPeer->toStandardPeer(); + // If the peer exists and is up to date, return it from our local database instead. (Quicker) + try + { + $informationFields = PeerInformationManager::getFields($existingPeer); + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to obtain local information fields about an external peer locally due to an internal server error', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return new Peer([ + 'address' => $existingPeer->getAddress(), + 'information_fields' => $informationFields, + 'flags' => $existingPeer->getFlags(), + 'registered' => $existingPeer->getCreated()->getTimestamp() + ]); } /** From 3014b0b97f05b5b43127879c003cfb026cec5828 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 31 Jan 2025 15:32:06 -0500 Subject: [PATCH 314/420] Removed unnecessary check --- .../Classes/StandardMethods/Verification/Authenticate.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/Verification/Authenticate.php b/src/Socialbox/Classes/StandardMethods/Verification/Authenticate.php index 2fe5438..c52d689 100644 --- a/src/Socialbox/Classes/StandardMethods/Verification/Authenticate.php +++ b/src/Socialbox/Classes/StandardMethods/Verification/Authenticate.php @@ -26,11 +26,6 @@ return $rpcRequest->produceError(StandardError::FORBIDDEN, 'Only external peers can authenticate using this method'); } - if($request->getSession()->isAuthenticated()) - { - return $rpcRequest->produceError(StandardError::FORBIDDEN, 'External host is already authenticated'); - } - SessionManager::updateFlow($request->getSession(), [SessionFlags::AUTHENTICATION_REQUIRED]); } catch(Exception $e) From d8fe574437e48f2f2dcd3b924d002ac0e104dd16 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 31 Jan 2025 15:33:05 -0500 Subject: [PATCH 315/420] Improved exception handling --- .../StandardMethods/Settings/SettingsAddSignature.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddSignature.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddSignature.php index 5b3de2a..fba4d44 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddSignature.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddSignature.php @@ -7,6 +7,8 @@ use Socialbox\Abstracts\Method; use Socialbox\Classes\Configuration; use Socialbox\Enums\StandardError; + use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; use Socialbox\Exceptions\Standard\MissingRpcArgumentException; use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; @@ -54,9 +56,9 @@ } catch(InvalidArgumentException $e) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, $e->getMessage()); + throw new InvalidRpcArgumentException($e); } - catch(Exception $e) + catch(DatabaseOperationException $e) { throw new StandardRpcException('Failed to add the signing key', StandardError::INTERNAL_SERVER_ERROR, $e); } From b78d176fdecd4c9cd52662cf67e5eb5a6218fee4 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 31 Jan 2025 15:33:49 -0500 Subject: [PATCH 316/420] Improved limit reached error message --- .../Classes/StandardMethods/Settings/SettingsAddSignature.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddSignature.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddSignature.php index fba4d44..7d60956 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddSignature.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddSignature.php @@ -49,7 +49,7 @@ $peerUuid = $request->getPeer()->getUuid(); if(SigningKeysManager::getSigningKeyCount($peerUuid) >= Configuration::getPoliciesConfiguration()->getMaxSigningKeys()) { - return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The maximum number of signing keys has been reached'); + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The maximum number of signing keys has been reached, the server\'s configured limit is: ' . Configuration::getPoliciesConfiguration()->getMaxSigningKeys()); } $uuid = SigningKeysManager::addSigningKey($peerUuid, $rpcRequest->getParameter('public_key'), $name, $expires); From 6ba7397783c411050abb3d031518049e7412755a Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 31 Jan 2025 18:11:40 -0500 Subject: [PATCH 317/420] Minor correction --- .../Classes/StandardMethods/Settings/SettingsDeletePassword.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeletePassword.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeletePassword.php index 06b78e2..1f45c67 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeletePassword.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeletePassword.php @@ -43,7 +43,6 @@ throw new InvalidRpcArgumentException('password', 'Must be a valid SHA-512 hash'); } - try { // Get the peer From 508d4565d96f84566b9aa8227c2794f33dba3cd7 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 31 Jan 2025 18:13:45 -0500 Subject: [PATCH 318/420] Added existence check --- .../Settings/SettingsDeleteSignature.php | 16 +++++++++--- src/Socialbox/Managers/SigningKeysManager.php | 25 +++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteSignature.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteSignature.php index 0211037..176163c 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteSignature.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsDeleteSignature.php @@ -7,6 +7,9 @@ use ncc\ThirdParty\Symfony\Uid\Uuid; use Socialbox\Abstracts\Method; use Socialbox\Enums\StandardError; + use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; + use Socialbox\Exceptions\Standard\MissingRpcArgumentException; use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\SigningKeysManager; @@ -22,23 +25,28 @@ { if(!$rpcRequest->containsParameter('uuid')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'uuid' parameter"); + throw new MissingRpcArgumentException('uuid'); } try { $uuid = Uuid::fromString($rpcRequest->getParameter('uuid')); } - catch(InvalidArgumentException $e) + catch(InvalidArgumentException) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Invalid UUID', $e); + throw new InvalidRpcArgumentException('uuid'); } try { + if(!SigningKeysManager::signingKeyExists($request->getPeer()->getUuid(), $uuid)) + { + return $rpcRequest->produceResponse(false); + } + SigningKeysManager::deleteSigningKey($request->getPeer()->getUuid(), $uuid); } - catch(Exception $e) + catch(DatabaseOperationException $e) { throw new StandardRpcException('Failed to delete the signing key', StandardError::INTERNAL_SERVER_ERROR, $e); } diff --git a/src/Socialbox/Managers/SigningKeysManager.php b/src/Socialbox/Managers/SigningKeysManager.php index 2b78fa7..3dde942 100644 --- a/src/Socialbox/Managers/SigningKeysManager.php +++ b/src/Socialbox/Managers/SigningKeysManager.php @@ -195,6 +195,31 @@ } } + /** + * Checks if a signing key exists in the database using the provided UUID. + * + * @param string $peerUuid The UUID of the peer associated with the signing key. + * @param string $uuid The UUID of the signing key to check. + * @return bool True if the signing key exists, false otherwise. + * @throws DatabaseOperationException If a database error occurs during the operation. + */ + public static function signingKeyExists(string $peerUuid, string $uuid): bool + { + try + { + $statement = Database::getConnection()->prepare("SELECT COUNT(*) FROM signing_keys WHERE uuid=:uuid AND peer_uuid=:peer_uuid"); + $statement->bindParam(':uuid', $uuid); + $statement->bindParam(':peer_uuid', $peerUuid); + $statement->execute(); + + return $statement->fetchColumn() > 0; + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to check if the signing key exists in the database', $e); + } + } + /** * Deletes a signing key from the database using the provided UUID. * From cd4a45c81165e1d8caf64168748c144f5db763e3 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 31 Jan 2025 18:14:36 -0500 Subject: [PATCH 319/420] Added SettingsSignatureExists --- .../Settings/SettingsSignatureExists.php | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/Socialbox/Classes/StandardMethods/Settings/SettingsSignatureExists.php diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsSignatureExists.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsSignatureExists.php new file mode 100644 index 0000000..86a7c9d --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsSignatureExists.php @@ -0,0 +1,50 @@ +containsParameter('uuid')) + { + throw new MissingRpcArgumentException('uuid'); + } + + try + { + $uuid = Uuid::fromString($rpcRequest->getParameter('uuid')); + } + catch(InvalidArgumentException) + { + throw new InvalidRpcArgumentException('uuid'); + } + + try + { + return $rpcRequest->produceResponse(SigningKeysManager::signingKeyExists($request->getPeer()->getUuid(), $uuid)); + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to check the signing key existence', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + } + } \ No newline at end of file From cf2936e8e5eb341b5e13779b1404753f4f988aad Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 31 Jan 2025 18:19:09 -0500 Subject: [PATCH 320/420] Improved exception handling --- .../Settings/SettingsGetInformationField.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetInformationField.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetInformationField.php index 61447e9..58bb968 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetInformationField.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetInformationField.php @@ -6,6 +6,8 @@ use Socialbox\Enums\StandardError; use Socialbox\Enums\Types\InformationFieldName; use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; + use Socialbox\Exceptions\Standard\MissingRpcArgumentException; use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PeerInformationManager; @@ -21,12 +23,12 @@ { if(!$rpcRequest->containsParameter('field')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The required field parameter is missing'); + throw new MissingRpcArgumentException('field'); } $fieldName = InformationFieldName::tryFrom(strtoupper($rpcRequest->getParameter('field'))); if($fieldName === null) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The provided field parameter is invalid'); + throw new InvalidRpcArgumentException('field'); } try From 3510204daeed0061c08f30f1200906ef83faf0ed Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 31 Jan 2025 18:19:44 -0500 Subject: [PATCH 321/420] Improved exception handling --- .../Classes/StandardMethods/Settings/SettingsGetSigningKey.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSigningKey.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSigningKey.php index d58923b..a6d3d91 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSigningKey.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSigningKey.php @@ -5,6 +5,7 @@ use Socialbox\Abstracts\Method; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\Standard\MissingRpcArgumentException; use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\SigningKeysManager; @@ -20,7 +21,7 @@ { if(!$rpcRequest->containsParameter('uuid') && $rpcRequest->getParameter('uuid') !== null) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'uuid' parameter"); + throw new MissingRpcArgumentException('uuid'); } try From a0acb28037500d3d7f77195e17f725c8ccce12f8 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 31 Jan 2025 18:26:53 -0500 Subject: [PATCH 322/420] Improved exception handling --- .../StandardMethods/Settings/SettingsSetOtp.php | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsSetOtp.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsSetOtp.php index 34e933d..5089d5d 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsSetOtp.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsSetOtp.php @@ -7,7 +7,10 @@ use Socialbox\Classes\Cryptography; use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; + use Socialbox\Exceptions\CryptographyException; use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; + use Socialbox\Exceptions\Standard\MissingRpcArgumentException; use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\OneTimePasswordManager; @@ -23,10 +26,10 @@ */ public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { - $peer = $request->getPeer(); try { + $peer = $request->getPeer(); if (OneTimePasswordManager::usesOtp($peer->getUuid())) { return $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, "Cannot set One Time Password when one is already set, use 'settingsUpdateOtp' instead"); @@ -59,12 +62,12 @@ { if(!$rpcRequest->containsParameter('password')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'When a password is set, the current password must be provided to set an OTP'); + throw new MissingRpcArgumentException('password'); } if(!Cryptography::validateSha512($rpcRequest->getParameter('password'))) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The provided password is not a valid SHA-512 hash'); + throw new InvalidRpcArgumentException('password', 'The provided password is not a valid SHA-512 hash'); } try @@ -74,7 +77,11 @@ return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The provided password is incorrect'); } } - catch(Exception $e) + catch(CryptographyException $e) + { + throw new StandardRpcException($e->getMessage(), StandardError::CRYPTOGRAPHIC_ERROR, $e); + } + catch(DatabaseOperationException $e) { throw new StandardRpcException('Failed to verify password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } From 056ee905157ef5cd7e21a2b8fe3a7e21c544dcd9 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 31 Jan 2025 18:28:09 -0500 Subject: [PATCH 323/420] Improved exception handling --- .../Settings/SettingsSetPassword.php | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsSetPassword.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsSetPassword.php index 91c688b..54fbaab 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsSetPassword.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsSetPassword.php @@ -2,12 +2,14 @@ namespace Socialbox\Classes\StandardMethods\Settings; - use Exception; use Socialbox\Abstracts\Method; use Socialbox\Classes\Cryptography; use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; + use Socialbox\Exceptions\CryptographyException; use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; + use Socialbox\Exceptions\Standard\MissingRpcArgumentException; use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PasswordManager; @@ -24,12 +26,12 @@ { if(!$rpcRequest->containsParameter('password')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'password' parameter"); + throw new MissingRpcArgumentException('password'); } if(!Cryptography::validatePasswordHash($rpcRequest->getParameter('password'))) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Invalid 'password' parameter, must be a valid argon2id hash"); + throw new InvalidRpcArgumentException('password', "Must be a valid argon2id hash"); } try @@ -52,7 +54,11 @@ // Remove the SET_PASSWORD flag & update the session flow if necessary SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_PASSWORD]); } - catch(Exception $e) + catch(CryptographyException $e) + { + throw new StandardRpcException($e->getMessage(), StandardError::CRYPTOGRAPHIC_ERROR, $e); + } + catch(DatabaseOperationException $e) { throw new StandardRpcException('Failed to set password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } From 998a7ad2ecdcf0f8e0cce3c627b66b1e5f7115c3 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 31 Jan 2025 18:29:12 -0500 Subject: [PATCH 324/420] Improved exception handling --- .../Settings/SettingsUpdateInformationField.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdateInformationField.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdateInformationField.php index 01dc98d..43cf1e7 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdateInformationField.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdateInformationField.php @@ -6,6 +6,8 @@ use Socialbox\Enums\StandardError; use Socialbox\Enums\Types\InformationFieldName; use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; + use Socialbox\Exceptions\Standard\MissingRpcArgumentException; use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PeerInformationManager; @@ -16,29 +18,30 @@ { /** * @inheritDoc + * @noinspection DuplicatedCode */ public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { // Field parameter is required if(!$rpcRequest->containsParameter('field')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The required field parameter is missing'); + throw new MissingRpcArgumentException('field'); } $fieldName = InformationFieldName::tryFrom(strtoupper($rpcRequest->getParameter('field'))); if($fieldName === null) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The provided field parameter is invalid'); + throw new InvalidRpcArgumentException('field'); } // Value parameter is required if(!$rpcRequest->containsParameter('value')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The required value parameter is missing'); + throw new MissingRpcArgumentException('value'); } $value = $rpcRequest->getParameter('value'); if(!$fieldName->validate($value)) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The provided value parameter is invalid'); + throw new InvalidRpcArgumentException('value'); } try @@ -46,7 +49,7 @@ $peer = $request->getPeer(); if(!PeerInformationManager::fieldExists($peer, $fieldName)) { - return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The information field does not exist'); + return $rpcRequest->produceResponse(false); } PeerInformationManager::updateField($peer, $fieldName, $value); From c813139d9e26d545820df81e342147d8fa629524 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 31 Jan 2025 18:33:24 -0500 Subject: [PATCH 325/420] Improved exception handling --- .../Settings/SettingsUpdateInformationPrivacy.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdateInformationPrivacy.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdateInformationPrivacy.php index 9c8e449..0ffc888 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdateInformationPrivacy.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdateInformationPrivacy.php @@ -7,6 +7,8 @@ use Socialbox\Enums\StandardError; use Socialbox\Enums\Types\InformationFieldName; use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; + use Socialbox\Exceptions\Standard\MissingRpcArgumentException; use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PeerInformationManager; @@ -23,24 +25,24 @@ // Field parameter is required if(!$rpcRequest->containsParameter('field')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The required field parameter is missing'); + throw new MissingRpcArgumentException('field'); } $fieldName = InformationFieldName::tryFrom(strtoupper($rpcRequest->getParameter('field'))); if($fieldName === null) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The provided field parameter is invalid'); + throw new InvalidRpcArgumentException('field'); } // Privacy parameter is required - $privacy = null; if(!$rpcRequest->containsParameter('privacy')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The required privacy parameter is missing'); + throw new MissingRpcArgumentException('privacy'); } + $privacy = PrivacyState::tryFrom(strtoupper($rpcRequest->getParameter('privacy'))); if($privacy === null) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The provided privacy parameter is invalid'); + throw new InvalidRpcArgumentException('privacy'); } try @@ -48,7 +50,7 @@ $peer = $request->getPeer(); if(!PeerInformationManager::fieldExists($peer, $fieldName)) { - return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The information field does not exist'); + return $rpcRequest->produceResponse(false); } PeerInformationManager::updatePrivacyState($peer, $fieldName, $privacy); From 388605dbce12023348a35b1d87cc1b25214aca3f Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 31 Jan 2025 18:34:59 -0500 Subject: [PATCH 326/420] Improved exception handling --- .../Settings/SettingsUpdatePassword.php | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdatePassword.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdatePassword.php index a644788..3127648 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdatePassword.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsUpdatePassword.php @@ -2,11 +2,13 @@ namespace Socialbox\Classes\StandardMethods\Settings; - use Exception; use Socialbox\Abstracts\Method; use Socialbox\Classes\Cryptography; use Socialbox\Enums\StandardError; + use Socialbox\Exceptions\CryptographyException; use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; + use Socialbox\Exceptions\Standard\MissingRpcArgumentException; use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PasswordManager; @@ -22,29 +24,29 @@ { if(!$rpcRequest->containsParameter('password')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'password' parameter"); + throw new MissingRpcArgumentException('password'); } if(!Cryptography::validatePasswordHash($rpcRequest->getParameter('password'))) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Invalid 'password' parameter, must be a valid argon2id hash"); + throw new InvalidRpcArgumentException('password', 'Must be a valid argon2id hash'); } if(!$rpcRequest->containsParameter('existing_password')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'existing_password' parameter"); + throw new MissingRpcArgumentException('existing_password'); } if(!Cryptography::validateSha512($rpcRequest->getParameter('existing_password'))) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Invalid 'existing_password' parameter, must be a valid SHA-512 hash"); + throw new InvalidRpcArgumentException('existing_password', 'Must be a valid SHA-512 hash'); } try { if (!PasswordManager::usesPassword($request->getPeer()->getUuid())) { - return $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, "Cannot update password when one isn't already set, use 'settingsSetPassword' instead"); + return $rpcRequest->produceResponse(false); } } catch (DatabaseOperationException $e) @@ -56,10 +58,14 @@ { if (!PasswordManager::verifyPassword($request->getPeer()->getUuid(), $rpcRequest->getParameter('existing_password'))) { - return $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, "Failed to update password due to incorrect existing password"); + return $rpcRequest->produceResponse(false); } } - catch (Exception $e) + catch(CryptographyException $e) + { + throw new StandardRpcException($e->getMessage(), StandardError::CRYPTOGRAPHIC_ERROR, $e); + } + catch (DatabaseOperationException $e) { throw new StandardRpcException('Failed to verify existing password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } @@ -69,7 +75,11 @@ // Set the password PasswordManager::updatePassword($request->getPeer(), $rpcRequest->getParameter('password')); } - catch(Exception $e) + catch(CryptographyException $e) + { + throw new StandardRpcException($e->getMessage(), StandardError::CRYPTOGRAPHIC_ERROR, $e); + } + catch(DatabaseOperationException $e) { throw new StandardRpcException('Failed to set password due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e); } From fdf9da12aa5ef0ce72f562f85a9f2befd059545c Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 31 Jan 2025 22:33:51 -0500 Subject: [PATCH 327/420] Updated method Authenticate to do one additional check --- .../StandardMethods/Verification/Authenticate.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Socialbox/Classes/StandardMethods/Verification/Authenticate.php b/src/Socialbox/Classes/StandardMethods/Verification/Authenticate.php index c52d689..03bea68 100644 --- a/src/Socialbox/Classes/StandardMethods/Verification/Authenticate.php +++ b/src/Socialbox/Classes/StandardMethods/Verification/Authenticate.php @@ -26,7 +26,14 @@ return $rpcRequest->produceError(StandardError::FORBIDDEN, 'Only external peers can authenticate using this method'); } - SessionManager::updateFlow($request->getSession(), [SessionFlags::AUTHENTICATION_REQUIRED]); + $session = $request->getSession(); + + if(!$session->flagExists(SessionFlags::AUTHENTICATION_REQUIRED)) + { + return $rpcRequest->produceResponse(false); + } + + SessionManager::updateFlow($session, [SessionFlags::AUTHENTICATION_REQUIRED]); } catch(Exception $e) { From 24530f54943ab32e23721e4740f4f2e4c57d67b0 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Feb 2025 13:02:36 -0500 Subject: [PATCH 328/420] Improved Exception Handling --- .../Verification/VerificationAnswerImageCaptcha.php | 5 +++-- src/Socialbox/Enums/StandardError.php | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/Verification/VerificationAnswerImageCaptcha.php b/src/Socialbox/Classes/StandardMethods/Verification/VerificationAnswerImageCaptcha.php index 79af8fd..cc840c4 100644 --- a/src/Socialbox/Classes/StandardMethods/Verification/VerificationAnswerImageCaptcha.php +++ b/src/Socialbox/Classes/StandardMethods/Verification/VerificationAnswerImageCaptcha.php @@ -6,6 +6,7 @@ use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\Standard\MissingRpcArgumentException; use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\CaptchaManager; @@ -23,7 +24,7 @@ { if(!$rpcRequest->containsParameter('answer')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'The answer parameter is required'); + throw new MissingRpcArgumentException('answer'); } $session = $request->getSession(); @@ -32,7 +33,7 @@ { if(CaptchaManager::getCaptcha($session->getPeerUuid())?->isExpired()) { - return $rpcRequest->produceError(StandardError::CAPTCHA_EXPIRED, 'The captcha has expired'); + return $rpcRequest->produceError(StandardError::EXPIRED, 'The captcha has expired'); } } catch(DatabaseOperationException $e) diff --git a/src/Socialbox/Enums/StandardError.php b/src/Socialbox/Enums/StandardError.php index ad2721f..2184f55 100644 --- a/src/Socialbox/Enums/StandardError.php +++ b/src/Socialbox/Enums/StandardError.php @@ -16,7 +16,8 @@ case NOT_FOUND = -105; case RESOLUTION_FAILED = -106; case CONFLICT = -107; - case CRYPTOGRAPHIC_ERROR = -108; + case EXPIRED = -108; + case CRYPTOGRAPHIC_ERROR = -109; // RPC Errors case RPC_METHOD_NOT_FOUND = -1000; From 2a0aab4a211446c653891162b55b3cbdce75b06d Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Feb 2025 13:12:40 -0500 Subject: [PATCH 329/420] Improved Exception Handling --- .../VerificationGetImageCaptcha.php | 13 ++++++++++--- .../VerificationOtpAuthentication.php | 8 +++++--- .../VerificationPasswordAuthentication.php | 18 ++++++++++-------- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/Verification/VerificationGetImageCaptcha.php b/src/Socialbox/Classes/StandardMethods/Verification/VerificationGetImageCaptcha.php index 21066e9..a741332 100644 --- a/src/Socialbox/Classes/StandardMethods/Verification/VerificationGetImageCaptcha.php +++ b/src/Socialbox/Classes/StandardMethods/Verification/VerificationGetImageCaptcha.php @@ -21,17 +21,24 @@ */ public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { - $session = $request->getSession(); + try + { + $session = $request->getSession(); + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('An error occurred while trying to get the session', StandardError::INTERNAL_SERVER_ERROR, $e); + } + // Check for session conditions if(!$session->flagExists(SessionFlags::VER_IMAGE_CAPTCHA)) { return $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, 'Solving an image captcha is not required at this time'); } - $peer = $request->getPeer(); - try { + $peer = $request->getPeer(); if(CaptchaManager::captchaExists($peer)) { $captchaRecord = CaptchaManager::getCaptcha($peer); diff --git a/src/Socialbox/Classes/StandardMethods/Verification/VerificationOtpAuthentication.php b/src/Socialbox/Classes/StandardMethods/Verification/VerificationOtpAuthentication.php index 3d5c3c1..064d346 100644 --- a/src/Socialbox/Classes/StandardMethods/Verification/VerificationOtpAuthentication.php +++ b/src/Socialbox/Classes/StandardMethods/Verification/VerificationOtpAuthentication.php @@ -8,6 +8,8 @@ use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\CryptographyException; + use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; + use Socialbox\Exceptions\Standard\MissingRpcArgumentException; use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\OneTimePasswordManager; @@ -25,18 +27,18 @@ { if(!$rpcRequest->containsParameter('code')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'code' parameter"); + throw new MissingRpcArgumentException('code'); } if(strlen($rpcRequest->getParameter('code')) !== Configuration::getSecurityConfiguration()->getOtpDigits()) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Invalid 'code' parameter length"); + throw new InvalidRpcArgumentException('code', 'Invalid OTP code length'); } $session = $request->getSession(); if(!$session->flagExists(SessionFlags::VER_OTP)) { - return $rpcRequest->produceError(StandardError::FORBIDDEN, 'One Time Password verification is not required at this time'); + return $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, 'One Time Password verification is not required at this time'); } try diff --git a/src/Socialbox/Classes/StandardMethods/Verification/VerificationPasswordAuthentication.php b/src/Socialbox/Classes/StandardMethods/Verification/VerificationPasswordAuthentication.php index f2cc87b..7a8b078 100644 --- a/src/Socialbox/Classes/StandardMethods/Verification/VerificationPasswordAuthentication.php +++ b/src/Socialbox/Classes/StandardMethods/Verification/VerificationPasswordAuthentication.php @@ -8,6 +8,8 @@ use Socialbox\Enums\Flags\SessionFlags; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\CryptographyException; + use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; + use Socialbox\Exceptions\Standard\MissingRpcArgumentException; use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Managers\PasswordManager; @@ -25,22 +27,22 @@ { if(!$rpcRequest->containsParameter('password')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'password' parameter"); + throw new MissingRpcArgumentException('password'); } if(!Cryptography::validateSha512($rpcRequest->getParameter('password'))) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Invalid 'password' parameter, must be a valid SHA-512 hash"); - } - - $session = $request->getSession(); - if(!$session->flagExists(SessionFlags::VER_PASSWORD)) - { - return $rpcRequest->produceError(StandardError::FORBIDDEN, 'Password verification is not required at this time'); + throw new InvalidRpcArgumentException('password', 'Invalid SHA-512 hash'); } try { + $session = $request->getSession(); + if(!$session->flagExists(SessionFlags::VER_PASSWORD)) + { + return $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, 'Password verification is not required at this time'); + } + $result = PasswordManager::verifyPassword($request->getPeer()->getUuid(), $rpcRequest->getParameter('password')); if($result) From 0a13183a0e634f52e6e1e4bcb46e7cc48cd11df2 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Feb 2025 13:25:16 -0500 Subject: [PATCH 330/420] Removed composer.json (Ew!) --- composer.json | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 composer.json diff --git a/composer.json b/composer.json deleted file mode 100644 index fc11b64..0000000 --- a/composer.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "vendor_name/socialbox-php", - "description": "description", - "minimum-stability": "stable", - "license": "proprietary", - "authors": [ - { - "name": "Netkas", - "email": "netkas@nosial.net" - } - ], - "require": { - "ext-pdo": "*", - "ext-openssl": "*", - "ext-redis": "*", - "ext-memcached": "*", - "ext-curl": "*", - "ext-gd": "*", - "ext-sodium": "*" - } -} \ No newline at end of file From f74a91963e285c28766c451b9c33880361b581a6 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Feb 2025 13:38:41 -0500 Subject: [PATCH 331/420] Made message signing in Cryptography use SHA512 as the message content for signatures rather than the message content itself to allow for simplified verification in the future --- src/Socialbox/Classes/Cryptography.php | 64 +++++++++++++++++++------- 1 file changed, 47 insertions(+), 17 deletions(-) diff --git a/src/Socialbox/Classes/Cryptography.php b/src/Socialbox/Classes/Cryptography.php index d4337a9..b7b6e2c 100644 --- a/src/Socialbox/Classes/Cryptography.php +++ b/src/Socialbox/Classes/Cryptography.php @@ -297,20 +297,32 @@ * @return string The base64-encoded digital signature. * @throws CryptographyException If the message or private key is invalid, or if signing fails. */ - public static function signMessage(string $message, string $privateKey): string + public static function signMessage(string $message, string $privateKey, bool $hash=true): string { + if (empty($message)) + { + throw new CryptographyException("Empty message provided"); + } + + if (empty($privateKey)) + { + throw new CryptographyException("Empty private key provided"); + } + + if($hash) + { + $message = hash('sha512', $message); + } + else + { + if(!self::validateSha512($message)) + { + throw new CryptographyException("Invalid SHA-512 hash provided"); + } + } + try { - if (empty($message)) - { - throw new CryptographyException("Empty message provided"); - } - - if (empty($privateKey)) - { - throw new CryptographyException("Empty private key provided"); - } - $privateKey = self::validateAndExtractKey($privateKey, self::KEY_TYPE_SIGNING); $decodedKey = sodium_base642bin($privateKey, self::BASE64_VARIANT, true); @@ -326,6 +338,11 @@ } catch (Exception $e) { + if($e instanceof CryptographyException) + { + throw $e; + } + throw new CryptographyException("Failed to sign message: " . $e->getMessage()); } } @@ -336,18 +353,31 @@ * @param string $message The original message that was signed. * @param string $signature The base64-encoded signature to be verified. * @param string $publicKey The base64-encoded public key used to verify the signature. + * @param bool $hash True to hash the message before verification, false to use the message directly. * @return bool True if the signature is valid; false otherwise. * @throws CryptographyException If any parameter is empty, if the public key or signature is invalid, or if the verification process fails. */ - public static function verifyMessage(string $message, string $signature, string $publicKey): bool + public static function verifyMessage(string $message, string $signature, string $publicKey, bool $hash=true): bool { + if (empty($message) || empty($signature) || empty($publicKey)) + { + throw new CryptographyException("Empty parameter(s) provided"); + } + + if($hash) + { + $message = hash('sha512', $message); + } + else + { + if(!self::validateSha512($message)) + { + throw new CryptographyException("Invalid SHA-512 hash provided"); + } + } + try { - if (empty($message) || empty($signature) || empty($publicKey)) - { - throw new CryptographyException("Empty parameter(s) provided"); - } - $publicKey = self::validateAndExtractKey($publicKey, self::KEY_TYPE_SIGNING); $decodedKey = sodium_base642bin($publicKey, self::BASE64_VARIANT, true); $decodedSignature = sodium_base642bin($signature, self::BASE64_VARIANT, true); From 3472728f3a2b625b1ab2ea1b4aff8e012ec502a9 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Feb 2025 13:41:48 -0500 Subject: [PATCH 332/420] Added missing method --- src/Socialbox/Exceptions/RequestException.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Socialbox/Exceptions/RequestException.php b/src/Socialbox/Exceptions/RequestException.php index 8d09517..488dc20 100644 --- a/src/Socialbox/Exceptions/RequestException.php +++ b/src/Socialbox/Exceptions/RequestException.php @@ -3,6 +3,7 @@ namespace Socialbox\Exceptions; use Exception; + use Socialbox\Enums\StandardError; use Throwable; class RequestException extends Exception @@ -18,4 +19,14 @@ { parent::__construct($message, $code, $previous); } + + /** + * Gets the standard error associated with the exception code. + * + * @return StandardError The standard error associated with the exception code. + */ + public function getStandardError(): StandardError + { + return StandardError::tryFrom($this->getCode()) ?? StandardError::UNKNOWN; + } } \ No newline at end of file From 0e190c085a5ec0db5df72fc585bbb40c45ce2c1e Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Feb 2025 13:59:11 -0500 Subject: [PATCH 333/420] Added method signTimedMessage() and verifyTimedMessage() for verifying messages based on signed timestamps --- src/Socialbox/Classes/Cryptography.php | 75 +++++++++++++++++++ .../Status/SignatureVerificationStatus.php | 21 ++++++ src/Socialbox/Socialbox.php | 51 +++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 src/Socialbox/Enums/Status/SignatureVerificationStatus.php diff --git a/src/Socialbox/Classes/Cryptography.php b/src/Socialbox/Classes/Cryptography.php index b7b6e2c..9d83e9d 100644 --- a/src/Socialbox/Classes/Cryptography.php +++ b/src/Socialbox/Classes/Cryptography.php @@ -347,6 +347,43 @@ } } + /** + * Signs a message using the provided private key and a timestamp. + * + * @param string $message The message to be signed. + * @param string $privateKey The base64-encoded private key used for signing. + * @param int $timestamp The timestamp to be included in the signed message. + * @param bool $hash True to hash the message before signing, false to use the message directly. + * @return string The base64-encoded digital signature. + * @throws CryptographyException If the message or private key is invalid, or if signing fails. + */ + public static function signTimedMessage(string $message, string $privateKey, int $timestamp, bool $hash=true): string + { + if (empty($message)) + { + throw new CryptographyException("Empty message provided"); + } + + if($timestamp <= 0) + { + throw new CryptographyException("Invalid timestamp provided"); + } + + if($hash) + { + $message = hash('sha512', $message); + } + else + { + if(!self::validateSha512($message)) + { + throw new CryptographyException("Invalid SHA-512 hash provided"); + } + } + + return self::signMessage(sprintf("digest:%s;timestamp:%d", $message, $timestamp), $privateKey); + } + /** * Verifies the validity of a given signature for a message using the provided public key. * @@ -405,6 +442,44 @@ } } + /** + * Verifies the validity of a given signature for a message using the provided public key and timestamp. + * + * @param string $message The original message that was signed. + * @param string $signature The base64-encoded signature to be verified. + * @param string $publicKey The base64-encoded public key used to verify the signature. + * @param int $timestamp The timestamp to be included in the signed message. + * @param bool $hash True to hash the message before verification, false to use the message directly. + * @return bool True if the signature is valid; false otherwise. + * @throws CryptographyException If any parameter is empty, if the public key or signature is invalid, or if the verification process fails. + */ + public static function verifyTimedMessage(string $message, string $signature, string $publicKey, int $timestamp, bool $hash=true): bool + { + if (empty($message) || empty($signature) || empty($publicKey)) + { + throw new CryptographyException("Empty parameter(s) provided"); + } + + if($timestamp <= 0) + { + throw new CryptographyException("Invalid timestamp provided"); + } + + if($hash) + { + $message = hash('sha512', $message); + } + else + { + if(!self::validateSha512($message)) + { + throw new CryptographyException("Invalid SHA-512 hash provided"); + } + } + + return self::verifyMessage(sprintf("digest:%s;timestamp:%d", $message, $timestamp), $signature, $publicKey); + } + /** * Determines if the provided algorithm is supported. * diff --git a/src/Socialbox/Enums/Status/SignatureVerificationStatus.php b/src/Socialbox/Enums/Status/SignatureVerificationStatus.php new file mode 100644 index 0000000..17f4055 --- /dev/null +++ b/src/Socialbox/Enums/Status/SignatureVerificationStatus.php @@ -0,0 +1,21 @@ +getPublicKey(), false)) + { + return SignatureVerificationStatus::INVALID; + } + } + catch (CryptographyException) + { + return SignatureVerificationStatus::INVALID; + } + } + /** * Resolves a peer signature key based on the given peer address or string identifier. * From 7485733219f20663f28bc32d218fd8160c5c640a Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Feb 2025 14:11:07 -0500 Subject: [PATCH 334/420] Added method verifyPeerSignature(PeerAddress|string $signingPeer, string $signatureUuid, string $signatureKey, string $signature, string $messageHash, int $signatureTime): SignatureVerificationStatus --- .../Status/SignatureVerificationStatus.php | 17 ++++++++- src/Socialbox/Socialbox.php | 37 ++++++++++++++++--- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/Socialbox/Enums/Status/SignatureVerificationStatus.php b/src/Socialbox/Enums/Status/SignatureVerificationStatus.php index 17f4055..3fb4cb0 100644 --- a/src/Socialbox/Enums/Status/SignatureVerificationStatus.php +++ b/src/Socialbox/Enums/Status/SignatureVerificationStatus.php @@ -10,7 +10,17 @@ case INVALID = 'INVALID'; /** - * The provided signature was valid but the key associated with the signature has expired. + * The provided signature was valid but the public key used to verify the signature was not the expected public key. + */ + case PUBLIC_KEY_MISMATCH = 'PUBLIC_KEY_MISMATCH'; + + /** + * The provided signature was valid but the UUID used to verify the signature was not the expected UUID. + */ + case UUID_MISMATCH = 'UUID_MISMATCH'; + + /** + * The provided signature was valid but the signing key has expired. */ case EXPIRED = 'EXPIRED'; @@ -18,4 +28,9 @@ * The provided signature was valid but unable to be verified against the peer's known public key. */ case UNVERIFIED = 'UNVERIFIED'; + + /** + * The provided signature was valid and verified locally and against the peer's known public key successfully. + */ + case VERIFIED = 'VERIFIED'; } diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index e0e598c..79c2b30 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -15,6 +15,7 @@ use Socialbox\Enums\PrivacyState; use Socialbox\Enums\ReservedUsernames; use Socialbox\Enums\SessionState; + use Socialbox\Enums\SigningKeyState; use Socialbox\Enums\StandardError; use Socialbox\Enums\StandardHeaders; use Socialbox\Enums\StandardMethods; @@ -751,6 +752,11 @@ } /** + * Verifies the signature of a message using the public key of the signing peer both locally and externally. + * If the peer is registered locally, the signature is verified using the local public key. + * If the peer is external, the signature is verified by resolving the peer's public key from the external server. + * The signature is verified against the resolved public key, and the status of the verification is returned. + * * @param PeerAddress|string $signingPeer The peer address or string identifier of the signing peer * @param string $signatureUuid The UUID of the signature key to be resolved * @param string $signatureKey The public key of the signature that was used to sign the message @@ -761,12 +767,9 @@ */ public static function verifyPeerSignature(PeerAddress|string $signingPeer, string $signatureUuid, string $signatureKey, string $signature, string $messageHash, int $signatureTime): SignatureVerificationStatus { - $messageHash = sprintf('%s:%d', $messageHash, $signatureTime); - - // First verify the signature with the provided parameters try { - if (!Cryptography::verifyMessage($messageHash, $signature, $signatureKey, false)) + if (!Cryptography::verifyTimedMessage($messageHash, $signature, $signatureKey, $signatureTime, false)) { return SignatureVerificationStatus::INVALID; } @@ -779,17 +782,37 @@ // Resolve the peer signature key try { - $signingKey = self::resolvePeerSignature($peerAddress, $signatureUuid); + $signingKey = self::resolvePeerSignature($signingPeer, $signatureUuid); } catch(StandardRpcException) { return SignatureVerificationStatus::UNVERIFIED; } + if($signingKey === null) + { + return SignatureVerificationStatus::UNVERIFIED; + } + + if($signingKey->getPublicKey() !== $signatureKey) + { + return SignatureVerificationStatus::PUBLIC_KEY_MISMATCH; + } + + if($signingKey->getUuid() !== $signatureUuid) + { + return SignatureVerificationStatus::UUID_MISMATCH; + } + + if(time() > $signingKey->getExpires()) + { + return SignatureVerificationStatus::EXPIRED; + } + // Verify the signature with the resolved key try { - if (!Cryptography::verifyMessage($messageHash, $signature, $signingKey->getPublicKey(), false)) + if (!Cryptography::verifyTimedMessage($messageHash, $signature, $signingKey->getPublicKey(), $signatureTime, false)) { return SignatureVerificationStatus::INVALID; } @@ -798,6 +821,8 @@ { return SignatureVerificationStatus::INVALID; } + + return SignatureVerificationStatus::VERIFIED; } /** From c4eefd53568b010ed14de15ba3bf7b0acaa79947 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Feb 2025 14:52:17 -0500 Subject: [PATCH 335/420] Improved exception handling --- .../Core/ResolvePeerSignature.php | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/Core/ResolvePeerSignature.php b/src/Socialbox/Classes/StandardMethods/Core/ResolvePeerSignature.php index 10b483a..d9248f4 100644 --- a/src/Socialbox/Classes/StandardMethods/Core/ResolvePeerSignature.php +++ b/src/Socialbox/Classes/StandardMethods/Core/ResolvePeerSignature.php @@ -6,6 +6,8 @@ use InvalidArgumentException; use Socialbox\Abstracts\Method; use Socialbox\Enums\StandardError; + use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; + use Socialbox\Exceptions\Standard\MissingRpcArgumentException; use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\ClientRequest; @@ -25,12 +27,12 @@ // Check if the required 'peer' parameter is set. if(!$rpcRequest->containsParameter('peer')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'peer' parameter"); + throw new MissingRpcArgumentException('peer'); } if(!$rpcRequest->containsParameter('uuid')) { - return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, "Missing 'uuid' parameter"); + throw new MissingRpcArgumentException('uuid'); } try @@ -39,7 +41,7 @@ } catch(InvalidArgumentException $e) { - throw new StandardRpcException('Invalid UUID', StandardError::RPC_INVALID_ARGUMENTS, $e); + throw new InvalidRpcArgumentException('uuid', $e); } // Parse the peer address @@ -49,20 +51,9 @@ } catch(InvalidArgumentException $e) { - throw new StandardRpcException('Peer Address Error: ' . $e->getMessage(), StandardError::RPC_INVALID_ARGUMENTS, $e); + throw new InvalidRpcArgumentException('peer', $e); } - try - { - return $rpcRequest->produceResponse(Socialbox::resolvePeerSignature($peerAddress, $uuid->toRfc4122())); - } - catch(StandardRpcException $e) - { - throw $e; - } - catch (Exception $e) - { - throw new StandardRpcException('Failed to resolve peer signature', StandardError::INTERNAL_SERVER_ERROR, $e); - } + return $rpcRequest->produceResponse(Socialbox::resolvePeerSignature($peerAddress, $uuid->toRfc4122())); } } \ No newline at end of file From 8920080ada6c5705e2be086ffbf2f85e19e10640 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Feb 2025 15:00:37 -0500 Subject: [PATCH 336/420] Added method VerifyPeerSignature --- .../Core/VerifyPeerSignature.php | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/Socialbox/Classes/StandardMethods/Core/VerifyPeerSignature.php diff --git a/src/Socialbox/Classes/StandardMethods/Core/VerifyPeerSignature.php b/src/Socialbox/Classes/StandardMethods/Core/VerifyPeerSignature.php new file mode 100644 index 0000000..0d7f079 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/Core/VerifyPeerSignature.php @@ -0,0 +1,83 @@ +containsParameter('signing_peer')) + { + throw new MissingRpcArgumentException('signing_peer'); + } + + if(!$rpcRequest->containsParameter('signature_uuid')) + { + throw new MissingRpcArgumentException('signature_uuid'); + } + + if(!$rpcRequest->containsParameter('signature_key')) + { + throw new MissingRpcArgumentException('signature_key'); + } + + if(!$rpcRequest->containsParameter('signature')) + { + throw new MissingRpcArgumentException('signature'); + } + + if(!$rpcRequest->containsParameter('message_hash')) + { + throw new MissingRpcArgumentException('message_hash'); + } + + if(!$rpcRequest->containsParameter('signature_time')) + { + throw new MissingRpcArgumentException('signature_time'); + } + + // Parse the peer address + try + { + $peerAddress = PeerAddress::fromAddress($rpcRequest->getParameter('signing_peer')); + } + catch(InvalidArgumentException $e) + { + throw new InvalidRpcArgumentException('signing_peer', $e); + } + + try + { + return $rpcRequest->produceResponse(Socialbox::verifyPeerSignature( + signingPeer: $peerAddress, + signatureUuid: $rpcRequest->getParameter('signature_uuid'), + signatureKey: $rpcRequest->getParameter('signature_key'), + signature: $rpcRequest->getParameter('signature'), + messageHash: $rpcRequest->getParameter('message_hash'), + signatureTime: $rpcRequest->getParameter('signature_time') + )->value); + } + catch (Exception $e) + { + throw new StandardRpcException('Failed to resolve peer signature', StandardError::INTERNAL_SERVER_ERROR, $e); + } + } + } \ No newline at end of file From a7fc8e87d9b44c5d302e9538d2fe5974a20b05eb Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Feb 2025 15:22:44 -0500 Subject: [PATCH 337/420] Added method SettingsInformationFieldExists --- .../SettingsInformationFieldExists.php | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/Socialbox/Classes/StandardMethods/Settings/SettingsInformationFieldExists.php diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsInformationFieldExists.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsInformationFieldExists.php new file mode 100644 index 0000000..3823e81 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsInformationFieldExists.php @@ -0,0 +1,46 @@ +containsParameter('field')) + { + throw new MissingRpcArgumentException('field'); + } + $fieldName = InformationFieldName::tryFrom(strtoupper($rpcRequest->getParameter('field'))); + if($fieldName === null) + { + throw new InvalidRpcArgumentException('field'); + } + + try + { + return $rpcRequest->produceResponse(PeerInformationManager::fieldExists($request->getPeer(), $fieldName)); + + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to check if the requested information field exists or not', StandardError::INTERNAL_SERVER_ERROR, $e); + } + } + } \ No newline at end of file From cf1bd2e0570aa78ad2a024432d9d0cf814848fb0 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Feb 2025 17:53:39 -0500 Subject: [PATCH 338/420] Renamed Authenticate to VerificationAuthenticate --- .../{Authenticate.php => VerificationAuthenticate.php} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/Socialbox/Classes/StandardMethods/Verification/{Authenticate.php => VerificationAuthenticate.php} (96%) diff --git a/src/Socialbox/Classes/StandardMethods/Verification/Authenticate.php b/src/Socialbox/Classes/StandardMethods/Verification/VerificationAuthenticate.php similarity index 96% rename from src/Socialbox/Classes/StandardMethods/Verification/Authenticate.php rename to src/Socialbox/Classes/StandardMethods/Verification/VerificationAuthenticate.php index 03bea68..25ed7c1 100644 --- a/src/Socialbox/Classes/StandardMethods/Verification/Authenticate.php +++ b/src/Socialbox/Classes/StandardMethods/Verification/VerificationAuthenticate.php @@ -12,7 +12,7 @@ use Socialbox\Objects\ClientRequest; use Socialbox\Objects\RpcRequest; - class Authenticate extends Method + class VerificationAuthenticate extends Method { /** * @inheritDoc From 4edb34aab15fab9684a6ab80b0f2dda866a66d3c Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Feb 2025 17:55:18 -0500 Subject: [PATCH 339/420] Refactored StandardMethods to be more organized --- src/Socialbox/Enums/StandardMethods.php | 412 ++++++++++++++++-------- 1 file changed, 278 insertions(+), 134 deletions(-) diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index e706ab8..1e41102 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -4,15 +4,20 @@ use Socialbox\Classes\Configuration; use Socialbox\Classes\StandardMethods\AddressBook\AddressBookAddContact; + use Socialbox\Classes\StandardMethods\AddressBook\AddressBookContactExists; use Socialbox\Classes\StandardMethods\AddressBook\AddressBookDeleteContact; + use Socialbox\Classes\StandardMethods\AddressBook\AddressBookGetContact; use Socialbox\Classes\StandardMethods\AddressBook\AddressBookGetContacts; + use Socialbox\Classes\StandardMethods\AddressBook\AddressBookRevokeSignature; use Socialbox\Classes\StandardMethods\AddressBook\AddressBookTrustSignature; use Socialbox\Classes\StandardMethods\AddressBook\AddressBookUpdateRelationship; use Socialbox\Classes\StandardMethods\Core\GetAllowedMethods; + use Socialbox\Classes\StandardMethods\Core\GetSelf; use Socialbox\Classes\StandardMethods\Core\GetSessionState; use Socialbox\Classes\StandardMethods\Core\Ping; use Socialbox\Classes\StandardMethods\Core\ResolvePeer; use Socialbox\Classes\StandardMethods\Core\ResolvePeerSignature; + use Socialbox\Classes\StandardMethods\Core\VerifyPeerSignature; use Socialbox\Classes\StandardMethods\ServerDocuments\AcceptCommunityGuidelines; use Socialbox\Classes\StandardMethods\ServerDocuments\AcceptPrivacyPolicy; use Socialbox\Classes\StandardMethods\ServerDocuments\AcceptTermsOfService; @@ -29,12 +34,14 @@ use Socialbox\Classes\StandardMethods\Settings\SettingsGetInformationFields; use Socialbox\Classes\StandardMethods\Settings\SettingsGetSigningKey; use Socialbox\Classes\StandardMethods\Settings\SettingsGetSigningKeys; + use Socialbox\Classes\StandardMethods\Settings\SettingsInformationFieldExists; use Socialbox\Classes\StandardMethods\Settings\SettingsSetOtp; use Socialbox\Classes\StandardMethods\Settings\SettingsSetPassword; + use Socialbox\Classes\StandardMethods\Settings\SettingsSignatureExists; use Socialbox\Classes\StandardMethods\Settings\SettingsUpdateInformationField; use Socialbox\Classes\StandardMethods\Settings\SettingsUpdateInformationPrivacy; use Socialbox\Classes\StandardMethods\Settings\SettingsUpdatePassword; - use Socialbox\Classes\StandardMethods\Verification\Authenticate; + use Socialbox\Classes\StandardMethods\Verification\VerificationAuthenticate; use Socialbox\Classes\StandardMethods\Verification\VerificationAnswerImageCaptcha; use Socialbox\Classes\StandardMethods\Verification\VerificationGetImageCaptcha; use Socialbox\Classes\StandardMethods\Verification\VerificationOtpAuthentication; @@ -51,56 +58,73 @@ enum StandardMethods : string { - case PING = 'ping'; - case GET_SESSION_STATE = 'getSessionState'; + // AddressBook Methods + case ADDRESS_BOOK_ADD_CONTACT = 'addressBookAddContact'; + case ADDRESS_BOOK_CONTACT_EXISTS = 'addressBookContactExists'; + case ADDRESS_BOOK_DELETE_CONTACT = 'addressBookDeleteContact'; + case ADDRESS_BOOK_GET_CONTACT = 'addressBookGetContact'; + case ADDRESS_BOOK_GET_CONTACTS = 'addressBookGetContacts'; + case ADDRESS_BOOK_TRUST_SIGNATURE = 'addressBookTrustSignature'; + case ADDRESS_BOOK_REVOKE_SIGNATURE = 'addressBookRevokeSignature'; + case ADDRESS_BOOK_UPDATE_RELATIONSHIP = 'addressBookUpdateRelationship'; + + // Core Methods case GET_ALLOWED_METHODS = 'getAllowedMethods'; - - case GET_PRIVACY_POLICY = 'getPrivacyPolicy'; + case GET_SELF = 'getSelf'; + case GET_SESSION_STATE = 'getSessionState'; + case PING = 'ping'; + case RESOLVE_PEER = 'resolvePeer'; + case RESOLVE_PEER_SIGNATURE = 'resolvePeerSignature'; + case VERIFY_PEER_SIGNATURE = 'verifyPeerSignature'; + + // ServerDocument Methods + case ACCEPT_COMMUNITY_GUIDELINES = 'acceptCommunityGuidelines'; case ACCEPT_PRIVACY_POLICY = 'acceptPrivacyPolicy'; - case GET_TERMS_OF_SERVICE = 'getTermsOfService'; case ACCEPT_TERMS_OF_SERVICE = 'acceptTermsOfService'; case GET_COMMUNITY_GUIDELINES = 'getCommunityGuidelines'; - case ACCEPT_COMMUNITY_GUIDELINES = 'acceptCommunityGuidelines'; + case GET_PRIVACY_POLICY = 'getPrivacyPolicy'; + case GET_TERMS_OF_SERVICE = 'getTermsOfService'; + // Settings Methods + case SETTINGS_ADD_INFORMATION_FIELD = 'settingsAddInformationField'; + case SETTINGS_ADD_SIGNATURE = 'settingsAddSigningKey'; + case SETTINGS_DELETE_INFORMATION_FIELD = 'settingsDeleteInformationField'; + case SETTINGS_DELETE_OTP = 'settingsDeleteOtp'; + case SETTINGS_DELETE_PASSWORD = 'settingsDeletePassword'; + case SETTINGS_DELETE_SIGNATURE = 'settingsDeleteSigningKey'; + case SETTINGS_GET_INFORMATION_FIELD = 'settingsGetInformationField'; + case SETTINGS_GET_INFORMATION_FIELDS = 'settingsGetInformationFields'; + case SETTINGS_GET_SIGNATURE = 'settingsGetSigningKey'; + case SETTINGS_GET_SIGNATURES = 'settingsGetSigningKeys'; + case SETTINGS_INFORMATION_FIELD_EXISTS = 'settingsInformationFieldExists'; + case SETTINGS_SET_OTP = 'settingsSetOtp'; + case SETTINGS_SET_PASSWORD = 'settingsSetPassword'; + case SETTINGS_SIGNATURE_EXISTS = 'settingsSignatureExists'; + case SETTINGS_UPDATE_INFORMATION_FIELD = 'settingsUpdateInformationField'; + case SETTINGS_UPDATE_INFORMATION_PRIVACY = 'settingsUpdateInformationPrivacy'; + case SETTINGS_UPDATE_PASSWORD = 'settingsUpdatePassword'; + + // Verification Methods + case VERIFICATION_ANSWER_IMAGE_CAPTCHA = 'verificationAnswerImageCaptcha'; case VERIFICATION_AUTHENTICATE = 'authenticate'; + case VERIFICATION_GET_IMAGE_CAPTCHA = 'verificationGetImageCaptcha'; + case VERIFICATION_OTP_AUTHENTICATION = 'verificationOtpAuthentication'; + case VERIFICATION_PASSWORD_AUTHENTICATION = 'verificationPasswordAuthentication'; + // NOT IMPLEMENTED VERIFICATION METHODS case VERIFICATION_EMAIL = 'verificationEmail'; // NOT IMPLEMENTED case VERIFICATION_ANSWER_EMAIL = 'verificationAnswerEmail'; // NOT IMPLEMENTED case VERIFICATION_SMS = 'verificationSms'; // NOT IMPLEMENTED case VERIFICATION_ANSWER_SMS = 'verificationAnswerSms'; // NOT IMPLEMENTED case VERIFICATION_PHONE_CALL = 'verificationPhoneCall'; // NOT IMPLEMENTED case VERIFICATION_ANSWER_PHONE_CALL = 'verificationAnswerPhoneCall'; // NOT IMPLEMENTED - case VERIFICATION_GET_IMAGE_CAPTCHA = 'verificationGetImageCaptcha'; - case VERIFICATION_ANSWER_IMAGE_CAPTCHA = 'verificationAnswerImageCaptcha'; case VERIFICATION_GET_TEXT_CAPTCHA = 'verificationGetTextCaptcha'; // NOT IMPLEMENTED case VERIFICATION_ANSWER_TEXT_CAPTCHA = 'verificationAnswerTextCaptcha'; // NOT IMPLEMENTED case VERIFICATION_GET_EXTERNAL_URL = 'verificationGetExternalUrl'; // NOT IMPLEMENTED case VERIFICATION_ANSWER_EXTERNAL_URL = 'verificationAnswerExternalUrl'; // NOT IMPLEMENTED - case VERIFICATION_PASSWORD_AUTHENTICATION = 'verificationPasswordAuthentication'; - case VERIFICATION_OTP_AUTHENTICATION = 'verificationOtpAuthentication'; - case SETTINGS_SET_PASSWORD = 'settingsSetPassword'; - case SETTINGS_UPDATE_PASSWORD = 'settingsUpdatePassword'; - case SETTINGS_DELETE_PASSWORD = 'settingsDeletePassword'; - case SETTINGS_SET_OTP = 'settingsSetOtp'; - case SETTINGS_DELETE_OTP = 'settingsDeleteOtp'; - case SETTINGS_ADD_INFORMATION_FIELD = 'settingsAddInformationField'; - case SETTINGS_GET_INFORMATION_FIELDS = 'settingsGetInformationFields'; - case SETTINGS_GET_INFORMATION_FIELD = 'settingsGetInformationField'; - case SETTINGS_UPDATE_INFORMATION_FIELD = 'settingsUpdateInformationField'; - case SETTINGS_DELETE_INFORMATION_FIELD = 'settingsDeleteInformationField'; - case SETTINGS_UPDATE_INFORMATION_PRIVACY = 'settingsUpdateInformationPrivacy'; - - case SETTINGS_ADD_SIGNATURE = 'settingsAddSigningKey'; - case SETTINGS_DELETE_SIGNATURE = 'settingsDeleteSigningKey'; - case SETTINGS_GET_SIGNATURES = 'settingsGetSigningKeys'; - case SETTINGS_GET_SIGNATURE = 'settingsGetSigningKey'; - - case ADDRESS_BOOK_ADD_CONTACT = 'addressBookAddContact'; - case ADDRESS_BOOK_DELETE_CONTACT = 'addressBookDeleteContact'; - case ADDRESS_BOOK_GET_CONTACTS = 'addressBookGetContacts'; - case ADDRESS_BOOK_UPDATE_RELATIONSHIP = 'addressBookUpdateRelationship'; - case ADDRESS_BOOK_TRUST_SIGNATURE = 'addressBookTrustSignature'; + // TODO: COMPLETE THE REST + // MISC case GET_STATE = 'getState'; // End-to-End channels for communication purposes @@ -137,9 +161,6 @@ case MESSAGES_SET_MESSAGE_BODY = 'messagesSetMessageBody'; case MESSAGES_SEND_MESSAGE = 'messagesSendMessage'; - case RESOLVE_PEER = 'resolvePeer'; - case RESOLVE_PEER_SIGNATURE = 'resolvePeerSignature'; - /** * Executes the appropriate operation based on the current context and requests provided. * @@ -152,51 +173,60 @@ { return match ($this) { - self::PING => Ping::execute($request, $rpcRequest), - self::GET_SESSION_STATE => GetSessionState::execute($request, $rpcRequest), - self::GET_ALLOWED_METHODS => GetAllowedMethods::execute($request, $rpcRequest), - - self::GET_PRIVACY_POLICY => GetPrivacyPolicy::execute($request, $rpcRequest), - self::ACCEPT_PRIVACY_POLICY => AcceptPrivacyPolicy::execute($request, $rpcRequest), - self::GET_TERMS_OF_SERVICE => GetTermsOfService::execute($request, $rpcRequest), - self::ACCEPT_TERMS_OF_SERVICE => AcceptTermsOfService::execute($request, $rpcRequest), - self::GET_COMMUNITY_GUIDELINES => GetCommunityGuidelines::execute($request, $rpcRequest), - self::ACCEPT_COMMUNITY_GUIDELINES => AcceptCommunityGuidelines::execute($request, $rpcRequest), - - self::VERIFICATION_GET_IMAGE_CAPTCHA => VerificationGetImageCaptcha::execute($request, $rpcRequest), - self::VERIFICATION_ANSWER_IMAGE_CAPTCHA => VerificationAnswerImageCaptcha::execute($request, $rpcRequest), - - self::VERIFICATION_PASSWORD_AUTHENTICATION => VerificationPasswordAuthentication::execute($request, $rpcRequest), - self::VERIFICATION_OTP_AUTHENTICATION => VerificationOtpAuthentication::execute($request, $rpcRequest), - - self::SETTINGS_SET_PASSWORD => SettingsSetPassword::execute($request, $rpcRequest), - self::SETTINGS_UPDATE_PASSWORD => SettingsUpdatePassword::execute($request, $rpcRequest), - self::SETTINGS_DELETE_PASSWORD => SettingsDeletePassword::execute($request, $rpcRequest), - self::SETTINGS_SET_OTP => SettingsSetOtp::execute($request, $rpcRequest), - self::SETTINGS_DELETE_OTP => SettingsDeleteOtp::execute($request, $rpcRequest), - - self::SETTINGS_ADD_INFORMATION_FIELD => SettingsAddInformationField::execute($request, $rpcRequest), - self::SETTINGS_GET_INFORMATION_FIELDS => SettingsGetInformationFields::execute($request, $rpcRequest), - self::SETTINGS_GET_INFORMATION_FIELD => SettingsGetInformationField::execute($request, $rpcRequest), - self::SETTINGS_UPDATE_INFORMATION_FIELD => SettingsUpdateInformationField::execute($request, $rpcRequest), - self::SETTINGS_UPDATE_INFORMATION_PRIVACY => SettingsUpdateInformationPrivacy::execute($request, $rpcRequest), - self::SETTINGS_DELETE_INFORMATION_FIELD => SettingsDeleteInformationField::execute($request, $rpcRequest), - - self::SETTINGS_ADD_SIGNATURE => SettingsAddSignature::execute($request, $rpcRequest), - self::SETTINGS_DELETE_SIGNATURE => SettingsDeleteSignature::execute($request, $rpcRequest), - self::SETTINGS_GET_SIGNATURES => SettingsGetSigningKeys::execute($request, $rpcRequest), - self::SETTINGS_GET_SIGNATURE => SettingsGetSigningKey::execute($request, $rpcRequest), - + // AddressBook Methods self::ADDRESS_BOOK_ADD_CONTACT => AddressBookAddContact::execute($request, $rpcRequest), + self::ADDRESS_BOOK_CONTACT_EXISTS => AddressBookContactExists::execute($request, $rpcRequest), self::ADDRESS_BOOK_DELETE_CONTACT => AddressBookDeleteContact::execute($request, $rpcRequest), + self::ADDRESS_BOOK_GET_CONTACT => AddressBookGetContact::execute($request, $rpcRequest), self::ADDRESS_BOOK_GET_CONTACTS => AddressBookGetContacts::execute($request, $rpcRequest), - self::ADDRESS_BOOK_UPDATE_RELATIONSHIP => AddressBookUpdateRelationship::execute($request, $rpcRequest), self::ADDRESS_BOOK_TRUST_SIGNATURE => AddressBookTrustSignature::execute($request, $rpcRequest), + self::ADDRESS_BOOK_REVOKE_SIGNATURE => AddressBookRevokeSignature::execute($request, $rpcRequest), + self::ADDRESS_BOOK_UPDATE_RELATIONSHIP => AddressBookUpdateRelationship::execute($request, $rpcRequest), - self::VERIFICATION_AUTHENTICATE => Authenticate::execute($request, $rpcRequest), + // Core Methods + self::GET_ALLOWED_METHODS => GetAllowedMethods::execute($request, $rpcRequest), + self::GET_SELF => GetSelf::execute($request, $rpcRequest), + self::GET_SESSION_STATE => GetSessionState::execute($request, $rpcRequest), + self::PING => Ping::execute($request, $rpcRequest), self::RESOLVE_PEER => ResolvePeer::execute($request, $rpcRequest), self::RESOLVE_PEER_SIGNATURE => ResolvePeerSignature::execute($request, $rpcRequest), + self::VERIFY_PEER_SIGNATURE => VerifyPeerSignature::execute($request, $rpcRequest), + // Server Document Methods + self::ACCEPT_PRIVACY_POLICY => AcceptPrivacyPolicy::execute($request, $rpcRequest), + self::ACCEPT_COMMUNITY_GUIDELINES => AcceptCommunityGuidelines::execute($request, $rpcRequest), + self::ACCEPT_TERMS_OF_SERVICE => AcceptTermsOfService::execute($request, $rpcRequest), + self::GET_COMMUNITY_GUIDELINES => GetCommunityGuidelines::execute($request, $rpcRequest), + self::GET_PRIVACY_POLICY => GetPrivacyPolicy::execute($request, $rpcRequest), + self::GET_TERMS_OF_SERVICE => GetTermsOfService::execute($request, $rpcRequest), + + // Settings Methods + self::SETTINGS_ADD_INFORMATION_FIELD => SettingsAddInformationField::execute($request, $rpcRequest), + self::SETTINGS_ADD_SIGNATURE => SettingsAddSignature::execute($request, $rpcRequest), + self::SETTINGS_DELETE_INFORMATION_FIELD => SettingsDeleteInformationField::execute($request, $rpcRequest), + self::SETTINGS_DELETE_OTP => SettingsDeleteOtp::execute($request, $rpcRequest), + self::SETTINGS_DELETE_PASSWORD => SettingsDeletePassword::execute($request, $rpcRequest), + self::SETTINGS_DELETE_SIGNATURE => SettingsDeleteSignature::execute($request, $rpcRequest), + self::SETTINGS_GET_INFORMATION_FIELD => SettingsGetInformationField::execute($request, $rpcRequest), + self::SETTINGS_GET_INFORMATION_FIELDS => SettingsGetInformationFields::execute($request, $rpcRequest), + self::SETTINGS_GET_SIGNATURE => SettingsGetSigningKey::execute($request, $rpcRequest), + self::SETTINGS_GET_SIGNATURES => SettingsGetSigningKeys::execute($request, $rpcRequest), + self::SETTINGS_INFORMATION_FIELD_EXISTS => SettingsInformationFieldExists::execute($request, $rpcRequest), + self::SETTINGS_SET_OTP => SettingsSetOtp::execute($request, $rpcRequest), + self::SETTINGS_SET_PASSWORD => SettingsSetPassword::execute($request, $rpcRequest), + self::SETTINGS_SIGNATURE_EXISTS => SettingsSignatureExists::execute($request, $rpcRequest), + self::SETTINGS_UPDATE_INFORMATION_FIELD => SettingsUpdateInformationField::execute($request, $rpcRequest), + self::SETTINGS_UPDATE_INFORMATION_PRIVACY => SettingsUpdateInformationPrivacy::execute($request, $rpcRequest), + self::SETTINGS_UPDATE_PASSWORD => SettingsUpdatePassword::execute($request, $rpcRequest), + + // Verification Methods + self::VERIFICATION_ANSWER_IMAGE_CAPTCHA => VerificationAnswerImageCaptcha::execute($request, $rpcRequest), + self::VERIFICATION_AUTHENTICATE => VerificationAuthenticate::execute($request, $rpcRequest), + self::VERIFICATION_GET_IMAGE_CAPTCHA => VerificationGetImageCaptcha::execute($request, $rpcRequest), + self::VERIFICATION_OTP_AUTHENTICATION => VerificationOtpAuthentication::execute($request, $rpcRequest), + self::VERIFICATION_PASSWORD_AUTHENTICATION => VerificationPasswordAuthentication::execute($request, $rpcRequest), + + // Default Unknown/Not Implemented default => $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, sprintf("The method %s is not supported by the server", $rpcRequest->getMethod())) }; } @@ -230,16 +260,7 @@ public static function getAllowedMethods(ClientRequest $clientRequest): array { // These methods should always accessible - $methods = [ - // Important methods - self::PING, // Always allow the ping method - self::GET_SESSION_STATE, // The session state should always be accessible - self::GET_ALLOWED_METHODS, // Client should always be able to get the allowed methods - self::GET_PRIVACY_POLICY, // The user should always be able to get the privacy policy - self::GET_TERMS_OF_SERVICE, // The user should always be able to get the terms of service - self::GET_COMMUNITY_GUIDELINES, // The user should always be able to get the community guidelines - ]; - + $methods = self::getCoreMethods(); $session = $clientRequest->getSession(); if($session === null) @@ -265,7 +286,7 @@ // If the session is authenticated, then allow additional method calls elseif($session->isAuthenticated()) { - $methods = array_merge($methods, self::getAuthenticatedMethods()); + $methods = array_merge($methods, self::getAuthenticatedMethods($session)); } // If the session isn't authenticated, check if it's a registering user elseif($session->flagExists(SessionFlags::REGISTRATION_REQUIRED)) @@ -311,36 +332,14 @@ * * @return array An array of methods that are available to */ - private static function getAuthenticatedMethods(): array + private static function getAuthenticatedMethods(?SessionRecord $session=null): array { - - // These methods are always allowed for authenticated users - $methods = [ - self::SETTINGS_ADD_SIGNATURE, - self::SETTINGS_GET_SIGNATURES, - self::SETTINGS_GET_SIGNATURE, - - self::SETTINGS_ADD_INFORMATION_FIELD, - self::SETTINGS_GET_INFORMATION_FIELDS, - self::SETTINGS_GET_INFORMATION_FIELD, - self::SETTINGS_UPDATE_INFORMATION_FIELD, - self::SETTINGS_UPDATE_INFORMATION_PRIVACY, - self::SETTINGS_DELETE_INFORMATION_FIELD, - - self::SETTINGS_SET_PASSWORD, - self::SETTINGS_DELETE_PASSWORD, - self::SETTINGS_UPDATE_PASSWORD, - self::SETTINGS_SET_OTP, - self::SETTINGS_DELETE_OTP, - self::RESOLVE_PEER, - self::RESOLVE_PEER_SIGNATURE, - - self::ADDRESS_BOOK_ADD_CONTACT, - self::ADDRESS_BOOK_DELETE_CONTACT, - self::ADDRESS_BOOK_GET_CONTACTS, - ]; - - return $methods; + return array_merge( + self::getAddressBookMethods(), + self::getServerDocumentMethods($session), + self::getSettingsMethods(), + self::getVerificationMethods($session) + ); } /** @@ -357,14 +356,7 @@ return []; } - $methods = [ - self::SETTINGS_ADD_INFORMATION_FIELD, - self::SETTINGS_GET_INFORMATION_FIELDS, - self::SETTINGS_GET_INFORMATION_FIELD, - self::SETTINGS_UPDATE_INFORMATION_FIELD, - self::SETTINGS_UPDATE_INFORMATION_PRIVACY, - self::SETTINGS_DELETE_INFORMATION_FIELD - ]; + $methods = self::getSettingsMethods(); // If the flag `VER_PRIVACY_POLICY` is set, then the user can accept the privacy policy if($session->flagExists(SessionFlags::VER_PRIVACY_POLICY)) @@ -391,22 +383,9 @@ $methods[] = self::VERIFICATION_ANSWER_IMAGE_CAPTCHA; } - // If the flag `SET_PASSWORD` is set, then the user has to set a password - if($session->flagExists(SessionFlags::SET_PASSWORD)) - { - $methods[] = self::SETTINGS_SET_PASSWORD; - } - - // If the flag `SET_OTP` is set, then the user has to set an OTP - if($session->flagExists(SessionFLags::SET_OTP)) - { - $methods[] = self::SETTINGS_SET_OTP; - } - return $methods; } - /** * Retrieves the list of authentication methods available for the given client request. * @@ -445,4 +424,169 @@ return $methods; } + + /** + * Returns an array of methods for managing the peer's AddressBook + * + * @param bool $readOnly If True, only methods related to reading will be returned. + * @return StandardMethods[] The array of AddressBook methods to return + */ + public static function getAddressBookMethods(bool $readOnly=false): array + { + if($readOnly) + { + return [ + self::ADDRESS_BOOK_CONTACT_EXISTS, + self::ADDRESS_BOOK_GET_CONTACT, + self::ADDRESS_BOOK_GET_CONTACTS, + ]; + } + + return [ + self::ADDRESS_BOOK_ADD_CONTACT, + self::ADDRESS_BOOK_CONTACT_EXISTS, + self::ADDRESS_BOOK_DELETE_CONTACT, + self::ADDRESS_BOOK_GET_CONTACT, + self::ADDRESS_BOOK_GET_CONTACTS, + self::ADDRESS_BOOK_REVOKE_SIGNATURE, + self::ADDRESS_BOOK_TRUST_SIGNATURE, + self::ADDRESS_BOOK_UPDATE_RELATIONSHIP + ]; + } + + /** + * Returns an array of methods for the core methods of the Socialbox RPC protocol + * + * @return StandardMethods[] An array of Core methods + */ + public static function getCoreMethods(): array + { + return [ + self::GET_ALLOWED_METHODS, + self::GET_SELF, + self::GET_SESSION_STATE, + self::PING, + self::RESOLVE_PEER, + self::RESOLVE_PEER_SIGNATURE, + self::VERIFY_PEER_SIGNATURE + ]; + } + + /** + * Returns na array of ServerDocument methods made available for the peer, if $session is false then all + * methods would be returned, otherwise the allowed methods would be returned. + * + * @param SessionRecord|null $session Optional. If null, all session will return otherwise only allowed methods would be returned + * @return StandardMethods[] An array of standard methods that are related to Server documentation + */ + public static function getServerDocumentMethods(?SessionRecord $session=null): array + { + if($session === null) + { + return [ + self::ACCEPT_COMMUNITY_GUIDELINES, + self::ACCEPT_PRIVACY_POLICY, + self::ACCEPT_TERMS_OF_SERVICE, + self::GET_COMMUNITY_GUIDELINES, + self::GET_PRIVACY_POLICY, + self::GET_TERMS_OF_SERVICE + ]; + } + + $results = [ + self::GET_COMMUNITY_GUIDELINES, + self::GET_PRIVACY_POLICY, + self::GET_TERMS_OF_SERVICE + ]; + + if($session->flagExists(SessionFLags::VER_COMMUNITY_GUIDELINES)) + { + $results[] = self::ACCEPT_COMMUNITY_GUIDELINES; + } + + if($session->flagExists(SessionFlags::VER_PRIVACY_POLICY)) + { + $results[] = self::ACCEPT_PRIVACY_POLICY; + } + + if($session->flagExists(SessionFlags::VER_TERMS_OF_SERVICE)) + { + $results[] = self::ACCEPT_TERMS_OF_SERVICE; + } + + return $results; + } + + /** + * Returns an array of setting methods that are accessible. + * + * @return StandardMethods[] + */ + public static function getSettingsMethods(): array + { + return [ + self::SETTINGS_ADD_INFORMATION_FIELD, + self::SETTINGS_ADD_SIGNATURE, + self::SETTINGS_DELETE_INFORMATION_FIELD, + self::SETTINGS_DELETE_OTP, + self::SETTINGS_DELETE_PASSWORD, + self::SETTINGS_DELETE_SIGNATURE, + self::SETTINGS_GET_INFORMATION_FIELD, + self::SETTINGS_GET_INFORMATION_FIELDS, + self::SETTINGS_GET_SIGNATURE, + self::SETTINGS_GET_SIGNATURES, + self::SETTINGS_INFORMATION_FIELD_EXISTS, + self::SETTINGS_SET_OTP, + self::SETTINGS_SET_PASSWORD, + self::SETTINGS_SIGNATURE_EXISTS, + self::SETTINGS_UPDATE_INFORMATION_FIELD, + self::SETTINGS_UPDATE_INFORMATION_PRIVACY, + self::SETTINGS_UPDATE_PASSWORD + ]; + } + + /** + * Returns an array of verification methods that are accessible, if $session is null, all methods are returned, + * otherwise only accessible methods are returned. + * + * @return StandardMethods[] + */ + public static function getVerificationMethods(?SessionRecord $session=null): array + { + if($session === null) + { + return [ + self::VERIFICATION_ANSWER_IMAGE_CAPTCHA, + self::VERIFICATION_AUTHENTICATE, + self::VERIFICATION_GET_IMAGE_CAPTCHA, + self::VERIFICATION_OTP_AUTHENTICATION, + self::VERIFICATION_PASSWORD_AUTHENTICATION + ]; + } + + $results = []; + + if($session->flagExists(SessionFlags::VER_IMAGE_CAPTCHA)) + { + $results[] = self::VERIFICATION_GET_IMAGE_CAPTCHA; + $results[] = self::VERIFICATION_ANSWER_IMAGE_CAPTCHA; + } + + if($session->flagExists(SessionFlags::VER_AUTHENTICATION)) + { + $results[] = self::VERIFICATION_AUTHENTICATE; + } + + if($session->flagExists(SessionFlags::VER_OTP)) + { + $results[] = self::VERIFICATION_OTP_AUTHENTICATION; + } + + if($session->flagExists(SessionFlags::VER_PASSWORD)) + { + $results[] = self::VERIFICATION_PASSWORD_AUTHENTICATION; + } + + return $results; + } } \ No newline at end of file From 4068862d3c81237277185416c2c81e2dfb732648 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 4 Feb 2025 15:06:41 -0500 Subject: [PATCH 340/420] Removed unused exception --- src/Socialbox/Socialbox.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index 79c2b30..9b4651d 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -873,10 +873,6 @@ return null; } } - catch(StandardRpcException $e) - { - throw $e; - } catch(Exception $e) { throw new StandardRpcException('There was an error while trying to resolve the signature key for the peer locally', StandardError::INTERNAL_SERVER_ERROR, $e); From b380df95fe0fa31710c978bba94e4e4afbb155ba Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 4 Feb 2025 15:07:05 -0500 Subject: [PATCH 341/420] Removed unused import --- src/Socialbox/Socialbox.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index 9b4651d..a734458 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -15,7 +15,6 @@ use Socialbox\Enums\PrivacyState; use Socialbox\Enums\ReservedUsernames; use Socialbox\Enums\SessionState; - use Socialbox\Enums\SigningKeyState; use Socialbox\Enums\StandardError; use Socialbox\Enums\StandardHeaders; use Socialbox\Enums\StandardMethods; From 0e08bef3bc6adf9fdf3ec210bb5e155c38deeaa4 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 6 Feb 2025 15:13:00 -0500 Subject: [PATCH 342/420] Added Encrypted channels and communication methods --- .idea/sqldialects.xml | 3 + .../Resources/database/channel_com.sql | 32 + .../Resources/database/encrypted_channels.sql | 36 ++ src/Socialbox/Enums/DatabaseObjects.php | 5 + src/Socialbox/Enums/StandardError.php | 1 + .../Enums/Status/EncryptionChannelState.php | 12 + .../Types/CommunicationRecipientType.php | 9 + .../Managers/EncryptionChannelManager.php | 568 ++++++++++++++++++ .../Objects/Database/ChannelMessageRecord.php | 141 +++++ .../Database/EncryptionChannelRecord.php | 259 ++++++++ 10 files changed, 1066 insertions(+) create mode 100644 src/Socialbox/Classes/Resources/database/channel_com.sql create mode 100644 src/Socialbox/Classes/Resources/database/encrypted_channels.sql create mode 100644 src/Socialbox/Enums/Status/EncryptionChannelState.php create mode 100644 src/Socialbox/Enums/Types/CommunicationRecipientType.php create mode 100644 src/Socialbox/Managers/EncryptionChannelManager.php create mode 100644 src/Socialbox/Objects/Database/ChannelMessageRecord.php create mode 100644 src/Socialbox/Objects/Database/EncryptionChannelRecord.php diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml index b4dcf43..8fb5dbe 100644 --- a/.idea/sqldialects.xml +++ b/.idea/sqldialects.xml @@ -4,8 +4,10 @@ + + @@ -14,6 +16,7 @@ + diff --git a/src/Socialbox/Classes/Resources/database/channel_com.sql b/src/Socialbox/Classes/Resources/database/channel_com.sql new file mode 100644 index 0000000..ab75fd7 --- /dev/null +++ b/src/Socialbox/Classes/Resources/database/channel_com.sql @@ -0,0 +1,32 @@ +create table channel_com +( + uuid varchar(36) default uuid() not null comment 'The UUID of the message', + channel_uuid varchar(36) not null comment 'The UUID of the encryption channel used', + recipient enum ('SENDER', 'RECEIVER') not null comment 'The recipient of the message', + message text not null comment 'The encrypted message content', + signature varchar(64) not null comment 'The signature of the decrypted message', + received tinyint(1) default 0 not null comment 'True if the message was received by the recipient', + timestamp timestamp default current_timestamp() not null comment 'The timestamp of the mssage being sent', + primary key (uuid, channel_uuid) comment 'The Unique Pair Index for the channel UUID and message UUID', + constraint channel_com_uuid_channel_uuid_uindex + unique (uuid, channel_uuid) comment 'The Unique Pair Index for the channel UUID and message UUID', + constraint channel_com_uuid_channel_uuid_uindex_2 + unique (uuid, channel_uuid) comment 'The Unique Index Pair for the channel UUID and message UUID', + constraint channel_com_encryption_channels_uuid_fk + foreign key (channel_uuid) references encryption_channels (uuid) + on update cascade on delete cascade +) + comment 'Table for housing communication messages over encryption channels'; + +create index channel_com_received_index + on channel_com (received) + comment 'The index for the received column'; + +create index channel_com_recipient_index + on channel_com (recipient) + comment 'The index for the recipient column'; + +create index channel_com_timestamp_index + on channel_com (timestamp) + comment 'The index for the Timestamp column'; + diff --git a/src/Socialbox/Classes/Resources/database/encrypted_channels.sql b/src/Socialbox/Classes/Resources/database/encrypted_channels.sql new file mode 100644 index 0000000..f906d93 --- /dev/null +++ b/src/Socialbox/Classes/Resources/database/encrypted_channels.sql @@ -0,0 +1,36 @@ +create table encryption_channels +( + uuid varchar(36) not null comment 'The Unique Universal Identifier for the encryption channel' + primary key comment 'The Unique Index of the encryption channel UUID', + calling_peer varchar(320) not null comment 'The address of the calling peer', + calling_signature_uuid varchar(64) not null comment 'The UUID of the signing key that the calling peer is going to use to sign their messages', + calling_signature_public_key varchar(32) not null, + calling_encryption_public_key varchar(32) not null comment 'The public encryption key of the caller', + receiving_peer varchar(320) not null comment 'The address of the receiving peer', + receiving_signature_uuid varchar(256) null comment 'The UUID of the signature that the receiver peer will use to sign messages with', + receiving_signature_public_key varchar(32) null comment 'The public key of the receiver''s signing key', + receiving_encryption_public_key varchar(32) null comment 'The encryption key of the receiver', + transport_encryption_algorithm enum ('xchacha20', 'chacha20', 'aes256gcm') default 'xchacha20' not null comment 'The transport encryption algorithm used as selected by the caller', + transport_encryption_key varchar(256) null comment 'The transport encryption key encrypted using the caller''s public encryption key', + state enum ('AWAITING_RECEIVER', 'ERROR', 'DECLINED', 'AWAITING_DHE', 'OPENED', 'CLOSED') default 'AWAITING_RECEIVER' not null comment 'The current state of the encryption channel', + created timestamp default current_timestamp() not null comment 'The Timestamp for when this record was created', + constraint encryption_channels_uuid_uindex + unique (uuid) comment 'The Unique Index of the encryption channel UUID' +); + +create index encryption_channels_calling_peer_index + on encryption_channels (calling_peer) + comment 'The index of the calling peer address'; + +create index encryption_channels_created_index + on encryption_channels (created) + comment 'The Index for when the record was created'; + +create index encryption_channels_receiving_peer_index + on encryption_channels (receiving_peer) + comment 'The index of the receiving peer address'; + +create index encryption_channels_state_index + on encryption_channels (state) + comment 'The index for the state column'; + diff --git a/src/Socialbox/Enums/DatabaseObjects.php b/src/Socialbox/Enums/DatabaseObjects.php index 327698b..f2ad497 100644 --- a/src/Socialbox/Enums/DatabaseObjects.php +++ b/src/Socialbox/Enums/DatabaseObjects.php @@ -18,6 +18,9 @@ case SIGNING_KEYS = 'signing_keys.sql'; case EXTERNAL_SESSIONS = 'external_sessions.sql'; + case ENCRYPTED_CHANNELS = 'encrypted_channels.sql'; + case CHANNEL_COM = 'channel_com.sql'; + case CONTACT_KNOWN_KEYS = 'contact_known_keys.sql'; /** @@ -43,6 +46,8 @@ self::SIGNING_KEYS, self::EXTERNAL_SESSIONS => 2, + self::ENCRYPTED_CHANNELS, + self::CHANNEL_COM, self::CONTACT_KNOWN_KEYS => 3, }; } diff --git a/src/Socialbox/Enums/StandardError.php b/src/Socialbox/Enums/StandardError.php index 2184f55..0cf213d 100644 --- a/src/Socialbox/Enums/StandardError.php +++ b/src/Socialbox/Enums/StandardError.php @@ -18,6 +18,7 @@ case CONFLICT = -107; case EXPIRED = -108; case CRYPTOGRAPHIC_ERROR = -109; + case UUID_CONFLICT = -110; // RPC Errors case RPC_METHOD_NOT_FOUND = -1000; diff --git a/src/Socialbox/Enums/Status/EncryptionChannelState.php b/src/Socialbox/Enums/Status/EncryptionChannelState.php new file mode 100644 index 0000000..fd46611 --- /dev/null +++ b/src/Socialbox/Enums/Status/EncryptionChannelState.php @@ -0,0 +1,12 @@ +toRfc4122(); + } + + try + { + $stmt = Database::getConnection()->prepare('INSERT INTO encryption_channels (uuid, calling_peer, calling_signature_uuid, calling_signature_public_key, calling_encryption_public_key, receiving_peer, transport_encryption_algorithm) VALUES (:uuid, :calling_peer, :calling_signature_uuid, :calling_signature_public_key, :calling_encryption_public_key, :receiving_peer, :transport_encryption_algorithm)'); + $stmt->bindParam(':uuid', $uuid); + $callingPeerAddress = $callingPeer->getAddress(); + $stmt->bindParam(':calling_peer', $callingPeerAddress); + $stmt->bindParam(':calling_signature_uuid', $signatureUuid); + $stmt->bindParam(':calling_signature_public_key', $signingPublicKey); + $stmt->bindParam(':calling_encryption_public_key', $encryptionPublicKey); + $receivingPeerAddress = $receivingPeer->getAddress(); + $stmt->bindParam(':receiving_peer', $receivingPeerAddress); + $stmt->bindParam(':transport_encryption_algorithm', $transportEncryptionAlgorithm); + $stmt->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to create the encryption channel', $e); + } + + return $uuid; + } + + /** + * Retrieves the incoming encryption channels for the specified peer. + * + * @param string|PeerAddress $peerAddress The peer to retrieve the channels for. + * @param int $limit The maximum number of channels to retrieve. + * @param int $page The page of channels to retrieve. + * @return EncryptionChannelRecord[] The incoming channels for the peer. + * @throws DatabaseOperationException If an error occurs while retrieving the channels. + * @throws \DateMalformedStringException If the created date is not a valid date string. + */ + public static function getChannels(string|PeerAddress $peerAddress, int $limit=100, int $page=0): array + { + if($peerAddress instanceof PeerAddress) + { + $peerAddress = $peerAddress->getAddress(); + } + + try + { + $stmt = Database::getConnection()->prepare('SELECT * FROM encryption_channels WHERE calling_peer=:address OR receiving_peer=:address LIMIT :limit OFFSET :offset'); + $stmt->bindParam(':address', $peerAddress); + $stmt->bindParam(':limit', $limit, PDO::PARAM_INT); + $offset = $page * $limit; + $stmt->bindParam(':offset', $offset, PDO::PARAM_INT); + $stmt->execute(); + $results = $stmt->fetchAll(); + + $channels = []; + foreach($results as $result) + { + $channels[] = new EncryptionChannelRecord($result); + } + + return $channels; + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to retrieve the encryption channels', $e); + } + } + + /** + * Retrieves the incoming encryption channels for the specified peer. + * + * @param string|PeerAddress $peerAddress The peer to retrieve the channels for. + * @param int $limit The maximum number of channels to retrieve. + * @param int $page The page of channels to retrieve. + * @return EncryptionChannelRecord[] The incoming channels for the peer. + * @throws DatabaseOperationException If an error occurs while retrieving the channels. + * @throws \DateMalformedStringException If the created date is not a valid date string. + */ + public static function getRequests(string|PeerAddress $peerAddress, int $limit=100, int $page=0): array + { + if($peerAddress instanceof PeerAddress) + { + $peerAddress = $peerAddress->getAddress(); + } + + try + { + $stmt = Database::getConnection()->prepare('SELECT * FROM encryption_channels WHERE receiving_peer=:address AND state=:state LIMIT :limit OFFSET :offset'); + $stmt->bindParam(':address', $peerAddress); + $state = EncryptionChannelState::AWAITING_RECEIVER->value; + $stmt->bindParam(':state', $state); + $stmt->bindParam(':limit', $limit, PDO::PARAM_INT); + $offset = $page * $limit; + $stmt->bindParam(':offset', $offset, PDO::PARAM_INT); + $stmt->execute(); + $results = $stmt->fetchAll(); + + $channels = []; + foreach($results as $result) + { + $channels[] = new EncryptionChannelRecord($result); + } + + return $channels; + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to retrieve the encryption channels', $e); + } + } + + /** + * Retrieves the incoming encryption channels for the specified peer. + * + * @param string|PeerAddress $peerAddress The peer to retrieve the channels for. + * @param int $limit The maximum number of channels to retrieve. + * @param int $page The page of channels to retrieve. + * @return EncryptionChannelRecord[] The incoming channels for the peer. + * @throws DatabaseOperationException If an error occurs while retrieving the channels. + * @throws \DateMalformedStringException If the created date is not a valid date string. + */ + public static function getIncomingChannels(string|PeerAddress $peerAddress, int $limit=100, int $page=0): array + { + if($peerAddress instanceof PeerAddress) + { + $peerUuid = $peerAddress->getAddress(); + } + + try + { + $stmt = Database::getConnection()->prepare('SELECT * FROM encryption_channels WHERE receiving_peer=:address LIMIT :limit OFFSET :offset'); + $stmt->bindParam(':address', $peerUuid); + $stmt->bindParam(':limit', $limit, PDO::PARAM_INT); + $offset = $page * $limit; + $stmt->bindParam(':offset', $offset, PDO::PARAM_INT); + $stmt->execute(); + $results = $stmt->fetchAll(); + + $channels = []; + foreach($results as $result) + { + $channels[] = new EncryptionChannelRecord($result); + } + + return $channels; + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to retrieve the encryption channels', $e); + } + } + + /** + * Retrieves the outgoing channels for the specified peer. + * + * @param string|PeerAddress $peerAddress The peer to retrieve the channels for. + * @param int $limit The maximum number of channels to retrieve. + * @param int $page The page of channels to retrieve. + * @return EncryptionChannelRecord[] The outgoing channels for the specified peer. + * @throws DatabaseOperationException If an error occurs while retrieving the channels. + * @throws \DateMalformedStringException If the created date is not a valid date string. + */ + public static function getOutgoingChannels(string|PeerAddress $peerAddress, int $limit=100, int $page=0): array + { + if($peerAddress instanceof PeerAddress) + { + $peerAddress = $peerAddress->getAddress(); + } + + try + { + $stmt = Database::getConnection()->prepare('SELECT * FROM encryption_channels WHERE calling_peer=:address LIMIT :limit OFFSET :offset'); + $stmt->bindParam(':address', $peerAddress); + $stmt->bindParam(':limit', $limit, PDO::PARAM_INT); + $offset = $page * $limit; + $stmt->bindParam(':offset', $offset, PDO::PARAM_INT); + $stmt->execute(); + $results = $stmt->fetchAll(); + + $channels = []; + foreach($results as $result) + { + $channels[] = new EncryptionChannelRecord($result); + } + + return $channels; + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to retrieve the encryption channels', $e); + } + } + + /** + * Declines the encryption channel with the specified UUID. + * + * @param string $channelUuid The UUID of the channel to decline. + * @throws DatabaseOperationException If an error occurs while declining the channel. + */ + public static function declineChannel(string $channelUuid): void + { + try + { + $stmt = Database::getConnection()->prepare('UPDATE encryption_channels SET state=:state WHERE uuid=:uuid'); + $state = EncryptionChannelState::DECLINED->value; + $stmt->bindParam(':state', $state); + $stmt->bindParam(':uuid', $channelUuid); + $stmt->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to decline the encryption channel', $e); + } + } + + /** + * Accepts the encryption channel with the specified UUID. + * + * @param string $channelUuid The UUID of the channel to accept. + * @param string $signatureUuid The UUID of the signature used to create the channel. + * @param string $signaturePublicKey The public key used for signing. + * @param string $encryptionPublicKey The public key used for encryption. + * @param string $transportEncryptionAlgorithm The algorithm used for transport encryption. + * @param string $encryptedTransportEncryptionKey The encrypted transport encryption key. + * @throws DatabaseOperationException If an error occurs while accepting the channel. + */ + public static function acceptChannel(string $channelUuid, string $signatureUuid, string $signaturePublicKey, string $encryptionPublicKey, string $transportEncryptionAlgorithm, string $encryptedTransportEncryptionKey): void + { + try + { + $stmt = Database::getConnection()->prepare('UPDATE encryption_channels SET state=:state, receiving_signature_uuid=:receiving_signature_uuid, receiving_signature_public_key=:receiving_signature_public_key, receiving_encryption_public_key=:receiving_encryption_public_key, transport_encryption_algorithm=:transport_encryption_algorithm, transport_encryption_key=:transport_encryption_key WHERE uuid=:uuid'); + $state = EncryptionChannelState::OPENED->value; + $stmt->bindParam(':state', $state); + $stmt->bindParam(':receiving_signature_uuid', $signatureUuid); + $stmt->bindParam(':receiving_signature_public_key', $signaturePublicKey); + $stmt->bindParam(':receiving_encryption_public_key', $encryptionPublicKey); + $stmt->bindParam(':transport_encryption_algorithm', $transportEncryptionAlgorithm); + $stmt->bindParam(':transport_encryption_key', $encryptedTransportEncryptionKey); + $stmt->bindParam(':uuid', $channelUuid); + $stmt->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to accept the encryption channel', $e); + } + } + + /** + * Retrieves the encryption channel with the specified UUID. + * + * @param string $channelUuid The UUID of the channel to retrieve. + * @return EncryptionChannelRecord|null The record of the encryption channel. Null if the channel does not exist. + * @throws DatabaseOperationException If an error occurs while retrieving the channel. + * @throws \DateMalformedStringException If the created date is not a valid date string. + */ + public static function getChannel(string $channelUuid): ?EncryptionChannelRecord + { + try + { + $stmt = Database::getConnection()->prepare('SELECT * FROM encryption_channels WHERE uuid=:uuid'); + $stmt->bindParam(':uuid', $channelUuid); + $stmt->execute(); + $result = $stmt->fetch(); + + if($result === false) + { + return null; + } + + return new EncryptionChannelRecord($result); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to retrieve the encryption channel', $e); + } + } + + /** + * Deletes the encryption channel with the specified UUID. + * + * @param string $channelUuid The UUID of the channel to delete. + * @return void + *@throws DatabaseOperationException If an error occurs while deleting the channel. + */ + public static function deleteChannel(string $channelUuid): void + { + try + { + $stmt = Database::getConnection()->prepare('DELETE FROM encryption_channels WHERE uuid=:uuid'); + $stmt->bindParam(':uuid', $channelUuid); + $stmt->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to delete the encryption channel', $e); + } + } + + /** + * Updates the state of the encryption channel with the specified UUID. + * + * @param string $channelUuid The UUID of the channel to update. + * @return EncryptionChannelState The current state of the channel. + * @throws DatabaseOperationException If an error occurs while updating the channel state. + */ + public static function getChannelState(string $channelUuid): EncryptionChannelState + { + try + { + $stmt = Database::getConnection()->prepare('SELECT state FROM encryption_channels WHERE uuid=:uuid'); + $stmt->bindParam(':uuid', $channelUuid); + $stmt->execute(); + + return EncryptionChannelState::from($stmt->fetchColumn()); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to retrieve the encryption channel state', $e); + } + } + + /** + * Updates the state of the encryption channel with the specified UUID. + * + * @param string $channelUuid The UUID of the channel to update. + * @param EncryptionChannelState $state The new state of the channel. + * @return void The current state of the channel. + * @throws DatabaseOperationException If an error occurs while updating the channel state. + */ + public static function updateChannelState(string $channelUuid, EncryptionChannelState $state): void + { + try + { + $stmt = Database::getConnection()->prepare('UPDATE encryption_channels SET state=:state WHERE uuid=:uuid'); + $state = $state->value; + $stmt->bindParam(':state', $state); + $stmt->bindParam(':uuid', $channelUuid); + $stmt->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to update the encryption channel state', $e); + } + } + + /** + * Checks if a channel with the provided UUID exists. + * + * @param string $uuid The UUID of the channel to check. + * @return bool True if the channel exists, False otherwise. + * @throws DatabaseOperationException If an error occurs while checking the channel. + */ + public static function channelExists(string $uuid): bool + { + try + { + $stmt = Database::getConnection()->prepare('SELECT COUNT(*) FROM encryption_channels WHERE uuid=:uuid'); + $stmt->bindParam(':uuid', $uuid); + $stmt->execute(); + + return $stmt->fetchColumn() > 0; + } + catch(PDOException $e) + { + throw new DatabaseOperationException('There was an error while trying to check if the channel UUID exists', $e); + } + } + + /** + * Sends data to the specified channel. + * + * @param string $channelUuid The UUID of the channel to send the data to. + * @param string $message The message to send. + * @param string $signature The signature of the message. + * @param CommunicationRecipientType $recipient The recipient type. + * @return string The UUID of the sent message. + * @throws DatabaseOperationException If an error occurs while sending the message. + */ + public static function sendData(string $channelUuid, string $message, string $signature, CommunicationRecipientType $recipient): string + { + $uuid = UuidV4::v4()->toRfc4122(); + + try + { + $stmt = Database::getConnection()->prepare('INSERT INTO channel_com (uuid, channel_uuid, recipient, message, signature) VALUES (:uuid, :channel_uuid, :recipient, :message, :signature)'); + $stmt->bindParam(':uuid', $uuid); + $stmt->bindParam(':channel_uuid', $channelUuid); + $recipient = $recipient->value; + $stmt->bindParam(':recipient', $recipient); + $stmt->bindParam(':message', $message); + $stmt->bindParam(':signature', $signature); + + $stmt->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to send the message', $e); + } + + return $uuid; + } + + /** + * Retrieves the messages for the specified channel and recipient. + * + * @param string $channelUuid The UUID of the channel to retrieve the messages for. + * @param CommunicationRecipientType $recipient The recipient type to retrieve the messages for. + * @return ChannelMessageRecord[] The messages for the specified channel and recipient. + * @throws DatabaseOperationException If an error occurs while retrieving the messages. + */ + public static function receiveData(string $channelUuid, CommunicationRecipientType $recipient): array + { + try + { + $stmt = Database::getConnection()->prepare('SELECT * FROM channel_com WHERE channel_uuid=:channel_uuid AND recipient=:recipient AND received=0 ORDER BY timestamp'); + $stmt->bindParam(':channel_uuid', $channelUuid); + $recipient = $recipient->value; + $stmt->bindParam(':recipient', $recipient); + $stmt->execute(); + $results = $stmt->fetchAll(); + + $messages = []; + foreach($results as $result) + { + $messages[] = new ChannelMessageRecord($result); + } + + return $messages; + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to retrieve the messages', $e); + } + } + + /** + * Retrieves the message with the specified UUID. + * + * @param string $channelUuid The UUID of the channel to retrieve the message for. + * @param string $messageUuid The UUID of the message to retrieve. + * @return ChannelMessageRecord|null The message with the specified UUID. Null if the message does not exist. + * @throws DatabaseOperationException If an error occurs while retrieving the message. + */ + public static function getData(string $channelUuid, string $messageUuid): ?ChannelMessageRecord + { + try + { + $stmt = Database::getConnection()->prepare('SELECT * FROM channel_com WHERE channel_uuid=:channel_uuid AND uuid=:uuid'); + $stmt->bindParam(':channel_uuid', $channelUuid); + $stmt->bindParam(':uuid', $messageUuid); + $stmt->execute(); + $result = $stmt->fetch(); + + if($result === false) + { + return null; + } + + return new ChannelMessageRecord($result); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to retrieve the message', $e); + } + } + + /** + * Marks the message with the specified UUID as received. + * + * @param string $uuid The UUID of the message to mark as received. + * @throws DatabaseOperationException If an error occurs while marking the message as received. + */ + public static function markDataAsReceived(string $uuid): void + { + try + { + $stmt = Database::getConnection()->prepare('UPDATE channel_com SET received=1 WHERE uuid=:uuid'); + $stmt->bindParam(':uuid', $uuid); + $stmt->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to mark the message as received', $e); + } + } + + /** + * Deletes the message with the specified UUID. + * + * @param string $uuid The UUID of the message to delete. + * @throws DatabaseOperationException If an error occurs while deleting the message. + */ + public static function deleteData(string $uuid): void + { + try + { + $stmt = Database::getConnection()->prepare('DELETE FROM channel_com WHERE uuid=:uuid'); + $stmt->bindParam(':uuid', $uuid); + $stmt->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to delete the message', $e); + } + } + } \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/ChannelMessageRecord.php b/src/Socialbox/Objects/Database/ChannelMessageRecord.php new file mode 100644 index 0000000..aed0925 --- /dev/null +++ b/src/Socialbox/Objects/Database/ChannelMessageRecord.php @@ -0,0 +1,141 @@ +uuid = $data['uuid']; + $this->channelUuid = $data['channel_uuid']; + $this->recipient = CommunicationRecipientType::from($data['recipient']); + $this->message = $data['message']; + $this->signature = $data['signature']; + $this->received = (bool)$data['received']; + + if($data['timestamp'] instanceof \DateTime) + { + $this->timestamp = $data['timestamp']; + } + elseif(is_int($data['timestamp'])) + { + $this->timestamp = (new \DateTime())->setTimestamp($data['timestamp']); + } + elseif(is_string($data['timestamp'])) + { + $this->timestamp = new \DateTime($data['timestamp']); + } + else + { + throw new \InvalidArgumentException('Invalid timestamp type, got ' . gettype($data['timestamp'])); + } + } + + /** + * Returns the unique identifier for the message. + * + * @return string + */ + public function getUuid(): string + { + return $this->uuid; + } + + /** + * Returns the UUID of the channel that the message belongs to. + * + * @return string + */ + public function getChannelUuid(): string + { + return $this->channelUuid; + } + + /** + * Returns the recipient type of the message. + * + * @return CommunicationRecipientType + */ + public function getRecipient(): CommunicationRecipientType + { + return $this->recipient; + } + + /** + * Returns the message content. + * + * @return string + */ + public function getMessage(): string + { + return $this->message; + } + + /** + * Returns the signature of the message. + * + * @return string + */ + public function getSignature(): string + { + return $this->signature; + } + + public function isReceived(): bool + { + return $this->received; + } + + public function getTimestamp(): \DateTime + { + return $this->timestamp; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): ChannelMessageRecord + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'uuid' => $this->uuid, + 'channel_uuid' => $this->channelUuid, + 'recipient' => $this->recipient->value, + 'message' => $this->message, + 'signature' => $this->signature, + 'received' => $this->received, + 'timestamp' => $this->timestamp->format('Y-m-d H:i:s') + ]; + } + } \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/EncryptionChannelRecord.php b/src/Socialbox/Objects/Database/EncryptionChannelRecord.php new file mode 100644 index 0000000..c8979f0 --- /dev/null +++ b/src/Socialbox/Objects/Database/EncryptionChannelRecord.php @@ -0,0 +1,259 @@ +uuid = $data['uuid']; + + if(!isset($data['calling_peer'])) + { + throw new InvalidArgumentException('Missing property calling_peer'); + } + else + { + if(is_string($data['calling_peer'])) + { + $this->callingPeer = PeerAddress::fromAddress($data['calling_peer']); + } + elseif($data['calling_peer'] instanceof PeerAddress) + { + $this->callingPeer = $data['calling_peer']; + } + else + { + throw new InvalidArgumentException('Unexpected calling_peer type, got ' . gettype($data['calling_peer'])); + } + } + + $this->callingSignatureUuid = $data['calling_signature_uuid']; + $this->callingEncryptionPublicKey = $data['calling_encryption_public_key']; + + if(!isset($data['receiving_peer'])) + { + throw new InvalidArgumentException('Missing property receiving_peer'); + } + else + { + if(is_string($data['receiving_peer'])) + { + $this->receivingPeer = PeerAddress::fromAddress($data['receiving_peer']); + } + elseif($data['receiving_peer'] instanceof PeerAddress) + { + $this->receivingPeer = $data['receiving_peer']; + } + else + { + throw new InvalidArgumentException('Unexpected receiving_peer type, got ' . gettype($data['receiving_peer'])); + } + } + + $this->receivingSignatureUuid = $data['receiving_signature_uuid'] ?? null; + $this->receivingSignaturePublicKey = $data['receiving_signature_public_key'] ?? null; + $this->receivingEncryptionPublicKey = $data['receiving_encryption_public_key'] ?? null; + $this->transportEncryptionAlgorithm = $data['transport_encryption_algorithm']; + $this->transportEncryptionKey = $data['transport_encryption_key'] ?? null; + $this->state = EncryptionChannelState::tryFrom($data['state']) ?? EncryptionChannelState::ERROR; + + if(!isset($data['created'])) + { + throw new InvalidArgumentException('Missing property created'); + } + else + { + if(is_string($data['created'])) + { + $this->created = new DateTime($data['created']); + } + elseif(is_int($data['created'])) + { + $this->created = (new DateTime())->setTimestamp($data['created']); + } + elseif($data['created'] instanceof DateTime) + { + $this->created = $data['created']; + } + else + { + throw new InvalidArgumentException('Unexpected created type, got ' . gettype($data['created'])); + } + } + } + + /** + * Returns the Unique Universal Identifier for the encryption record + * + * @return string + */ + public function getUuid(): string + { + return $this->uuid; + } + + /** + * Returns the address of the calling peer + * + * @return PeerAddress + */ + public function getCallingPeer(): PeerAddress + { + return $this->callingPeer; + } + + /** + * Returns the UUID of the signing keypair that the caller is using + * + * @return string + */ + public function getCallingSignatureUuid(): string + { + return $this->callingSignatureUuid; + } + + /** + * Returns the public key of the encryption keypair that the caller is using + * + * @return string + */ + public function getCallingEncryptionPublicKey(): string + { + return $this->callingEncryptionPublicKey; + } + + /** + * Returns the address of the receiving peer + * + * @return PeerAddress + */ + public function getReceivingPeer(): PeerAddress + { + return $this->receivingPeer; + } + + /** + * Returns the UUID of the signing keypair that the receiver is using + * + * @return string|null + */ + public function getReceivingSignatureUuid(): ?string + { + return $this->receivingSignatureUuid; + } + + /** + * Returns the public key of the signing keypair that the receiver is using + * + * @return string|null + */ + public function getReceivingSignaturePublicKey(): ?string + { + return $this->receivingSignaturePublicKey; + } + + /** + * Returns the public key of the encryption keypair that the receiver is using + * + * @return string|null + */ + public function getReceivingEncryptionPublicKey(): ?string + { + return $this->receivingEncryptionPublicKey; + } + + /** + * Returns the algorithm used for transport encryption + * + * @return string + */ + public function getTransportEncryptionAlgorithm(): string + { + return $this->transportEncryptionAlgorithm; + } + + /** + * Returns the key used for transport encryption + * + * @return string|null + */ + public function getTransportEncryptionKey(): ?string + { + return $this->transportEncryptionKey; + } + + /** + * Returns the current state of the encryption channel + * + * @return EncryptionChannelState + */ + public function getState(): EncryptionChannelState + { + return $this->state; + } + + /** + * Returns the creation date of the encryption channel + * + * @return DateTime + */ + public function getCreated(): DateTime + { + return $this->created; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): EncryptionChannelRecord + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'uuid' => $this->uuid, + 'calling_peer' => $this->callingPeer->getAddress(), + 'calling_signature_uuid' => $this->callingSignatureUuid, + 'calling_encryption_public_key' => $this->callingEncryptionPublicKey, + 'receiving_peer' => $this->receivingPeer->getAddress(), + 'receiving_signature_uuid' => $this->receivingSignatureUuid, + 'receiving_signature_public_key' => $this->receivingSignaturePublicKey, + 'receiving_encryption_public_key' => $this->receivingEncryptionPublicKey, + 'transport_encryption_algorithm' => $this->transportEncryptionAlgorithm, + 'transport_encryption_key' => $this->transportEncryptionKey, + 'state' => $this->state->value, + 'created' => $this->created->format('Y-m-d H:i:s') + ]; + } + } \ No newline at end of file From d929a9ec4cc188ac9c790d5f4c70fc709d6eda1e Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 6 Feb 2025 15:38:52 -0500 Subject: [PATCH 343/420] Added Standard Objects for the encryption channel --- .../Objects/Database/ChannelMessageRecord.php | 34 ++- .../Database/EncryptionChannelRecord.php | 11 + .../Objects/Standard/EncryptionChannel.php | 222 ++++++++++++++++++ .../Standard/EncryptionChannelMessage.php | 145 ++++++++++++ 4 files changed, 405 insertions(+), 7 deletions(-) create mode 100644 src/Socialbox/Objects/Standard/EncryptionChannel.php create mode 100644 src/Socialbox/Objects/Standard/EncryptionChannelMessage.php diff --git a/src/Socialbox/Objects/Database/ChannelMessageRecord.php b/src/Socialbox/Objects/Database/ChannelMessageRecord.php index aed0925..99c7c5a 100644 --- a/src/Socialbox/Objects/Database/ChannelMessageRecord.php +++ b/src/Socialbox/Objects/Database/ChannelMessageRecord.php @@ -2,8 +2,12 @@ namespace Socialbox\Objects\Database; + use DateMalformedStringException; + use DateTime; + use InvalidArgumentException; use Socialbox\Enums\Types\CommunicationRecipientType; use Socialbox\Interfaces\SerializableInterface; + use Socialbox\Objects\Standard\EncryptionChannelMessage; class ChannelMessageRecord implements SerializableInterface { @@ -13,7 +17,7 @@ private string $message; private string $signature; private bool $received; - private \DateTime $timestamp; + private DateTime $timestamp; /** * Constructs a new instance of this class and initializes its properties with the provided data. @@ -26,7 +30,7 @@ * - 'signature' (string): The signature. * - 'received' (bool): Whether the message has been received. * - 'timestamp' (int|string|\DateTime): The timestamp of the message. - * @return void + * @throws DateMalformedStringException If the timestamp is a string that cannot be parsed. */ public function __construct(array $data) { @@ -37,21 +41,21 @@ $this->signature = $data['signature']; $this->received = (bool)$data['received']; - if($data['timestamp'] instanceof \DateTime) + if($data['timestamp'] instanceof DateTime) { $this->timestamp = $data['timestamp']; } elseif(is_int($data['timestamp'])) { - $this->timestamp = (new \DateTime())->setTimestamp($data['timestamp']); + $this->timestamp = (new DateTime())->setTimestamp($data['timestamp']); } elseif(is_string($data['timestamp'])) { - $this->timestamp = new \DateTime($data['timestamp']); + $this->timestamp = new DateTime($data['timestamp']); } else { - throw new \InvalidArgumentException('Invalid timestamp type, got ' . gettype($data['timestamp'])); + throw new InvalidArgumentException('Invalid timestamp type, got ' . gettype($data['timestamp'])); } } @@ -105,12 +109,22 @@ return $this->signature; } + /** + * Returns whether the message has been received. + * + * @return bool + */ public function isReceived(): bool { return $this->received; } - public function getTimestamp(): \DateTime + /** + * Returns the timestamp of the message. + * + * @return DateTime + */ + public function getTimestamp(): DateTime { return $this->timestamp; } @@ -138,4 +152,10 @@ 'timestamp' => $this->timestamp->format('Y-m-d H:i:s') ]; } + + + public function toStandard(): EncryptionChannelMessage + { + return new EncryptionChannelMessage($this->toArray()); + } } \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/EncryptionChannelRecord.php b/src/Socialbox/Objects/Database/EncryptionChannelRecord.php index c8979f0..92a4246 100644 --- a/src/Socialbox/Objects/Database/EncryptionChannelRecord.php +++ b/src/Socialbox/Objects/Database/EncryptionChannelRecord.php @@ -7,6 +7,7 @@ use Socialbox\Enums\Status\EncryptionChannelState; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\PeerAddress; + use Socialbox\Objects\Standard\EncryptionChannel; class EncryptionChannelRecord implements SerializableInterface { @@ -256,4 +257,14 @@ 'created' => $this->created->format('Y-m-d H:i:s') ]; } + + /** + * Converts the Encryption Channel Record to a Standard Encryption Channel + * + * @return EncryptionChannel + */ + public function toStandard(): EncryptionChannel + { + return new EncryptionChannel($this->toArray()); + } } \ No newline at end of file diff --git a/src/Socialbox/Objects/Standard/EncryptionChannel.php b/src/Socialbox/Objects/Standard/EncryptionChannel.php new file mode 100644 index 0000000..f0df342 --- /dev/null +++ b/src/Socialbox/Objects/Standard/EncryptionChannel.php @@ -0,0 +1,222 @@ +uuid = $data['uuid']; + $this->callingPeer = $data['calling_peer']; + $this->callingSignatureUuid = $data['calling_signature_uuid']; + $this->callingSignaturePublicKey = $data['calling_signature_public_key']; + $this->callingEncryptionPublicKey = $data['calling_encryption_public_key']; + $this->receivingPeer = $data['receiving_peer']; + $this->receivingSignatureUuid = $data['receiving_signature_uuid']; + $this->receivingSignaturePublicKey = $data['receiving_signature_public_key']; + $this->receivingEncryptionPublicKey = $data['receiving_encryption_public_key']; + $this->transportEncryptionAlgorithm = $data['transport_encryption_algorithm']; + $this->transportEncryptionKey = $data['transport_encryption_key']; + $this->state = EncryptionChannelState::from($data['state']); + + if($data['created'] instanceof DateTime) + { + $this->created = $data['created']->getTimestamp(); + } + elseif(is_int($data['created'])) + { + $this->created = $data['created']; + } + elseif(is_string($data['created'])) + { + $this->created = strtotime($data['created']) ?: throw new InvalidArgumentException('Invalid date format'); + } + else + { + throw new InvalidArgumentException('Invalid date format, got type: ' . gettype($data['created'])); + } + } + + /** + * Returns the Unique Universal Identifier of the Encryption Channel + * + * @return string The UUID of the Encryption Channel + */ + public function getUuid(): string + { + return $this->uuid; + } + + /** + * Returns the Peer address that initiated the Encryption Channel + * + * @return string The Peer address that initiated the Encryption Channel + */ + public function getCallingPeer(): string + { + return $this->callingPeer; + } + + /** + * Returns the Unique Universal Identifier of the Signature used by the calling Peer + * + * @return string The UUID of the Signature used by the calling Peer + */ + public function getCallingSignatureUuid(): string + { + return $this->callingSignatureUuid; + } + + /** + * Returns the Public Key of the Signature used by the calling Peer + * + * @return string The Public Key of the Signature used by the calling Peer + */ + public function getCallingSignaturePublicKey(): string + { + return $this->callingSignaturePublicKey; + } + + /** + * Returns the Public Key of the Encryption used by the calling Peer + * + * @return string The Public Key of the Encryption used by the calling Peer + */ + public function getCallingEncryptionPublicKey(): string + { + return $this->callingEncryptionPublicKey; + } + + /** + * Returns the Peer address that received the Encryption Channel + * + * @return string The Peer address that received the Encryption Channel + */ + public function getReceivingPeer(): string + { + return $this->receivingPeer; + } + + /** + * Returns the Unique Universal Identifier of the Signature used by the receiving Peer + * + * @return string|null The UUID of the Signature used by the receiving Peer, or null if not set + */ + public function getReceivingSignatureUuid(): ?string + { + return $this->receivingSignatureUuid; + } + + /** + * Returns the Public Key of the Signature used by the receiving Peer + * + * @return string|null The Public Key of the Signature used by the receiving Peer, or null if not set + */ + public function getReceivingSignaturePublicKey(): ?string + { + return $this->receivingSignaturePublicKey; + } + + /** + * Returns the Public Key of the Encryption used by the receiving Peer + * + * @return string|null The Public Key of the Encryption used by the receiving Peer, or null if not set + */ + public function getReceivingEncryptionPublicKey(): ?string + { + return $this->receivingEncryptionPublicKey; + } + + /** + * Returns the Algorithm used for the Transport Encryption + * + * @return string The Algorithm used for the Transport Encryption + */ + public function getTransportEncryptionAlgorithm(): string + { + return $this->transportEncryptionAlgorithm; + } + + /** + * Returns the Key used for the Transport Encryption + * + * @return string|null The Key used for the Transport Encryption, or null if not set + */ + public function getTransportEncryptionKey(): ?string + { + return $this->transportEncryptionKey; + } + + /** + * Returns the State of the Encryption Channel + * + * @return EncryptionChannelState The State of the Encryption Channel + */ + public function getState(): EncryptionChannelState + { + return $this->state; + } + + /** + * Returns the Unix Timestamp of the creation date of the Encryption Channel + * + * @return int The Unix Timestamp of the creation date of the Encryption Channel + */ + public function getCreated(): int + { + return $this->created; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): EncryptionChannel + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'uuid' => $this->uuid, + 'calling_peer' => $this->callingPeer, + 'calling_signature_uuid' => $this->callingSignatureUuid, + 'calling_encryption_public_key' => $this->callingEncryptionPublicKey, + 'receiving_peer' => $this->receivingPeer, + 'receiving_signature_uuid' => $this->receivingSignatureUuid, + 'receiving_signature_public_key' => $this->receivingSignaturePublicKey, + 'receiving_encryption_public_key' => $this->receivingEncryptionPublicKey, + 'transport_encryption_algorithm' => $this->transportEncryptionAlgorithm, + 'transport_encryption_key' => $this->transportEncryptionKey, + 'state' => $this->state->value, + 'created' => $this->created + ]; + } + } \ No newline at end of file diff --git a/src/Socialbox/Objects/Standard/EncryptionChannelMessage.php b/src/Socialbox/Objects/Standard/EncryptionChannelMessage.php new file mode 100644 index 0000000..ccd8573 --- /dev/null +++ b/src/Socialbox/Objects/Standard/EncryptionChannelMessage.php @@ -0,0 +1,145 @@ +uuid = $data['uuid']; + $this->channelUuid = $data['channel_uuid']; + $this->recipient = CommunicationRecipientType::from($data['recipient']); + $this->message = $data['message']; + $this->signature = $data['signature']; + $this->received = (bool)$data['received']; + + if($data['timestamp'] instanceof DateTime) + { + $this->timestamp = $data['timestamp']->getTimestamp(); + } + elseif(is_int($data['timestamp'])) + { + $this->timestamp = $data['timestamp']; + } + elseif(is_string($data['timestamp'])) + { + $this->timestamp = strtotime($data['timestamp']) ?: throw new InvalidArgumentException('Invalid date format'); + } + else + { + throw new InvalidArgumentException('Invalid date format, got type: ' . gettype($data['timestamp'])); + } + } + + /** + * The Unique Universal Identifier of the message. + * + * @return string The UUID of the message. + */ + public function getUuid(): string + { + return $this->uuid; + } + + /** + * The Unique Universal Identifier of the channel. + * + * @return string The UUID of the channel. + */ + public function getChannelUuid(): string + { + return $this->channelUuid; + } + + /** + * The recipient of the message. + * + * @return CommunicationRecipientType The recipient of the message. + */ + public function getRecipient(): CommunicationRecipientType + { + return $this->recipient; + } + + /** + * The encrypted message. + * + * @return string The message. + */ + public function getMessage(): string + { + return $this->message; + } + + /** + * The signature of the decrypted message. + * + * @return string The signature of the message. + */ + public function getSignature(): string + { + return $this->signature; + } + + /** + * Whether the message has been received. + * + * @return bool Whether the message has been received. + */ + public function isReceived(): bool + { + return $this->received; + } + + /** + * The timestamp of the message. + * + * @return int The timestamp of the message. + */ + public function getTimestamp(): int + { + return $this->timestamp; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): EncryptionChannelMessage + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'uuid' => $this->uuid, + 'channel_uuid' => $this->channelUuid, + 'recipient' => $this->recipient->value, + 'message' => $this->message, + 'signature' => $this->signature, + 'received' => $this->received, + 'timestamp' => $this->timestamp + ]; + } + } \ No newline at end of file From a1704338c8b266fb8018f22ec71a6ea7fae4c5d6 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 6 Feb 2025 15:46:11 -0500 Subject: [PATCH 344/420] Added method importData to import data from external servers --- .../Managers/EncryptionChannelManager.php | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/Socialbox/Managers/EncryptionChannelManager.php b/src/Socialbox/Managers/EncryptionChannelManager.php index 3e2bdf7..badcede 100644 --- a/src/Socialbox/Managers/EncryptionChannelManager.php +++ b/src/Socialbox/Managers/EncryptionChannelManager.php @@ -14,6 +14,7 @@ use Socialbox\Objects\Database\ChannelMessageRecord; use Socialbox\Objects\Database\EncryptionChannelRecord; use Socialbox\Objects\PeerAddress; + use Socialbox\Objects\Standard\EncryptionChannelMessage; class EncryptionChannelManager { @@ -502,6 +503,7 @@ * @param string $messageUuid The UUID of the message to retrieve. * @return ChannelMessageRecord|null The message with the specified UUID. Null if the message does not exist. * @throws DatabaseOperationException If an error occurs while retrieving the message. + * @throws \DateMalformedStringException If the created date is not a valid date string. */ public static function getData(string $channelUuid, string $messageUuid): ?ChannelMessageRecord { @@ -526,6 +528,34 @@ } } + /** + * Imports the specified message data into the database. + * + * @param EncryptionChannelMessage|ChannelMessageRecord $message The message data to import. + * @throws DatabaseOperationException If an error occurs while importing the message. + */ + public static function importData(EncryptionChannelMessage|ChannelMessageRecord $message): void + { + $data = $message->toArray(); + + try + { + $stmt = Database::getConnection()->prepare('INSERT INTO channel_com (uuid, channel_uuid, recipient, message, signature, received, timestamp) VALUES (:uuid, :channel_uuid, :recipient, :message, :signature, :received, :timestamp)'); + $stmt->bindParam(':uuid', $data['uuid']); + $stmt->bindParam(':channel_uuid', $data['channel_uuid']); + $stmt->bindParam(':recipient', $data['recipient']); + $stmt->bindParam(':message', $data['message']); + $stmt->bindParam(':signature', $data['signature']); + $stmt->bindParam(':received', $data['received']); + $stmt->bindParam(':timestamp', $data['timestamp']); + $stmt->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to import the message', $e); + } + } + /** * Marks the message with the specified UUID as received. * From 05f6661a753826e4770fba447f201997a2932bf0 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 7 Feb 2025 18:23:10 -0500 Subject: [PATCH 345/420] Renamed ContactRecord to Contact --- .../AddressBook/AddressBookAddContact.php | 2 +- .../Encryption/EncryptionAcceptChannel.php | 31 ++ .../Encryption/EncryptionCreateChannel.php | 317 ++++++++++++++++++ src/Socialbox/Classes/Validator.php | 11 + src/Socialbox/Managers/ContactManager.php | 10 +- .../Database/ContactDatabaseRecord.php | 8 +- .../{ContactRecord.php => Contact.php} | 4 +- 7 files changed, 371 insertions(+), 12 deletions(-) create mode 100644 src/Socialbox/Classes/StandardMethods/Encryption/EncryptionAcceptChannel.php create mode 100644 src/Socialbox/Classes/StandardMethods/Encryption/EncryptionCreateChannel.php rename src/Socialbox/Objects/Standard/{ContactRecord.php => Contact.php} (96%) diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php index 7af29fd..4e4f7e4 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookAddContact.php @@ -41,7 +41,7 @@ throw new InvalidRpcArgumentException('peer', $e); } - if($rpcRequest->containsParameter('relationship')) + if($rpcRequest->containsParameter('relationship') && $rpcRequest->getParameter('relationship') !== null) { $relationship = ContactRelationshipType::tryFrom(strtoupper($rpcRequest->getParameter('relationship'))); if($relationship === null) diff --git a/src/Socialbox/Classes/StandardMethods/Encryption/EncryptionAcceptChannel.php b/src/Socialbox/Classes/StandardMethods/Encryption/EncryptionAcceptChannel.php new file mode 100644 index 0000000..df946c4 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/Encryption/EncryptionAcceptChannel.php @@ -0,0 +1,31 @@ +containsParameter('calling_encryption_public_key')) + { + throw new MissingRpcArgumentException('calling_encryption_public_key'); + } + if(!Cryptography::validatePublicEncryptionKey($rpcRequest->getParameter('calling_encryption_public_key'))) + { + throw new InvalidRpcArgumentException('calling_encryption_public_key', 'Invalid calling encryption public key'); + } + + // Transport Algorithm Validation + if(!$rpcRequest->containsParameter('transport_encryption_algorithm')) + { + throw new MissingRpcArgumentException('transport_encryption_algorithm'); + } + if(!Cryptography::isSupportedAlgorithm($rpcRequest->getParameter('transport_encryption_algorithm'))) + { + throw new InvalidRpcArgumentException('transport_encryption_algorithm', 'Unsupported Transport Encryption Algorithm'); + } + + // Create/Import the encryption channel + try + { + $channelUuid = EncryptionChannelManager::createChannel( + callingPeer: $callingPeer, + receivingPeer: $receivingPeer, + signatureUuid: $callingPeerSignature->getUuid(), + signingPublicKey: $callingPeerSignature->getPublicKey(), + encryptionPublicKey: $rpcRequest->getParameter('calling_encryption_public_key'), + transportEncryptionAlgorithm: $rpcRequest->getParameter('transport_encryption_algorithm'), + uuid: $channelUuid + ); + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to create the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + // If the receiving peer resides on an external server, then we need to tell the external server + // about the encryption channel so that the receiving peer can see it. + if($receivingPeer->getDomain() !== Configuration::getInstanceConfiguration()->getDomain()) + { + $rpcClient = Socialbox::getExternalSession($receivingPeer->getDomain()); + + } + + return $rpcRequest->produceResponse($channelUuid); + } + + /** + * Returns the PeerAddress of the calling peer, if a server is making a request then the server must provide + * both the UUID of the encryption channel and the PeerAddress of the calling peer to prevent UUID conflicts + * + * Otherwise, the calling peer is assumed to be the authenticated user and no UUID is required + * + * @param ClientRequest $request The full client request + * @param RpcRequest $rpcRequest The focused RPC request + * @return PeerAddress The calling peer + * @throws StandardRpcException If the calling peer cannot be resolved + */ + private static function getCallingPeer(ClientRequest $request, RpcRequest $rpcRequest): PeerAddress + { + if($request->getIdentifyAs() !== null) + { + try + { + // Prevent UUID conflicts if the server is trying to use an UUID that already exists on this server + if (EncryptionChannelManager::channelExists($rpcRequest->getParameter('uuid'))) + { + throw new StandardRpcException('UUID Conflict, a channel with this UUID already exists', StandardError::UUID_CONFLICT); + } + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to resolve channel UUID', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if($request->getIdentifyAs()->getUsername() == ReservedUsernames::HOST) + { + throw new StandardRpcException('The identifier cannot be a host', StandardError::BAD_REQUEST); + } + + if($request->getIdentifyAs()->getDomain() !== Configuration::getInstanceConfiguration()->getDomain()) + { + Socialbox::resolvePeer($request->getIdentifyAs()); + } + + return $request->getIdentifyAs(); + } + + try + { + return PeerAddress::fromAddress($request->getPeer()->getAddress()); + } + catch(StandardRpcException $e) + { + throw $e; + } + catch(Exception $e) + { + throw new StandardRpcException('The calling peer cannot be resolved', StandardError::INTERNAL_SERVER_ERROR, $e); + } + } + + /** + * Resolves and returns the calling peer's signing key, if the calling peer is coming from an external server + * then the signature returned is the resolved signature from the external server, otherwise the signature + * is locally resolved and returned + * + * @param PeerAddress $callingPeer The calling peer + * @param RpcRequest $rpcRequest The focused RPC request + * @return SigningKey The resolved signing key + * @throws InvalidRpcArgumentException If one or more RPC parameters are invalid + * @throws MissingRpcArgumentException If one or more RPC parameters are missing + * @throws StandardRpcException If the calling signature cannot be resolved + */ + private static function getCallingSignature(PeerAddress $callingPeer, RpcRequest $rpcRequest): SigningKey + { + // Caller signature verification + if(!$rpcRequest->containsParameter('calling_signature_uuid')) + { + throw new MissingRpcArgumentException('calling_signature_uuid'); + } + if(!Validator::validateUuid($rpcRequest->getParameter('calling_signature_uuid'))) + { + throw new InvalidRpcArgumentException('calling_signature_uuid', 'Invalid UUID V4'); + } + if(!$rpcRequest->containsParameter('calling_signature_public_key')) + { + throw new MissingRpcArgumentException('calling_signature_public_key'); + } + if(!Cryptography::validatePublicSigningKey($rpcRequest->getParameter('calling_signature_public_key'))) + { + throw new InvalidRpcArgumentException('calling_signature_public_key', 'Invalid Public Key'); + } + + // Resolve the signature + $resolvedCallingSignature = Socialbox::resolvePeerSignature($callingPeer, $rpcRequest->getParameter('calling_signature_uuid')); + if($resolvedCallingSignature->getPublicKey() !== $rpcRequest->getParameter('calling_signature_public_key')) + { + throw new InvalidRpcArgumentException('calling_signature_public_key', 'Public signing key of the calling peer does not match the resolved signature'); + } + if($resolvedCallingSignature->getState() === SigningKeyState::EXPIRED) + { + throw new StandardRpcException('The public signing key of the calling peer has expired', StandardError::EXPIRED); + } + + $resolvedSignature = Socialbox::resolvePeerSignature($callingPeer, $rpcRequest->getParameter('calling_signature_uuid')); + if($resolvedSignature === null) + { + throw new StandardRpcException('The calling peer signature could not be resolved', StandardError::NOT_FOUND); + } + + return $resolvedSignature; + } + + /** + * Returns the PeerAddress of the receiving peer, if the receiving peer is from an external server then the + * receiving peer is resolved and returned, otherwise the receiving peer is locally resolved and returned + * + * @param RpcRequest $rpcRequest The focused RPC request + * @return PeerAddress The receiving peer + * @throws InvalidRpcArgumentException If one or more RPC parameters are invalid + * @throws MissingRpcArgumentException If one or more RPC parameters are missing + * @throws StandardRpcException If the receiving peer cannot be resolved + */ + private static function getReceivingPeer(RpcRequest $rpcRequest): PeerAddress + { + if(!$rpcRequest->containsParameter('receiving_peer')) + { + throw new MissingRpcArgumentException('receiving_peer'); + } + + try + { + $receivingPeer = PeerAddress::fromAddress($rpcRequest->getParameter('receiving_peer')); + } + catch(InvalidArgumentException $e) + { + throw new InvalidRpcArgumentException('receiving_peer', $e); + } + + if($receivingPeer->getUsername() == ReservedUsernames::HOST) + { + throw new InvalidRpcArgumentException('receiving_peer', 'Hosts cannot receive channels'); + } + + // Resolve the receiving peer if it's from an external server + if($receivingPeer->getDomain() !== Configuration::getInstanceConfiguration()->getDomain()) + { + Socialbox::resolvePeer($receivingPeer); + } + + return $receivingPeer; + } + + /** + * @param PeerAddress $receivingPeer + * @param RpcRequest $rpcRequest + * @return SigningKey + * @throws InvalidRpcArgumentException + * @throws MissingRpcArgumentException + * @throws StandardRpcException + */ + private static function getReceivingSignature(PeerAddress $receivingPeer, RpcRequest $rpcRequest): SigningKey + { + // Receiving signature verification + if(!$rpcRequest->containsParameter('receiving_signature_uuid')) + { + throw new MissingRpcArgumentException('receiving_signature_uuid'); + } + if(!Validator::validateUuid($rpcRequest->getParameter('receiving_signature_uuid'))) + { + throw new InvalidRpcArgumentException('receiving_signature_uuid', 'Invalid UUID V4'); + } + if(!$rpcRequest->containsParameter('receiving_signature_public_key')) + { + throw new MissingRpcArgumentException('receiving_signature_public_key'); + } + if(!Cryptography::validatePublicSigningKey($rpcRequest->getParameter('receiving_signature_public_key'))) + { + throw new InvalidRpcArgumentException('receiving_signature_public_key', 'Invalid Public Key'); + } + + // Resolve the signature + $resolvedReceivingSignature = Socialbox::resolvePeerSignature($receivingPeer, $rpcRequest->getParameter('receiving_signature_uuid')); + if($resolvedReceivingSignature->getPublicKey() !== $rpcRequest->getParameter('receiving_signature_public_key')) + { + throw new InvalidRpcArgumentException('receiving_signature_public_key', 'Public signing key of the receiving peer does not match the resolved signature'); + } + if($resolvedReceivingSignature->getState() === SigningKeyState::EXPIRED) + { + throw new StandardRpcException('The public signing key of the receiving peer has expired', StandardError::EXPIRED); + } + + $resolvedSignature = Socialbox::resolvePeerSignature($receivingPeer, $rpcRequest->getParameter('receiving_signature_uuid')); + if($resolvedSignature === null) + { + throw new StandardRpcException('The receiving peer signature could not be resolved', StandardError::NOT_FOUND); + } + + return $resolvedSignature; + } + + /** + * @param ClientRequest $request + * @param RpcRequest $rpcRequest + * @return string|null + * @throws InvalidRpcArgumentException + * @throws MissingRpcArgumentException + */ + private static function getChannelUuid(ClientRequest $request, RpcRequest $rpcRequest): ?string + { + if($request->getIdentifyAs() !== null) + { + if(!$rpcRequest->containsParameter('uuid')) + { + throw new MissingRpcArgumentException('uuid'); + } + + if(!Validator::validateUuid($rpcRequest->getParameter('uuid'))) + { + throw new InvalidRpcArgumentException('uuid', 'Invalid UUID V4'); + } + + if(EncryptionChannelManager::channelExists($rpcRequest->getParameter('uuid'))) + { + throw new StandardRpcException('UUID Conflict, a channel with this UUID already exists', StandardError::UUID_CONFLICT); + } + + return $rpcRequest->getParameter('uuid'); + } + + return null; + } + } \ No newline at end of file diff --git a/src/Socialbox/Classes/Validator.php b/src/Socialbox/Classes/Validator.php index 2e3165f..02ab8f5 100644 --- a/src/Socialbox/Classes/Validator.php +++ b/src/Socialbox/Classes/Validator.php @@ -68,4 +68,15 @@ { return checkdate($month, $day, $year); } + + /** + * Validates whether the given UUID is a valid UUID. + * + * @param string $uuid The UUID to validate. + * @return bool Returns true if the provided UUID is valid, otherwise false. + */ + public static function validateUuid(string $uuid): bool + { + return preg_match("/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/", $uuid) === 1; + } } \ No newline at end of file diff --git a/src/Socialbox/Managers/ContactManager.php b/src/Socialbox/Managers/ContactManager.php index 51b06ae..5462e75 100644 --- a/src/Socialbox/Managers/ContactManager.php +++ b/src/Socialbox/Managers/ContactManager.php @@ -11,7 +11,7 @@ use Socialbox\Objects\Database\ContactDatabaseRecord; use Socialbox\Objects\Database\ContactKnownKeyRecord; use Socialbox\Objects\PeerAddress; - use Socialbox\Objects\Standard\ContactRecord; + use Socialbox\Objects\Standard\Contact; use Socialbox\Objects\Standard\SigningKey; class ContactManager @@ -288,7 +288,7 @@ * @param string $peerUuid The unique identifier for the peer whose contacts are to be retrieved. * @param int $limit The maximum number of contacts to retrieve per page. Defaults to 100. * @param int $page The page number to retrieve. Defaults to 1. - * @return ContactRecord[] An array of ContactRecord instances representing the contacts for the given peer. + * @return Contact[] An array of ContactRecord instances representing the contacts for the given peer. * @throws DatabaseOperationException If the database query fails. */ public static function getStandardContacts(string $peerUuid, int $limit=100, int $page=1): array @@ -551,10 +551,10 @@ * * @param string $peerUuid The unique identifier of the peer. * @param string|PeerAddress $contactAddress The contact's address, either as a string or a PeerAddress instance. - * @return ContactRecord|null The standard contact record if found, or null if no matching contact exists. + * @return Contact|null The standard contact record if found, or null if no matching contact exists. * @throws DatabaseOperationException If the database query fails. */ - public static function getStandardContact(string $peerUuid, string|PeerAddress $contactAddress): ?ContactRecord + public static function getStandardContact(string $peerUuid, string|PeerAddress $contactAddress): ?Contact { $contact = self::getContact($peerUuid, $contactAddress); if($contact === null) @@ -562,7 +562,7 @@ return null; } - return new ContactRecord([ + return new Contact([ 'address' => $contact->getContactPeerAddress(), 'relationship' => $contact->getRelationship(), 'known_keys' => self::contactGetSigningKeys($contact), diff --git a/src/Socialbox/Objects/Database/ContactDatabaseRecord.php b/src/Socialbox/Objects/Database/ContactDatabaseRecord.php index 47551a0..d10914b 100644 --- a/src/Socialbox/Objects/Database/ContactDatabaseRecord.php +++ b/src/Socialbox/Objects/Database/ContactDatabaseRecord.php @@ -7,7 +7,7 @@ use InvalidArgumentException; use Socialbox\Enums\Types\ContactRelationshipType; use Socialbox\Interfaces\SerializableInterface; - use Socialbox\Objects\Standard\ContactRecord; + use Socialbox\Objects\Standard\Contact; class ContactDatabaseRecord implements SerializableInterface { @@ -126,11 +126,11 @@ /** * Converts the object to a standard contact record. * - * @return ContactRecord The standard contact record. + * @return Contact The standard contact record. */ - public function toStandard(): ContactRecord + public function toStandard(): Contact { - return new ContactRecord([ + return new Contact([ 'address' => $this->contactPeerAddress, 'relationship' => $this->relationship, 'added_timestamp' => $this->created->getTimestamp() diff --git a/src/Socialbox/Objects/Standard/ContactRecord.php b/src/Socialbox/Objects/Standard/Contact.php similarity index 96% rename from src/Socialbox/Objects/Standard/ContactRecord.php rename to src/Socialbox/Objects/Standard/Contact.php index 8b4cabd..4d56f5a 100644 --- a/src/Socialbox/Objects/Standard/ContactRecord.php +++ b/src/Socialbox/Objects/Standard/Contact.php @@ -7,7 +7,7 @@ use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\PeerAddress; - class ContactRecord implements SerializableInterface + class Contact implements SerializableInterface { private PeerAddress $address; private ContactRelationshipType $relationship; @@ -102,7 +102,7 @@ /** * @inheritDoc */ - public static function fromArray(array $data): ContactRecord + public static function fromArray(array $data): Contact { return new self($data); } From ba3b35de9b7e9f47d0952978f29ede3e0021a1ca Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 7 Feb 2025 22:24:57 -0500 Subject: [PATCH 346/420] Added parameter nullAllowed to return True if the parameter exists AND the parameter is not null if nullAllowed is set to False, by default it's True. --- src/Socialbox/Objects/RpcRequest.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Socialbox/Objects/RpcRequest.php b/src/Socialbox/Objects/RpcRequest.php index 1efe1d8..7751907 100644 --- a/src/Socialbox/Objects/RpcRequest.php +++ b/src/Socialbox/Objects/RpcRequest.php @@ -68,10 +68,16 @@ * Checks if the parameter exists within the RPC request * * @param string $parameter The parameter to check + * @param bool $nullAllowed True if the parameter value can be null, False otherwise. * @return bool True if the parameter exists, False otherwise. */ - public function containsParameter(string $parameter): bool + public function containsParameter(string $parameter, bool $nullAllowed=true): bool { + if(!$nullAllowed) + { + return isset($this->parameters[$parameter]) && $this->parameters[$parameter] !== null; + } + return isset($this->parameters[$parameter]); } From 13e236f244113477d4fe36bdd570f00e6747a6f6 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 7 Feb 2025 22:25:37 -0500 Subject: [PATCH 347/420] Use the new nullAllowed parameter --- .../StandardMethods/AddressBook/AddressBookGetContacts.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContacts.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContacts.php index fa3cf84..f57f358 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContacts.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContacts.php @@ -23,7 +23,7 @@ public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { $limit = Configuration::getPoliciesConfiguration()->getGetContactsLimit(); - if($rpcRequest->containsParameter('limit')) + if($rpcRequest->containsParameter('limit', false)) { $limit = (int)$rpcRequest->getParameter('limit'); if($limit <= 0) @@ -35,7 +35,7 @@ } $page = 0; - if($rpcRequest->containsParameter('page')) + if($rpcRequest->containsParameter('page', false)) { $page = (int)$rpcRequest->getParameter('page'); if($page < 0) From 50e2d18520e8c4b6ac014a2969075b123b8125e8 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 7 Feb 2025 22:30:05 -0500 Subject: [PATCH 348/420] Reversed the default parameter --- src/Socialbox/Objects/RpcRequest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Socialbox/Objects/RpcRequest.php b/src/Socialbox/Objects/RpcRequest.php index 7751907..a5a6013 100644 --- a/src/Socialbox/Objects/RpcRequest.php +++ b/src/Socialbox/Objects/RpcRequest.php @@ -71,7 +71,7 @@ * @param bool $nullAllowed True if the parameter value can be null, False otherwise. * @return bool True if the parameter exists, False otherwise. */ - public function containsParameter(string $parameter, bool $nullAllowed=true): bool + public function containsParameter(string $parameter, bool $nullAllowed=false): bool { if(!$nullAllowed) { From 0b9a566e8f29a4acbe156c601c02de31de4021b8 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 7 Feb 2025 22:30:11 -0500 Subject: [PATCH 349/420] Correction --- .../StandardMethods/AddressBook/AddressBookGetContacts.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContacts.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContacts.php index f57f358..483bfc2 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContacts.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookGetContacts.php @@ -23,7 +23,7 @@ public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { $limit = Configuration::getPoliciesConfiguration()->getGetContactsLimit(); - if($rpcRequest->containsParameter('limit', false)) + if($rpcRequest->containsParameter('limit', true)) { $limit = (int)$rpcRequest->getParameter('limit'); if($limit <= 0) @@ -35,7 +35,7 @@ } $page = 0; - if($rpcRequest->containsParameter('page', false)) + if($rpcRequest->containsParameter('page', true)) { $page = (int)$rpcRequest->getParameter('page'); if($page < 0) From 0396ba5843db7b1232f3b11d8b0dfbdd847243a0 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 7 Feb 2025 22:30:14 -0500 Subject: [PATCH 350/420] Correction --- .../AddressBook/AddressBookRevokeSignature.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookRevokeSignature.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookRevokeSignature.php index 52d00f1..1d2deb9 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookRevokeSignature.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookRevokeSignature.php @@ -37,18 +37,18 @@ throw new InvalidRpcArgumentException('peer', $e); } - if(!$rpcRequest->containsParameter('uuid')) + if(!$rpcRequest->containsParameter('signature_uuid', false)) { - throw new MissingRpcArgumentException('uuid'); + throw new MissingRpcArgumentException('signature_uuid'); } try { - $uuid = Uuid::fromString($rpcRequest->getParameter('uuid')); + $signatureUuid = Uuid::fromString($rpcRequest->getParameter('signature_uuid')); } catch(InvalidArgumentException $e) { - throw new InvalidRpcArgumentException('uuid', $e); + throw new InvalidRpcArgumentException('signature_uuid', $e); } try @@ -69,12 +69,12 @@ try { - if(!ContactManager::contactSigningKeyUuidExists($contact, $uuid)) + if(!ContactManager::contactSigningKeyUuidExists($contact, $signatureUuid)) { return $rpcRequest->produceResponse(false); } - ContactManager::removeContactSigningKey($contact, $uuid); + ContactManager::removeContactSigningKey($contact, $signatureUuid); } catch (DatabaseOperationException $e) { From a03bc464c01c02625b097bb32c216ea204eabe13 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 7 Feb 2025 22:30:45 -0500 Subject: [PATCH 351/420] Correction --- .../StandardMethods/AddressBook/AddressBookRevokeSignature.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookRevokeSignature.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookRevokeSignature.php index 1d2deb9..ed5020d 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookRevokeSignature.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookRevokeSignature.php @@ -37,7 +37,7 @@ throw new InvalidRpcArgumentException('peer', $e); } - if(!$rpcRequest->containsParameter('signature_uuid', false)) + if(!$rpcRequest->containsParameter('signature_uuid')) { throw new MissingRpcArgumentException('signature_uuid'); } From 828b7c9b5c4a0a3d7d55abfd55e0657a8208c98c Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 7 Feb 2025 22:33:19 -0500 Subject: [PATCH 352/420] Renamed uuid to signature_uuid --- .../AddressBook/AddressBookTrustSignature.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookTrustSignature.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookTrustSignature.php index 673d5b0..174a823 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookTrustSignature.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookTrustSignature.php @@ -39,21 +39,21 @@ throw new InvalidRpcArgumentException('peer', $e); } - if(!$rpcRequest->containsParameter('uuid')) + if(!$rpcRequest->containsParameter('signature_uuid')) { - throw new MissingRpcArgumentException('uuid'); + throw new MissingRpcArgumentException('signature_uuid'); } try { - $uuid = Uuid::fromString($rpcRequest->getParameter('uuid')); + $signatureUuid = Uuid::fromString($rpcRequest->getParameter('signature_uuid')); } catch(InvalidArgumentException $e) { - throw new InvalidRpcArgumentException('uuid', $e); + throw new InvalidRpcArgumentException('signature_uuid', $e); } - $signingKey = Socialbox::resolvePeerSignature($address, $uuid); + $signingKey = Socialbox::resolvePeerSignature($address, $signatureUuid); try { From 7c8e0ddf862ead229a92bc4658f18bcec5f96b25 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 7 Feb 2025 22:36:12 -0500 Subject: [PATCH 353/420] Added DuplicationCode NoInspection --- .../StandardMethods/AddressBook/AddressBookRevokeSignature.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookRevokeSignature.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookRevokeSignature.php index ed5020d..849cf79 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookRevokeSignature.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookRevokeSignature.php @@ -20,6 +20,7 @@ { /** * @inheritDoc + * @noinspection DuplicatedCode */ public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { From 9e18db3e68a1edfb342d88b7121715d7ecff04f1 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 7 Feb 2025 22:39:48 -0500 Subject: [PATCH 354/420] Minor improvements --- .../StandardMethods/AddressBook/AddressBookTrustSignature.php | 1 - .../AddressBook/AddressBookUpdateRelationship.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookTrustSignature.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookTrustSignature.php index 174a823..da68386 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookTrustSignature.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookTrustSignature.php @@ -65,7 +65,6 @@ } $contact = ContactManager::getContact($peer, $address); - if(ContactManager::contactGetSigningKeysCount($contact) > Configuration::getPoliciesConfiguration()->getMaxContactSigningKeys()) { return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The contact has exceeded the maximum amount of trusted signatures'); diff --git a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookUpdateRelationship.php b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookUpdateRelationship.php index 7e1898a..b170085 100644 --- a/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookUpdateRelationship.php +++ b/src/Socialbox/Classes/StandardMethods/AddressBook/AddressBookUpdateRelationship.php @@ -34,7 +34,7 @@ } catch(InvalidArgumentException $e) { - throw new InvalidRpcArgumentException('peer'); + throw new InvalidRpcArgumentException('peer', $e); } if(!$rpcRequest->containsParameter('relationship')) From 95dd4eb5f268a02fa465caed6b50186696d84c5a Mon Sep 17 00:00:00 2001 From: netkas Date: Sat, 8 Feb 2025 00:33:19 -0500 Subject: [PATCH 355/420] updated VerifyPeerSignature to make timed signatures optional, updated parameter names and improved validation --- .../Core/VerifyPeerSignature.php | 59 ++++++++------ src/Socialbox/Socialbox.php | 76 ++++++++++++++++++- 2 files changed, 110 insertions(+), 25 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/Core/VerifyPeerSignature.php b/src/Socialbox/Classes/StandardMethods/Core/VerifyPeerSignature.php index 0d7f079..d78c79b 100644 --- a/src/Socialbox/Classes/StandardMethods/Core/VerifyPeerSignature.php +++ b/src/Socialbox/Classes/StandardMethods/Core/VerifyPeerSignature.php @@ -2,13 +2,12 @@ namespace Socialbox\Classes\StandardMethods\Core; - use Exception; use InvalidArgumentException; use Socialbox\Abstracts\Method; - use Socialbox\Enums\StandardError; + use Socialbox\Classes\Cryptography; + use Socialbox\Classes\Validator; use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; use Socialbox\Exceptions\Standard\MissingRpcArgumentException; - use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\ClientRequest; use Socialbox\Objects\PeerAddress; @@ -24,19 +23,23 @@ public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { // Check if the required 'peer' parameter is set. - if(!$rpcRequest->containsParameter('signing_peer')) + if(!$rpcRequest->containsParameter('peer')) { - throw new MissingRpcArgumentException('signing_peer'); + throw new MissingRpcArgumentException('peer'); } if(!$rpcRequest->containsParameter('signature_uuid')) { throw new MissingRpcArgumentException('signature_uuid'); } - - if(!$rpcRequest->containsParameter('signature_key')) + elseif(!Validator::validateUuid($rpcRequest->getParameter('signature_uuid'))) { - throw new MissingRpcArgumentException('signature_key'); + throw new InvalidRpcArgumentException('signature_uuid', 'Invalid UUID V4'); + } + + if(!$rpcRequest->containsParameter('signature_public_key')) + { + throw new MissingRpcArgumentException('signature_public_key'); } if(!$rpcRequest->containsParameter('signature')) @@ -44,40 +47,48 @@ throw new MissingRpcArgumentException('signature'); } - if(!$rpcRequest->containsParameter('message_hash')) + if(!$rpcRequest->containsParameter('sha512')) { - throw new MissingRpcArgumentException('message_hash'); + throw new MissingRpcArgumentException('sha512'); } - - if(!$rpcRequest->containsParameter('signature_time')) + elseif(!Cryptography::validateSha512($rpcRequest->getParameter('sha512'))) { - throw new MissingRpcArgumentException('signature_time'); + throw new InvalidRpcArgumentException('sha512', 'Invalid SHA512'); } // Parse the peer address try { - $peerAddress = PeerAddress::fromAddress($rpcRequest->getParameter('signing_peer')); + $peerAddress = PeerAddress::fromAddress($rpcRequest->getParameter('peer')); } catch(InvalidArgumentException $e) { - throw new InvalidRpcArgumentException('signing_peer', $e); + throw new InvalidRpcArgumentException('peer', $e); } - try + if($rpcRequest->containsParameter('signature_time')) { - return $rpcRequest->produceResponse(Socialbox::verifyPeerSignature( + if(!is_numeric($rpcRequest->getParameter('signature_time'))) + { + throw new InvalidRpcArgumentException('signature_time', 'Invalid timestamp, must be a Unix Timestamp'); + } + + return $rpcRequest->produceResponse(Socialbox::verifyTimedSignature( signingPeer: $peerAddress, signatureUuid: $rpcRequest->getParameter('signature_uuid'), - signatureKey: $rpcRequest->getParameter('signature_key'), + signatureKey: $rpcRequest->getParameter('signature_public_key'), signature: $rpcRequest->getParameter('signature'), - messageHash: $rpcRequest->getParameter('message_hash'), - signatureTime: $rpcRequest->getParameter('signature_time') + messageHash: $rpcRequest->getParameter('sha512'), + signatureTime: (int)$rpcRequest->getParameter('signature_time') )->value); } - catch (Exception $e) - { - throw new StandardRpcException('Failed to resolve peer signature', StandardError::INTERNAL_SERVER_ERROR, $e); - } + + return $rpcRequest->produceResponse(Socialbox::verifySignature( + signingPeer: $peerAddress, + signatureUuid: $rpcRequest->getParameter('signature_uuid'), + signatureKey: $rpcRequest->getParameter('signature_public_key'), + signature: $rpcRequest->getParameter('signature'), + messageHash: $rpcRequest->getParameter('sha512'), + )->value); } } \ No newline at end of file diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index a734458..d47f1b3 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -764,7 +764,7 @@ * @param int $signatureTime The time at which the message was signed * @return SignatureVerificationStatus The status of the signature verification */ - public static function verifyPeerSignature(PeerAddress|string $signingPeer, string $signatureUuid, string $signatureKey, string $signature, string $messageHash, int $signatureTime): SignatureVerificationStatus + public static function verifyTimedSignature(PeerAddress|string $signingPeer, string $signatureUuid, string $signatureKey, string $signature, string $messageHash, int $signatureTime): SignatureVerificationStatus { try { @@ -824,6 +824,80 @@ return SignatureVerificationStatus::VERIFIED; } + /** + * Verifies the signature of a message using the public key of the signing peer both locally and externally. + * If the peer is registered locally, the signature is verified using the local public key. + * If the peer is external, the signature is verified by resolving the peer's public key from the external server. + * The signature is verified against the resolved public key, and the status of the verification is returned. + * + * @param PeerAddress|string $signingPeer The peer address or string identifier of the signing peer + * @param string $signatureUuid The UUID of the signature key to be resolved + * @param string $signatureKey The public key of the signature that was used to sign the message + * @param string $signature The signature to be verified + * @param string $messageHash The SHA-512 hash of the message that was signed + * @return SignatureVerificationStatus The status of the signature verification + */ + public static function verifySignature(PeerAddress|string $signingPeer, string $signatureUuid, string $signatureKey, string $signature, string $messageHash): SignatureVerificationStatus + { + try + { + if (!Cryptography::verifyMessage($messageHash, $signature, $signatureKey, false)) + { + return SignatureVerificationStatus::INVALID; + } + } + catch (CryptographyException) + { + return SignatureVerificationStatus::INVALID; + } + + // Resolve the peer signature key + try + { + $signingKey = self::resolvePeerSignature($signingPeer, $signatureUuid); + } + catch(StandardRpcException) + { + return SignatureVerificationStatus::UNVERIFIED; + } + + if($signingKey === null) + { + return SignatureVerificationStatus::UNVERIFIED; + } + + if($signingKey->getPublicKey() !== $signatureKey) + { + return SignatureVerificationStatus::PUBLIC_KEY_MISMATCH; + } + + if($signingKey->getUuid() !== $signatureUuid) + { + return SignatureVerificationStatus::UUID_MISMATCH; + } + + if(time() > $signingKey->getExpires()) + { + return SignatureVerificationStatus::EXPIRED; + } + + // Verify the signature with the resolved key + try + { + if (!Cryptography::verifyTimedMessage($messageHash, $signature, $signingKey->getPublicKey(), false)) + { + return SignatureVerificationStatus::INVALID; + } + } + catch (CryptographyException) + { + return SignatureVerificationStatus::INVALID; + } + + return SignatureVerificationStatus::VERIFIED; + } + + /** * Resolves a peer signature key based on the given peer address or string identifier. * From 466ebcd1a8c2f626acccc096e852584cac0068d2 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 10 Feb 2025 15:16:45 -0500 Subject: [PATCH 356/420] Implemented getSelf --- .../Classes/StandardMethods/Core/GetSelf.php | 16 ++++++++++------ src/Socialbox/Objects/RpcRequest.php | 10 ++++++++-- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/Core/GetSelf.php b/src/Socialbox/Classes/StandardMethods/Core/GetSelf.php index d5c4a0b..bd2ec5b 100644 --- a/src/Socialbox/Classes/StandardMethods/Core/GetSelf.php +++ b/src/Socialbox/Classes/StandardMethods/Core/GetSelf.php @@ -2,18 +2,14 @@ namespace Socialbox\Classes\StandardMethods\Core; - use InvalidArgumentException; use Socialbox\Abstracts\Method; - use Socialbox\Enums\ReservedUsernames; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Exceptions\Standard\MissingRpcArgumentException; use Socialbox\Exceptions\Standard\StandardRpcException; use Socialbox\Interfaces\SerializableInterface; + use Socialbox\Managers\PeerInformationManager; use Socialbox\Objects\ClientRequest; - use Socialbox\Objects\PeerAddress; use Socialbox\Objects\RpcRequest; - use Socialbox\Socialbox; class GetSelf extends Method { @@ -23,6 +19,14 @@ */ public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface { - + try + { + $selfPeer = $request->getPeer(); + return $rpcRequest->produceResponse($selfPeer->toStandardPeer(PeerInformationManager::getFields($selfPeer))); + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('Unable to resolve self peer', StandardError::INTERNAL_SERVER_ERROR, $e); + } } } \ No newline at end of file diff --git a/src/Socialbox/Objects/RpcRequest.php b/src/Socialbox/Objects/RpcRequest.php index a5a6013..a785d83 100644 --- a/src/Socialbox/Objects/RpcRequest.php +++ b/src/Socialbox/Objects/RpcRequest.php @@ -4,6 +4,7 @@ use InvalidArgumentException; use Socialbox\Classes\Logger; + use Socialbox\Classes\Utilities; use Socialbox\Enums\StandardError; use Socialbox\Enums\StandardMethods; use Socialbox\Exceptions\Standard\StandardRpcException; @@ -19,16 +20,21 @@ * Constructs the object from an array of data. * * @param string|StandardMethods $method The method of the request. - * @param string|null $id The ID of the request. + * @param string|null $id The ID of the request. If 'RANDOM' a random crc32 hash will be used. * @param array|null $parameters The parameters of the request. */ - public function __construct(string|StandardMethods $method, ?string $id, ?array $parameters=null) + public function __construct(string|StandardMethods $method, ?string $id='RANDOM', ?array $parameters=null) { if($method instanceof StandardMethods) { $method = $method->value; } + if($id === 'RANDOM') + { + $id = Utilities::randomCrc32();; + } + $this->method = $method; $this->parameters = $parameters; $this->id = $id; From d9127a50e6e49edd55010c4a911b7415b0396f36 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 10 Feb 2025 15:23:21 -0500 Subject: [PATCH 357/420] Correction --- .../Classes/StandardMethods/Settings/SettingsAddSignature.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddSignature.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddSignature.php index 7d60956..cca89cf 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddSignature.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsAddSignature.php @@ -29,7 +29,7 @@ } $expires = null; - if($rpcRequest->containsParameter('expires') && $rpcRequest->getParameter('expires') !== null) + if($rpcRequest->containsParameter('expires')) { $expires = (int)$rpcRequest->getParameter('expires'); } @@ -39,7 +39,7 @@ } $name = null; - if($rpcRequest->containsParameter('name') && $rpcRequest->getParameter('name') !== null) + if($rpcRequest->containsParameter('name')) { $name = $rpcRequest->getParameter('name'); } From 6f600fa45e54bc0807ada886f71c7406cb95aa03 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 10 Feb 2025 15:37:53 -0500 Subject: [PATCH 358/420] Renamed SettingsGetSigningKey to SettingsGetSignature --- .../{SettingsGetSigningKey.php => SettingsGetSignature.php} | 2 +- src/Socialbox/Enums/StandardMethods.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/Socialbox/Classes/StandardMethods/Settings/{SettingsGetSigningKey.php => SettingsGetSignature.php} (97%) diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSigningKey.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSignature.php similarity index 97% rename from src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSigningKey.php rename to src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSignature.php index a6d3d91..a31814e 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSigningKey.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSignature.php @@ -12,7 +12,7 @@ use Socialbox\Objects\ClientRequest; use Socialbox\Objects\RpcRequest; - class SettingsGetSigningKey extends Method + class SettingsGetSignature extends Method { /** * @inheritDoc diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index 1e41102..ff5590f 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -32,7 +32,7 @@ use Socialbox\Classes\StandardMethods\Settings\SettingsDeleteSignature; use Socialbox\Classes\StandardMethods\Settings\SettingsGetInformationField; use Socialbox\Classes\StandardMethods\Settings\SettingsGetInformationFields; - use Socialbox\Classes\StandardMethods\Settings\SettingsGetSigningKey; + use Socialbox\Classes\StandardMethods\Settings\SettingsGetSignature; use Socialbox\Classes\StandardMethods\Settings\SettingsGetSigningKeys; use Socialbox\Classes\StandardMethods\Settings\SettingsInformationFieldExists; use Socialbox\Classes\StandardMethods\Settings\SettingsSetOtp; @@ -209,7 +209,7 @@ self::SETTINGS_DELETE_SIGNATURE => SettingsDeleteSignature::execute($request, $rpcRequest), self::SETTINGS_GET_INFORMATION_FIELD => SettingsGetInformationField::execute($request, $rpcRequest), self::SETTINGS_GET_INFORMATION_FIELDS => SettingsGetInformationFields::execute($request, $rpcRequest), - self::SETTINGS_GET_SIGNATURE => SettingsGetSigningKey::execute($request, $rpcRequest), + self::SETTINGS_GET_SIGNATURE => SettingsGetSignature::execute($request, $rpcRequest), self::SETTINGS_GET_SIGNATURES => SettingsGetSigningKeys::execute($request, $rpcRequest), self::SETTINGS_INFORMATION_FIELD_EXISTS => SettingsInformationFieldExists::execute($request, $rpcRequest), self::SETTINGS_SET_OTP => SettingsSetOtp::execute($request, $rpcRequest), From 2362e36ca479e7b654d4b9912574f8f27437b2da Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 10 Feb 2025 15:38:43 -0500 Subject: [PATCH 359/420] Renamed SettingsGetSigningKeys to SettingsGetSignatures --- ...ttingsGetSigningKeys.php => SettingsGetSignatures.php} | 2 +- src/Socialbox/Enums/StandardMethods.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) rename src/Socialbox/Classes/StandardMethods/Settings/{SettingsGetSigningKeys.php => SettingsGetSignatures.php} (96%) diff --git a/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSigningKeys.php b/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSignatures.php similarity index 96% rename from src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSigningKeys.php rename to src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSignatures.php index ee4dfb1..78e7039 100644 --- a/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSigningKeys.php +++ b/src/Socialbox/Classes/StandardMethods/Settings/SettingsGetSignatures.php @@ -11,7 +11,7 @@ use Socialbox\Objects\ClientRequest; use Socialbox\Objects\RpcRequest; - class SettingsGetSigningKeys extends Method + class SettingsGetSignatures extends Method { /** * @inheritDoc diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index ff5590f..137c02a 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -33,7 +33,7 @@ use Socialbox\Classes\StandardMethods\Settings\SettingsGetInformationField; use Socialbox\Classes\StandardMethods\Settings\SettingsGetInformationFields; use Socialbox\Classes\StandardMethods\Settings\SettingsGetSignature; - use Socialbox\Classes\StandardMethods\Settings\SettingsGetSigningKeys; + use Socialbox\Classes\StandardMethods\Settings\SettingsGetSignatures; use Socialbox\Classes\StandardMethods\Settings\SettingsInformationFieldExists; use Socialbox\Classes\StandardMethods\Settings\SettingsSetOtp; use Socialbox\Classes\StandardMethods\Settings\SettingsSetPassword; @@ -94,8 +94,8 @@ case SETTINGS_DELETE_SIGNATURE = 'settingsDeleteSigningKey'; case SETTINGS_GET_INFORMATION_FIELD = 'settingsGetInformationField'; case SETTINGS_GET_INFORMATION_FIELDS = 'settingsGetInformationFields'; - case SETTINGS_GET_SIGNATURE = 'settingsGetSigningKey'; - case SETTINGS_GET_SIGNATURES = 'settingsGetSigningKeys'; + case SETTINGS_GET_SIGNATURE = 'settingsGetSignature'; + case SETTINGS_GET_SIGNATURES = 'settingsGetSignatures'; case SETTINGS_INFORMATION_FIELD_EXISTS = 'settingsInformationFieldExists'; case SETTINGS_SET_OTP = 'settingsSetOtp'; case SETTINGS_SET_PASSWORD = 'settingsSetPassword'; @@ -210,7 +210,7 @@ self::SETTINGS_GET_INFORMATION_FIELD => SettingsGetInformationField::execute($request, $rpcRequest), self::SETTINGS_GET_INFORMATION_FIELDS => SettingsGetInformationFields::execute($request, $rpcRequest), self::SETTINGS_GET_SIGNATURE => SettingsGetSignature::execute($request, $rpcRequest), - self::SETTINGS_GET_SIGNATURES => SettingsGetSigningKeys::execute($request, $rpcRequest), + self::SETTINGS_GET_SIGNATURES => SettingsGetSignatures::execute($request, $rpcRequest), self::SETTINGS_INFORMATION_FIELD_EXISTS => SettingsInformationFieldExists::execute($request, $rpcRequest), self::SETTINGS_SET_OTP => SettingsSetOtp::execute($request, $rpcRequest), self::SETTINGS_SET_PASSWORD => SettingsSetPassword::execute($request, $rpcRequest), From ee48456f2ffeed14c88476e8a50eadfe5dfc171a Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 10 Feb 2025 15:40:20 -0500 Subject: [PATCH 360/420] Renamed SigningKey to Signature --- .../Encryption/EncryptionCreateChannel.php | 10 +++++----- src/Socialbox/Managers/ContactManager.php | 6 +++--- src/Socialbox/Objects/Database/SigningKeyRecord.php | 8 ++++---- .../Objects/Standard/{SigningKey.php => Signature.php} | 8 ++++---- src/Socialbox/Socialbox.php | 6 +++--- 5 files changed, 19 insertions(+), 19 deletions(-) rename src/Socialbox/Objects/Standard/{SigningKey.php => Signature.php} (95%) diff --git a/src/Socialbox/Classes/StandardMethods/Encryption/EncryptionCreateChannel.php b/src/Socialbox/Classes/StandardMethods/Encryption/EncryptionCreateChannel.php index f690365..b4eb8a5 100644 --- a/src/Socialbox/Classes/StandardMethods/Encryption/EncryptionCreateChannel.php +++ b/src/Socialbox/Classes/StandardMethods/Encryption/EncryptionCreateChannel.php @@ -20,7 +20,7 @@ use Socialbox\Objects\ClientRequest; use Socialbox\Objects\PeerAddress; use Socialbox\Objects\RpcRequest; - use Socialbox\Objects\Standard\SigningKey; + use Socialbox\Objects\Standard\Signature; use Socialbox\Socialbox; class EncryptionCreateChannel extends Method @@ -150,12 +150,12 @@ * * @param PeerAddress $callingPeer The calling peer * @param RpcRequest $rpcRequest The focused RPC request - * @return SigningKey The resolved signing key + * @return Signature The resolved signing key * @throws InvalidRpcArgumentException If one or more RPC parameters are invalid * @throws MissingRpcArgumentException If one or more RPC parameters are missing * @throws StandardRpcException If the calling signature cannot be resolved */ - private static function getCallingSignature(PeerAddress $callingPeer, RpcRequest $rpcRequest): SigningKey + private static function getCallingSignature(PeerAddress $callingPeer, RpcRequest $rpcRequest): Signature { // Caller signature verification if(!$rpcRequest->containsParameter('calling_signature_uuid')) @@ -238,12 +238,12 @@ /** * @param PeerAddress $receivingPeer * @param RpcRequest $rpcRequest - * @return SigningKey + * @return Signature * @throws InvalidRpcArgumentException * @throws MissingRpcArgumentException * @throws StandardRpcException */ - private static function getReceivingSignature(PeerAddress $receivingPeer, RpcRequest $rpcRequest): SigningKey + private static function getReceivingSignature(PeerAddress $receivingPeer, RpcRequest $rpcRequest): Signature { // Receiving signature verification if(!$rpcRequest->containsParameter('receiving_signature_uuid')) diff --git a/src/Socialbox/Managers/ContactManager.php b/src/Socialbox/Managers/ContactManager.php index 5462e75..29941f9 100644 --- a/src/Socialbox/Managers/ContactManager.php +++ b/src/Socialbox/Managers/ContactManager.php @@ -12,7 +12,7 @@ use Socialbox\Objects\Database\ContactKnownKeyRecord; use Socialbox\Objects\PeerAddress; use Socialbox\Objects\Standard\Contact; - use Socialbox\Objects\Standard\SigningKey; + use Socialbox\Objects\Standard\Signature; class ContactManager { @@ -334,11 +334,11 @@ * Adds a signing key to a contact in the database. * * @param string|ContactDatabaseRecord $contactUuid The unique identifier of the contact to add the signing key to. - * @param SigningKey $signingKey The signing key to add to the contact. + * @param Signature $signingKey The signing key to add to the contact. * @return void * @throws DatabaseOperationException If the database query fails. */ - public static function addContactSigningKey(string|ContactDatabaseRecord $contactUuid, SigningKey $signingKey): void + public static function addContactSigningKey(string|ContactDatabaseRecord $contactUuid, Signature $signingKey): void { if($contactUuid instanceof ContactDatabaseRecord) { diff --git a/src/Socialbox/Objects/Database/SigningKeyRecord.php b/src/Socialbox/Objects/Database/SigningKeyRecord.php index 2ca95f5..c3f100d 100644 --- a/src/Socialbox/Objects/Database/SigningKeyRecord.php +++ b/src/Socialbox/Objects/Database/SigningKeyRecord.php @@ -6,7 +6,7 @@ use InvalidArgumentException; use Socialbox\Enums\SigningKeyState; use Socialbox\Interfaces\SerializableInterface; - use Socialbox\Objects\Standard\SigningKey; + use Socialbox\Objects\Standard\Signature; class SigningKeyRecord implements SerializableInterface { @@ -188,10 +188,10 @@ /** * Converts the current signing key record to its standard format. * - * @return SigningKey The signing key in its standard format. + * @return Signature The signing key in its standard format. */ - public function toStandard(): SigningKey + public function toStandard(): Signature { - return SigningKey::fromSigningKeyRecord($this); + return Signature::fromSigningKeyRecord($this); } } \ No newline at end of file diff --git a/src/Socialbox/Objects/Standard/SigningKey.php b/src/Socialbox/Objects/Standard/Signature.php similarity index 95% rename from src/Socialbox/Objects/Standard/SigningKey.php rename to src/Socialbox/Objects/Standard/Signature.php index cfc216e..4092cf6 100644 --- a/src/Socialbox/Objects/Standard/SigningKey.php +++ b/src/Socialbox/Objects/Standard/Signature.php @@ -8,7 +8,7 @@ use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\Database\SigningKeyRecord; - class SigningKey implements SerializableInterface + class Signature implements SerializableInterface { private string $uuid; private string $name; @@ -135,9 +135,9 @@ * Creates a new SigningKey instance from a SigningKeyRecord. * * @param SigningKeyRecord $record The record containing the signing key data. - * @return SigningKey An instance of SigningKey populated with data from the provided record. + * @return Signature An instance of SigningKey populated with data from the provided record. */ - public static function fromSigningKeyRecord(SigningKeyRecord $record): SigningKey + public static function fromSigningKeyRecord(SigningKeyRecord $record): Signature { return new self([ 'uuid' => $record->getUuid(), @@ -152,7 +152,7 @@ /** * @inheritDoc */ - public static function fromArray(array $data): SigningKey + public static function fromArray(array $data): Signature { return new self($data); } diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index d47f1b3..4c5cee8 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -39,7 +39,7 @@ use Socialbox\Objects\Standard\InformationField; use Socialbox\Objects\Standard\Peer; use Socialbox\Objects\Standard\ServerInformation; - use Socialbox\Objects\Standard\SigningKey; + use Socialbox\Objects\Standard\Signature; use Throwable; class Socialbox @@ -903,10 +903,10 @@ * * @param PeerAddress|string $peerAddress The peer address or string identifier to be resolved. * @param string $signatureUuid The UUID of the signature key to be resolved. - * @return SigningKey|null The resolved signing key for the peer. Null if not found + * @return Signature|null The resolved signing key for the peer. Null if not found * @throws StandardRpcException If there was an error while resolving the peer signature key. */ - public static function resolvePeerSignature(PeerAddress|string $peerAddress, string $signatureUuid): ?SigningKey + public static function resolvePeerSignature(PeerAddress|string $peerAddress, string $signatureUuid): ?Signature { // Convert string peer address to object PeerAddress if(is_string($peerAddress)) From a456dda62dcf20ec93774e42c1cd89136e391029 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 11 Feb 2025 13:16:23 -0500 Subject: [PATCH 361/420] Improved exception handling --- .../Verification/VerificationAnswerImageCaptcha.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/Verification/VerificationAnswerImageCaptcha.php b/src/Socialbox/Classes/StandardMethods/Verification/VerificationAnswerImageCaptcha.php index cc840c4..c6a835b 100644 --- a/src/Socialbox/Classes/StandardMethods/Verification/VerificationAnswerImageCaptcha.php +++ b/src/Socialbox/Classes/StandardMethods/Verification/VerificationAnswerImageCaptcha.php @@ -27,10 +27,9 @@ throw new MissingRpcArgumentException('answer'); } - $session = $request->getSession(); - try { + $session = $request->getSession(); if(CaptchaManager::getCaptcha($session->getPeerUuid())?->isExpired()) { return $rpcRequest->produceError(StandardError::EXPIRED, 'The captcha has expired'); From b076f80ce5cd95bc04fbead102bb4568fdf8e64d Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 11 Feb 2025 13:31:15 -0500 Subject: [PATCH 362/420] Updated docs --- src/Socialbox/Enums/Status/SignatureVerificationStatus.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Socialbox/Enums/Status/SignatureVerificationStatus.php b/src/Socialbox/Enums/Status/SignatureVerificationStatus.php index 3fb4cb0..aa5d277 100644 --- a/src/Socialbox/Enums/Status/SignatureVerificationStatus.php +++ b/src/Socialbox/Enums/Status/SignatureVerificationStatus.php @@ -10,17 +10,17 @@ case INVALID = 'INVALID'; /** - * The provided signature was valid but the public key used to verify the signature was not the expected public key. + * The provided signature was valid, but the public key used to verify the signature was not the expected public key. */ case PUBLIC_KEY_MISMATCH = 'PUBLIC_KEY_MISMATCH'; /** - * The provided signature was valid but the UUID used to verify the signature was not the expected UUID. + * The provided signature was valid, but the UUID used to verify the signature was not the expected UUID. */ case UUID_MISMATCH = 'UUID_MISMATCH'; /** - * The provided signature was valid but the signing key has expired. + * The provided signature was valid, but the signing key has expired. */ case EXPIRED = 'EXPIRED'; From 25d1a001fd0c66627eeac67ef1aef9119a2fa1f0 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 11 Feb 2025 13:44:00 -0500 Subject: [PATCH 363/420] Rewrote SocialClient --- src/Socialbox/SocialClient.php | 1221 ++++++++++++++++++-------------- 1 file changed, 678 insertions(+), 543 deletions(-) diff --git a/src/Socialbox/SocialClient.php b/src/Socialbox/SocialClient.php index 57c99e6..2d19b77 100644 --- a/src/Socialbox/SocialClient.php +++ b/src/Socialbox/SocialClient.php @@ -6,26 +6,25 @@ use Socialbox\Classes\Cryptography; use Socialbox\Classes\RpcClient; - use Socialbox\Classes\Utilities; use Socialbox\Enums\PrivacyState; use Socialbox\Enums\StandardMethods; + use Socialbox\Enums\Status\SignatureVerificationStatus; + use Socialbox\Enums\Types\ContactRelationshipType; use Socialbox\Enums\Types\InformationFieldName; use Socialbox\Exceptions\CryptographyException; use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\ResolutionException; use Socialbox\Exceptions\RpcException; use Socialbox\Objects\Client\ExportedSession; - use Socialbox\Objects\Client\SignatureKeyPair; use Socialbox\Objects\PeerAddress; use Socialbox\Objects\RpcRequest; - use Socialbox\Objects\Standard\ExternalUrlVerification; + use Socialbox\Objects\Standard\Contact; use Socialbox\Objects\Standard\ImageCaptchaVerification; - use Socialbox\Objects\Standard\InformationField; + use Socialbox\Objects\Standard\InformationFieldState; use Socialbox\Objects\Standard\Peer; use Socialbox\Objects\Standard\ServerDocument; use Socialbox\Objects\Standard\SessionState; - use Socialbox\Objects\Standard\SigningKey; - use Socialbox\Objects\Standard\TextCaptchaVerification; + use Socialbox\Objects\Standard\Signature; class SocialClient extends RpcClient { @@ -40,567 +39,468 @@ * @throws ResolutionException If the domain cannot be resolved. * @throws RpcException If the RPC request fails. */ - public function __construct(string|PeerAddress $identifiedAs, ?string $server=null, ?ExportedSession $exportedSession=null) + public function __construct(PeerAddress|string $identifiedAs, ?string $server=null, ?ExportedSession $exportedSession=null) { parent::__construct($identifiedAs, $server, $exportedSession); } /** - * Sends a ping request to the server and checks the response. + * Adds a new peer to the AddressBook, returns True upon success or False if the contact already exists in + * the address book. * - * @return true Returns true if the ping request succeeds. - * @throws RpcException Thrown if the RPC request fails. + * @param PeerAddress|string $peer The address of the peer to add as a contact + * @param string|ContactRelationshipType|null $relationship Optional. The relationship for the peer + * @return bool Returns True if the contact was created, False if it already exists + * @throws RpcException Thrown if there was an error with the RPC request */ - public function ping(): true + public function addressBookAddContact(PeerAddress|string $peer, null|string|ContactRelationshipType $relationship=ContactRelationshipType::MUTUAL): bool { + if($peer instanceof PeerAddress) + { + $peer = $peer->getAddress(); + } + + if($relationship instanceof ContactRelationshipType) + { + $relationship = $relationship->value; + } + return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::PING, Utilities::randomCrc32()) + new RpcRequest(StandardMethods::ADDRESS_BOOK_ADD_CONTACT, parameters: [ + 'peer' => $peer, + 'relationship' => $relationship?->value + ]) )->getResponse()->getResult(); } /** - * Retrieves the current state of the session from the server. + * Checks if the given Peer Address exists as a contact in the address book, returns True if it exists or + * False otherwise. * - * @return SessionState Returns an instance of SessionState representing the session's current state. - * @throws RpcException Thrown if the RPC request fails. + * @param PeerAddress|string $peer The address of the peer to check + * @return bool Returns True if the contact exists, False otherwise + * @throws RpcException Thrown if there was an error with the RPC request */ - public function getSessionState(): SessionState + public function addressBookContactExists(PeerAddress|string $peer): bool { - return SessionState::fromArray($this->sendRequest( - new RpcRequest(StandardMethods::GET_SESSION_STATE, Utilities::randomCrc32()) + if($peer instanceof PeerAddress) + { + $peer = $peer->getAddress(); + } + + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::ADDRESS_BOOK_CONTACT_EXISTS, parameters: [ + 'peer' => $peer + ]) + )->getResponse()->getResult(); + } + + /** + * Deletes a contact from the address book, returns True if the contact was deleted or False if the contact + * does not exist. + * + * @param PeerAddress|string $peer The address of the peer to delete + * @return bool Returns True if the contact was deleted, False if the contact does not exist + * @throws RpcException Thrown if there was an error with the RPC request + */ + public function addressBookDeleteContact(PeerAddress|string $peer): bool + { + if($peer instanceof PeerAddress) + { + $peer = $peer->getAddress(); + } + + return (bool)$this->sendRequest( + new RpcRequest(StandardMethods::ADDRESS_BOOK_DELETE_CONTACT, parameters: [ + 'peer' => $peer + ]) + )->getResponse()->getResult(); + } + + /** + * Retrieves a contact from the address book, returns the contact as a Contact object. + * + * @param PeerAddress|string $peer The address of the peer to retrieve + * @return Contact The contact as a Contact object + * @throws RpcException Thrown if there was an error with the RPC request + */ + public function addressBookGetContact(PeerAddress|string $peer): Contact + { + if($peer instanceof PeerAddress) + { + $peer = $peer->getAddress(); + } + + return new Contact($this->sendRequest( + new RpcRequest(StandardMethods::ADDRESS_BOOK_GET_CONTACT, parameters: [ + 'peer' => $peer + ]) )->getResponse()->getResult()); } /** - * Retrieves the list of allowed methods, these are the methods that can be called by the client. + * Retrieves a list of contacts from the address book, returns an array of Contact objects. * - * @return array The allowed methods returned from the RPC request. - * @throws RpcException Thrown if the RPC request fails. + * @param int $page Optional. The page number to retrieve + * @param int|null $limit Optional. The number of contacts to retrieve + * @return Contact[] An array of Contact objects + * @throws RpcException Thrown if there was an error with the RPC request + */ + public function addressBookGetContacts(int $page=0, ?int $limit=null): array + { + $request = new RpcRequest(StandardMethods::ADDRESS_BOOK_GET_CONTACTS, parameters: [ + 'page' => $page, + 'limit' => $limit + ]); + + return array_map(fn($contact) => new Contact($contact), $this->sendRequest($request)->getResponse()->getResult()); + } + + /** + * Revokes a known signature associated with a peer in the address book, returns True if the signature was + * revoked or False if the signature does not exist. + * + * @param PeerAddress|string $peer The address of the peer to revoke the signature from + * @param string $signatureUuid The UUID of the signature to revoke + * @return bool Returns True if the signature was revoked, False if the signature does not exist + * @throws RpcException Thrown if there was an error with the RPC request + */ + public function addressBookRevokeSignature(PeerAddress|string $peer, string $signatureUuid): bool + { + if($peer instanceof PeerAddress) + { + $peer = $peer->getAddress(); + } + + return $this->sendRequest( + new RpcRequest(StandardMethods::ADDRESS_BOOK_REVOKE_SIGNATURE, parameters: [ + 'peer' => $peer, + 'signature_uuid' => $signatureUuid + ]) + )->getResponse()->getResult(); + } + + /** + * Trusts a known signature associated with a peer in the address book, returns True if the signature was trusted + * or False if the signature does not exist. + * + * @param PeerAddress|string $peer The address of the peer to trust the signature from + * @param string $signatureUuid The UUID of the signature to trust + * @return bool Returns True if the signature was trusted, False if the signature does not exist + * @throws RpcException Thrown if there was an error with the RPC request + */ + public function addressBookTrustSignature(PeerAddress|string $peer, string $signatureUuid): bool + { + if($peer instanceof PeerAddress) + { + $peer = $peer->getAddress(); + } + + return $this->sendRequest( + new RpcRequest(StandardMethods::ADDRESS_BOOK_TRUST_SIGNATURE, parameters: [ + 'peer' => $peer, + 'signature_uuid' => $signatureUuid + ]) + )->getResponse()->getResult(); + } + + /** + * Updates the relationship of a peer in the address book, returns True if the relationship was updated or False + * if the relationship does not exist. + * + * @param PeerAddress|string $peer The address of the peer to update the relationship for + * @param ContactRelationshipType|string $relationship The relationship to update to + * @return bool Returns True if the relationship was updated, False if the relationship does not exist + * @throws RpcException Thrown if there was an error with the RPC request + */ + public function addressBookUpdateRelationship(PeerAddress|string $peer, ContactRelationshipType|string $relationship): bool + { + if($peer instanceof PeerAddress) + { + $peer = $peer->getAddress(); + } + + if($relationship instanceof ContactRelationshipType) + { + $relationship = $relationship->value; + } + + return $this->sendRequest( + new RpcRequest(StandardMethods::ADDRESS_BOOK_UPDATE_RELATIONSHIP, parameters: [ + 'peer' => $peer, + 'relationship' => $relationship + ]) + )->getResponse()->getResult(); + } + + /** + * Retrieves a list of all available methods that can be called on the server, returns an array of method names. + * + * @return string[] An array of method names + * @throws RpcException Thrown if there was an error with the RPC request */ public function getAllowedMethods(): array { return $this->sendRequest( - new RpcRequest(StandardMethods::GET_ALLOWED_METHODS, Utilities::randomCrc32()) + new RpcRequest(StandardMethods::GET_ALLOWED_METHODS) )->getResponse()->getResult(); } /** - * Fetches the privacy policy document by sending a remote procedure call request. + * Retrieves the authenticated peer associated with the session, returns the peer as a Peer object. * - * @return ServerDocument The privacy policy document retrieved from the server. - * @throws RpcException Thrown if the RPC request fails. + * @return Peer The peer as a Peer object + * @throws RpcException Thrown if there was an error with the RPC request */ - public function getPrivacyPolicy(): ServerDocument + public function getSelf(): Peer { - return ServerDocument::fromArray($this->sendRequest( - new RpcRequest(StandardMethods::GET_PRIVACY_POLICY, Utilities::randomCrc32()) - )->getResponse()->getResult()); - } - - /** - * Accepts the privacy policy by sending a request to the server. - * - * @return true Returns true if the privacy policy is successfully accepted. - * @throws RpcException Thrown if the RPC request fails. - */ - public function acceptPrivacyPolicy(): true - { - return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::ACCEPT_PRIVACY_POLICY, Utilities::randomCrc32()) + return $this->sendRequest( + new RpcRequest(StandardMethods::GET_SELF) )->getResponse()->getResult(); } /** - * Retrieves the terms of service document by sending a remote procedure call request. + * Retrieves the session state from the server, returns the session state as a SessionState object. * - * @return ServerDocument The terms of service document retrieved from the server. - * @throws RpcException Thrown if the RPC request fails. + * @return SessionState The session state as a SessionState object + * @throws RpcException Thrown if there was an error with the RPC request */ - public function getTermsOfService(): ServerDocument + public function getSessionState(): SessionState { - return ServerDocument::fromArray($this->sendRequest( - new RpcRequest(StandardMethods::GET_TERMS_OF_SERVICE, Utilities::randomCrc32()) + return new SessionState($this->sendRequest( + new RpcRequest(StandardMethods::GET_SESSION_STATE) )->getResponse()->getResult()); } /** - * Sends a request to accept the terms of service and verifies the response. + * Pings the server to check if it is online, returns True if the server is online. * - * @return true Returns true if the terms of service are successfully accepted. - * @throws RpcException Thrown if the RPC request fails. + * @return true Returns True if the server is online + * @throws RpcException Thrown if there was an error with the RPC request */ - public function acceptTermsOfService(): true + public function ping(): true { - return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::ACCEPT_TERMS_OF_SERVICE, Utilities::randomCrc32()) + return $this->sendRequest( + new RpcRequest(StandardMethods::PING) )->getResponse()->getResult(); } /** - * Fetches the community guidelines document from the server by sending a remote procedure call request. + * Resolves a peer address to a Peer object, returns the peer as a Peer object. This is a decentralized + * method, meaning that the peer address can be resolved from any address even if the address doesn't + * belong to the server the request is being sent to. * - * @return ServerDocument The community guidelines document retrieved from the server. - * @throws RpcException Thrown if the RPC request fails. + * @param PeerAddress|string $peer The address of the peer to resolve + * @param PeerAddress|string|null $identifiedAs Optional. The address of the peer to identify as + * @return Peer The peer as a Peer object + * @throws RpcException Thrown if there was an error with the RPC request */ - public function getCommunityGuidelines(): ServerDocument + public function resolvePeer(PeerAddress|string $peer, null|PeerAddress|string $identifiedAs=null): Peer { - return ServerDocument::fromArray($this->sendRequest( - new RpcRequest(StandardMethods::GET_COMMUNITY_GUIDELINES, Utilities::randomCrc32()) + if($peer instanceof PeerAddress) + { + $peer = $peer->getAddress(); + } + + if($identifiedAs instanceof PeerAddress) + { + $identifiedAs = $identifiedAs->getAddress(); + } + + return new Peer($this->sendRequest( + new RpcRequest(StandardMethods::RESOLVE_PEER, parameters: [ + 'peer' => $peer + ]), true, $identifiedAs )->getResponse()->getResult()); } /** - * Sends a request to accept the community guidelines via a remote procedure call. + * Resolves a peer signature to a Signature object, returns the signature as a Signature object. This is + * a decentralized method, meaning that the signature can be resolved from any address even if the address + * doesn't belong to the server the request is being sent to. * - * @return true Indicates that the community guidelines have been successfully accepted. - * @throws RpcException Thrown if the RPC request encounters an error. + * @param PeerAddress|string $peer The address of the peer to resolve the signature from + * @param string $signatureUuid The UUID of the signature to resolve + * @return Signature|null The signature as a Signature object, or null if the signature does not exist + * @throws RpcException Thrown if there was an error with the RPC request + */ + public function resolvePeerSignature(PeerAddress|string $peer, string $signatureUuid): ?Signature + { + if($peer instanceof PeerAddress) + { + $peer = $peer->getAddress(); + } + + $result = $this->sendRequest( + new RpcRequest(StandardMethods::RESOLVE_PEER_SIGNATURE, parameters: [ + 'peer' => $peer, + 'signature_uuid' => $signatureUuid + ]) + )->getResponse()->getResult(); + + if($result === null) + { + return null; + } + + return new Signature($result); + } + + /** + * Verifies signature authenticity by resolving the signature UUID and comparing the given parameters with the + * signature data, returns True if the signature is verified. This is a decentralized method, meaning that any + * signature UUID can be verified for as longas the $peer parameter is the address of the peer that created the + * signature. + * + * @param PeerAddress|string $peer The address of the peer to verify the signature for + * @param string $signatureUuid The UUID of the signature to verify + * @param string $signaturePublicKey The public key that was used to create the signature + * @param string $signature The signature to verify + * @param string $sha512 The SHA512 hash of the data that was signed + * @param int|null $signatureTime Optional. The timestamp of the signature creation time + * @return SignatureVerificationStatus the status of the verification + * @throws RpcException Thrown if there was an error with the RPC request + */ + public function verifyPeerSignature(PeerAddress|string $peer, string $signatureUuid, string $signaturePublicKey, string $signature, string $sha512, ?int $signatureTime=null): SignatureVerificationStatus + { + if($peer instanceof PeerAddress) + { + $peer = $peer->getAddress(); + } + + return SignatureVerificationStatus::tryFrom($this->sendRequest( + new RpcRequest(StandardMethods::VERIFY_PEER_SIGNATURE, parameters: [ + 'peer' => $peer, + 'signature_uuid' => $signatureUuid, + 'signature_public_key' => $signaturePublicKey, + 'signature' => $signature, + 'sha512' => $sha512, + 'signature_time' => $signatureTime + ]) + )->getResponse()->getResult()) ?? SignatureVerificationStatus::INVALID; + } + + /** + * Accepts the community guidelines, returns True if the guidelines were accepted. + * + * @return true Returns True if the guidelines were accepted + * @throws RpcException Thrown if there was an error with the RPC request */ public function acceptCommunityGuidelines(): true { return $this->sendRequest( - new RpcRequest(StandardMethods::ACCEPT_COMMUNITY_GUIDELINES, Utilities::randomCrc32()) + new RpcRequest(StandardMethods::ACCEPT_COMMUNITY_GUIDELINES) )->getResponse()->getResult(); } /** - * Sends a verification email to the specified email address by making a remote procedure call request. + * Accepts the privacy policy, returns True if the privacy policy was accepted. * - * @return true Indicates the successful initiation of the verification process. - * @throws RpcException Thrown if the RPC request fails. + * @return true Returns True if the privacy policy was accepted + * @throws RpcException Thrown if there was an error with the RPC request */ - public function verificationEmail(): true + public function acceptPrivacyPolicy(): bool { - return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_EMAIL, Utilities::randomCrc32()) + return $this->sendRequest( + new RpcRequest(StandardMethods::ACCEPT_PRIVACY_POLICY) )->getResponse()->getResult(); } /** - * Confirms a verification process using an email verification code by sending a remote procedure call request. + * Accepts the terms of service, returns True if the terms of service were accepted. * - * @param string $verificationCode The verification code to validate the email. - * @return true The result indicating the successful processing of the verification. - * @throws RpcException Thrown if the RPC request fails. + * @return true Returns True if the terms of service were accepted + * @throws RpcException Thrown if there was an error with the RPC request */ - public function verificationAnswerEmail(string $verificationCode): true + public function acceptTermsOfService(): bool { - return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_ANSWER_EMAIL, Utilities::randomCrc32(), [ - 'verification_code' => $verificationCode - ]) + return $this->sendRequest( + new RpcRequest(StandardMethods::ACCEPT_TERMS_OF_SERVICE) )->getResponse()->getResult(); } /** - * Sends a verification SMS to the specified phone number by initiating a remote procedure call. + * Retrieves the community guidelines, returns the guidelines as a ServerDocument object. * - * @return true True if the SMS was sent successfully. - * @throws RpcException Thrown if the RPC request fails. + * @return ServerDocument The guidelines as a ServerDocument object + * @throws RpcException Thrown if there was an error with the RPC request */ - public function verificationSms(): true + public function getCommunityGuidelines(): ServerDocument { - return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_SMS, Utilities::randomCrc32()) - )->getResponse()->getResult(); - } - - /** - * Sends a verification SMS answer by providing the verification code through a remote procedure call request. - * - * @param string $verificationCode The verification code to be sent for completing the SMS verification process. - * @return true Returns true if the verification is successfully processed. - * @throws RpcException Thrown if the RPC request fails. - */ - public function verificationAnswerSms(string $verificationCode): true - { - return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_ANSWER_SMS, Utilities::randomCrc32(), [ - 'verification_code' => $verificationCode - ]) - )->getResponse()->getResult(); - } - - /** - * Initiates a phone verification process by sending a remote procedure call request. - * - * @return bool True if the phone verification request was successful. - * @throws RpcException Thrown if the RPC request fails. - */ - public function verificationPhone(): true - { - return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_PHONE_CALL, Utilities::randomCrc32()) - )->getResponse()->getResult(); - } - - /** - * Answers a verification phone call by sending a remote procedure call request with the provided verification code. - * - * @param string $verificationCode The verification code to authenticate the phone call. - * @return true Returns true if the verification phone call was successfully answered. - * @throws RpcException Thrown if the RPC request fails. - */ - public function verificationAnswerPhone(string $verificationCode): true - { - return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_ANSWER_PHONE_CALL, Utilities::randomCrc32(), [ - 'verification_code' => $verificationCode - ]) - )->getResponse()->getResult(); - } - - /** - * Retrieves the image captcha for verification purposes by sending a remote procedure call request. - * - * @return ImageCaptchaVerification The result of the image captcha request. - * @throws RpcException Thrown if the RPC request fails. - */ - public function verificationGetImageCaptcha(): ImageCaptchaVerification - { - return ImageCaptchaVerification::fromArray($this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_GET_IMAGE_CAPTCHA, Utilities::randomCrc32()) + return new ServerDocument($this->sendRequest( + new RpcRequest(StandardMethods::GET_COMMUNITY_GUIDELINES) )->getResponse()->getResult()); } /** - * Submits the answer for an image captcha verification by sending a remote procedure call request. + * Retrieves the privacy policy, returns the policy as a ServerDocument object. * - * @param string $verificationCode The code provided as the answer to the image captcha. - * @return true Returns true if the captcha answer is successfully verified. - * @throws RpcException Thrown if the RPC request fails. + * @return ServerDocument The policy as a ServerDocument object + * @throws RpcException Thrown if there was an error with the RPC request */ - public function verificationAnswerImageCaptcha(string $verificationCode): true + public function getPrivacyPolicy(): ServerDocument { - return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_ANSWER_IMAGE_CAPTCHA, Utilities::randomCrc32(), [ - 'verification_code' => $verificationCode - ]) - )->getResponse()->getResult(); - } - - /** - * Retrieves the text captcha verification response. - * - * @return TextCaptchaVerification The result of the text captcha verification request. - * @throws RpcException Thrown if the RPC request fails. - */ - public function verificationGetTextCaptcha(): TextCaptchaVerification - { - return TextCaptchaVerification::fromArray($this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_GET_TEXT_CAPTCHA, Utilities::randomCrc32()) + return new ServerDocument($this->sendRequest( + new RpcRequest(StandardMethods::GET_PRIVACY_POLICY) )->getResponse()->getResult()); } /** - * Sends a request to answer a text-based captcha for verification purposes. + * Retrieves the terms of service, returns the terms as a ServerDocument object. * - * @param string $verificationCode The code provided to answer the captcha. - * @return true Returns true if the captcha answer was successfully processed. - * @throws RpcException Thrown if the RPC request fails. + * @return ServerDocument The terms as a ServerDocument object + * @throws RpcException Thrown if there was an error with the RPC request */ - public function verificationAnswerTextCaptcha(string $verificationCode): true + public function getTermsOfService(): ServerDocument { - return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_ANSWER_TEXT_CAPTCHA, Utilities::randomCrc32(), [ - 'verification_code' => $verificationCode - ]) - )->getResponse()->getResult(); - } - - /** - * Retrieves the external URL for verification purposes by sending a remote procedure call request. - * - * @return ExternalUrlVerification The result of the verification URL request. - * @throws RpcException Thrown if the RPC request fails. - */ - public function verificationGetExternalUrl(): ExternalUrlVerification - { - return ExternalUrlVerification::fromArray($this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_GET_EXTERNAL_URL, Utilities::randomCrc32()) + return new ServerDocument($this->sendRequest( + new RpcRequest(StandardMethods::GET_TERMS_OF_SERVICE) )->getResponse()->getResult()); } /** - * Sends a verification code to answer an external URL for verification purposes. + * Adds a new information field to the peer's profile, returns True if the field was added. * - * @param string $verificationCode The verification code to be sent. - * @return true The result of the verification operation. - * @throws RpcException Thrown if the RPC request fails. + * @param InformationFieldName|string $field The name of the field to add + * @param string $value The value of the field + * @param PrivacyState|string|null $privacy Optional. The privacy state of the field + * @return bool Returns True if the field was added + * @throws RpcException Thrown if there was an error with the RPC request */ - public function verificationAnswerExternalUrl(string $verificationCode): true + public function settingsAddInformationField(InformationFieldName|string $field, string $value, null|PrivacyState|string $privacy): bool { - return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_ANSWER_EXTERNAL_URL, Utilities::randomCrc32(), [ - 'verification_code' => $verificationCode - ]) - )->getResponse()->getResult(); - } - - /** - * Authenticates a password by sending a remote procedure call request with an optional hashing operation. - * - * @param string $password The password to authenticate. - * @param bool $hash Indicates whether the password should be hashed using SHA-512 before authentication. - * @return bool The result of the password authentication request. - * @throws CryptographyException Thrown if the password hash is invalid. - * @throws RpcException Thrown if the RPC request fails. - */ - public function verificationPasswordAuthentication(string $password, bool $hash=true): bool - { - if($hash) + if($field instanceof InformationFieldName) { - $password = hash('sha512', $password); - } - elseif(!Cryptography::validateSha512($password)) - { - throw new CryptographyException('Invalid SHA-512 hash provided'); + $field = $field->value; } - return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_PASSWORD_AUTHENTICATION, Utilities::randomCrc32(), [ - 'password' => $password - ]) - )->getResponse()->getResult(); - } - - /** - * Authenticates an OTP code for verification purposes - * - * @param string $code The OTP code to be authenticated. - * @return bool True if the OTP authentication is successful, otherwise false. - * @throws RpcException Thrown if the RPC request fails. - */ - public function verificationOtpAuthentication(string $code): bool - { - return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_OTP_AUTHENTICATION, Utilities::randomCrc32(), [ - 'code' => $code - ]) - )->getResponse()->getResult(); - } - - /** - * Sets a new password for settings with optional hashing. - * - * @param string $password The password to be set. If hashing is enabled, the password will be hashed before being sent. - * @param bool $hash Optional. Determines whether the password should be hashed. Default is true. If false, the input is expected to be hashed using sha512. - * @return true Returns true if the password is successfully set. - * @throws CryptographyException Thrown if the password hash is invalid. - * @throws RpcException Thrown if the RPC request fails. - */ - public function settingsSetPassword(string $password, bool $hash=true): true - { - if($hash) + if($privacy instanceof PrivacyState) { - $password = Cryptography::hashPassword($password); - } - elseif(!Cryptography::validatePasswordHash($password)) - { - throw new CryptographyException('Invalid password hash provided'); + $privacy = $privacy->value; } - return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_SET_PASSWORD, Utilities::randomCrc32(), [ - 'password' => $password - ]) - )->getResponse()->getResult(); - } - - /** - * Deletes the user's password settings by sending a remote procedure call request. - * - * @param string $password The password to be deleted. - * @param bool $hash Indicates whether to hash the password before sending the request. Defaults to true. - * @return true Indicates successful deletion of the password. - * @throws RpcException Thrown if the RPC request fails. - */ - public function settingsDeletePassword(string $password, bool $hash=true): true - { - return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_DELETE_PASSWORD, Utilities::randomCrc32(), [ - 'password' => $hash ? hash('sha512', $password) : $password - ]) - )->getResponse()->getResult(); - } - - /** - * Updates the user's password by sending a remote procedure call request. - * - * @param string $password The new password to be set. - * @param string $existingPassword The current password for authentication. - * @param bool $hash - * @return bool True if the password was successfully updated, false otherwise. - * @throws CryptographyException - * @throws RpcException Thrown if the RPC request fails. - */ - public function settingsUpdatePassword(string $password, string $existingPassword, bool $hash=true): bool - { - if($hash) - { - $password = Cryptography::hashPassword($password); - $existingPassword = hash('sha512', $existingPassword); - } - elseif(!Cryptography::validatePasswordHash($password)) - { - throw new CryptographyException('Invalid password hash provided'); - } - - return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_UPDATE_PASSWORD, Utilities::randomCrc32(), [ - 'password' => $password, - 'existing_password' => $existingPassword - ]) - )->getResponse()->getResult(); - } - - /** - * Updates the OTP setting by sending a remote procedure call request with the provided OTP. - * - * @return string The result of the OTP URI request. - * @throws RpcException Thrown if the RPC request fails. - */ - public function settingsSetOtp(?string $password=null, bool $hash=true): string - { - if($hash && $password !== null) - { - $password = hash('sha512', $password); - } - - return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_SET_OTP, Utilities::randomCrc32(), [ - 'password' => $password - ]) - )->getResponse()->getResult(); - } - - /** - * Deletes the one-time password (OTP) settings by sending a remote procedure call request. - * - * @param string|null $password The password to authenticate the request. If provided, it will be hashed using SHA-512 if $hash is true. - * @param bool $hash Indicates whether to hash the password before sending the request. Defaults to true. - * @return bool True if the OTP settings were successfully deleted, false otherwise. - * @throws CryptographyException Thrown if the password hash is invalid. - * @throws RpcException Thrown if the RPC request fails. - */ - public function settingsDeleteOtp(?string $password=null, bool $hash=true): bool - { - if($hash && $password !== null) - { - $password = hash('sha512', $password); - } - elseif($password !== null && !Cryptography::validateSha512($password)) - { - throw new CryptographyException('Invalid SHA-512 hash provided'); - } - - return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_DELETE_OTP, Utilities::randomCrc32(), [ - 'password' => $password - ]) - )->getResponse()->getResult(); - } - - /** - * Updates the user's OTP settings by sending a remote procedure call request. - * - * @param InformationFieldName $field The field to be updated. - * @param string $value The value to be set. - * @param PrivacyState|null $privacy The privacy state to be set. Default is null. - * @return bool True if the OTP was successfully updated, false otherwise. - * @throws RpcException Thrown if the RPC request fails. - */ - public function settingsAddInformationField(InformationFieldName $field, string $value, ?PrivacyState $privacy=null): true - { - return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_ADD_INFORMATION_FIELD, Utilities::randomCrc32(), [ - 'field' => $field->value, + return $this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_ADD_INFORMATION_FIELD, parameters: [ + 'field' => $field, 'value' => $value, - 'privacy' => $privacy?->value - ]), - )->getResponse()->getResult(); - } - - /** - * Retrieves an information field by sending a remote procedure call request. - * - * @param InformationFieldName $field The field to be retrieved. - * @return InformationField The information field retrieved from the server. - * @throws RpcException Thrown if the RPC request fails. - */ - public function settingsGetInformationField(InformationFieldName $field): InformationField - { - return InformationField::fromArray($this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_GET_INFORMATION_FIELD, Utilities::randomCrc32(), [ - 'field' => $field->value - ]) - )->getResponse()->getResult()); - } - - /** - * Deletes an information field by sending a remote procedure call request. - * - * @param InformationFieldName $field The field to be deleted. - * @return bool True if the field was successfully deleted, false otherwise. - * @throws RpcException Thrown if the RPC request fails. - */ - public function settingsDeleteInformationField(InformationFieldName $field): true - { - return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_DELETE_INFORMATION_FIELD, Utilities::randomCrc32(), [ - 'field' => $field->value + 'privacy' => $privacy ]) )->getResponse()->getResult(); } /** - * Updates an information field by sending a remote procedure call request. + * Adds a new public signature to the peer's profile, returns the UUID of the signature. * - * @param InformationFieldName $field The field to be updated. - * @param string $value The value to be set. - * @return bool True if the field was successfully updated, false otherwise. - * @throws RpcException Thrown if the RPC request fails. - */ - public function settingsUpdateInformationField(InformationFieldName $field, string $value): true - { - return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_UPDATE_INFORMATION_FIELD, Utilities::randomCrc32(), [ - 'field' => $field->value, - 'value' => $value - ]) - )->getResponse()->getResult(); - } - - /** - * Updates the privacy of an information field by sending a remote procedure call request. - * - * @param InformationFieldName $field The field to be updated. - * @param PrivacyState $privacy The privacy state to be set. - * @return bool True if the privacy was successfully updated, false otherwise. - * @throws RpcException Thrown if the RPC request fails. - */ - public function settingsUpdateInformationPrivacy(InformationFieldName $field, PrivacyState $privacy): true - { - return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_UPDATE_INFORMATION_PRIVACY, Utilities::randomCrc32(), [ - 'field' => $field->value, - 'privacy' => $privacy->value - ]) - )->getResponse()->getResult(); - } - - /** - * Adds a signing key to the server associated with the peer by sending a remote procedure call request. - * - * @param string $publicKey The public key to be added. - * @param string|null $name The name of the signing key. - * @param int|null $expires The expiration date of the signing key. - * @return string The UUID of the signing key. - * @throws RpcException Thrown if the RPC request fails. + * @param string $publicKey The public key of the signature + * @param string|null $name Optional. The name of the signature + * @param int|null $expires Optional. The expiration time of the signature + * @return string The UUID of the signature + * @throws RpcException Thrown if there was an error with the RPC request */ public function settingsAddSignature(string $publicKey, ?string $name=null, ?int $expires=null): string { return $this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_ADD_SIGNATURE, Utilities::randomCrc32(), [ + new RpcRequest(StandardMethods::SETTINGS_ADD_SIGNATURE, parameters: [ 'public_key' => $publicKey, 'name' => $name, 'expires' => $expires @@ -609,144 +509,379 @@ } /** - * Creates a signing key pair by generating a new key pair and sending a remote procedure call request to add it. - * The generated key pair is returned, this is similar to settingsAddSignature but generates the key pair. + * Deletes an information field from the peer's profile, returns True if the field was deleted. * - * @param string|null $name The name of the signing key. - * @param int|null $expires The expiration date of the signing key. - * @return string The UUID of the signing key. - * @throws CryptographyException Thrown if the key generation fails. - * @throws RpcException Thrown if the RPC request fails. + * @param InformationFieldName|string $field The name of the field to delete + * @return bool Returns True if the field was deleted + * @throws RpcException Thrown if there was an error with the RPC request */ - public function settingsCreateSignature(?string $name=null, ?int $expires=null): string + public function settingsDeleteInformationField(InformationFieldName|string $field): bool { - $signingKeypair = Cryptography::generateSigningKeyPair(); - $uuid = $this->settingsAddSignature($signingKeypair->getPublicKey(), $name, $expires); - $signatureKeypair = new SignatureKeyPair([ - 'uuid' => $uuid, - 'name' => $name, - 'public_key' => $signingKeypair->getPublicKey(), - 'private_key' => $signingKeypair->getPrivateKey(), - 'expires' => $expires - ]); + if($field instanceof InformationFieldName) + { + $field = $field->value; + } - $this->addSigningKey($signatureKeypair); - return $uuid; - } - - /** - * Retrieves a signing key by sending a remote procedure call request. - * - * @param string $uuid The UUID of the signing key to be retrieved. - * @return SigningKey The signing key retrieved from the server. - * @throws RpcException Thrown if the RPC request fails. - */ - public function settingsGetSigningKey(string $uuid): SigningKey - { - return SigningKey::fromArray($this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_GET_SIGNATURE, Utilities::randomCrc32(), [ - 'uuid' => $uuid + return $this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_DELETE_INFORMATION_FIELD, parameters: [ + 'field' => $field ]) - )->getResponse()->getResult()); + )->getResponse()->getResult(); } /** - * Retrieves the list of signing keys associated with the peer by sending a remote procedure call request. + * Deletes the OTP from the peer's profile, returns True if the OTP was deleted. * - * @return SigningKey[] The list of signing keys retrieved from the server. - * @throws RpcException Thrown if the RPC request fails. + * @param string|null $password Optional. Required if a password is set, this is used to verify the operation + * @param bool $hash Optional. Whether to hash the password + * @return bool Returns True if the OTP was deleted + * @throws RpcException Thrown if there was an error with the RPC request */ - public function settingsGetSigningKeys(): array + public function settingsDeleteOtp(?string $password=null, bool $hash=true): bool { - return array_map(fn($key) => SigningKey::fromArray($key), $this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_GET_SIGNATURES, Utilities::randomCrc32()) - )->getResponse()->getResult()); + if($hash && $password != null) + { + $password = hash('sha512', $password); + } + + return $this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_DELETE_OTP, parameters: [ + 'password' => $password + ]) + )->getResponse()->getResult(); } /** - * Deletes a signing key by sending a remote procedure call request. + * Deletes the password from the peer's profile, returns True if the password was deleted. * - * @param string $uuid The UUID of the signing key to be deleted. - * @return bool True if the signing key was successfully deleted, false otherwise. - * @throws RpcException Thrown if the RPC request fails. + * @return bool Returns True if the password was deleted + * @throws RpcException Thrown if there was an error with the RPC request */ - public function settingsDeleteSigningKey(string $uuid): true + public function settingsDeletePassword(): bool { - return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::SETTINGS_DELETE_SIGNATURE, Utilities::randomCrc32(), [ + return $this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_DELETE_PASSWORD) + )->getResponse()->getResult(); + } + + /** + * Deletes a signature from the peer's profile, returns True if the signature was deleted. + * + * @param string $uuid The UUID of the signature to delete + * @return bool Returns True if the signature was deleted + * @throws RpcException Thrown if there was an error with the RPC request + */ + public function settingsDeleteSignature(string $uuid): bool + { + return $this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_DELETE_SIGNATURE, parameters: [ 'uuid' => $uuid ]) )->getResponse()->getResult(); } /** - * Authenticates the user by sending a remote procedure call request. - * Only applicable for server to server communication, this is the first method to call - * after connecting to the server. + * Retrieves the value of an information field from the peer's profile, returns the value of the field as an + * InformationFieldState object. * - * @return true Returns true if the authentication is successful. - * @throws RpcException Thrown if the RPC request fails. + * @param InformationFieldName|string $field The name of the field to retrieve + * @return InformationFieldState The value of the field as an InformationFieldState object + * @throws RpcException Thrown if there was an error with the RPC request */ - public function authenticate(): true + public function settingsGetInformationField(InformationFieldName|string $field): InformationFieldState { - return (bool)$this->sendRequest( - new RpcRequest(StandardMethods::VERIFICATION_AUTHENTICATE, Utilities::randomCrc32()) - )->getResponse()->getResult(); - } - - /** - * Resolves a peer by its address or a PeerAddress and returns information about the peer. Note that this is a - * decentralized method call, so passing on a peer that does not belong to the host server will result in the - * host server resolving the peer externally on its end. - * - * @param string|PeerAddress $peerAddress The peer address as a string or an instance of PeerAddress. - * @return Peer The resolved peer object. - * @throws RpcException Thrown if the RPC request fails. - */ - public function resolvePeer(string|PeerAddress $peerAddress, null|string|PeerAddress $identifiedAs=null): Peer - { - if($peerAddress instanceof PeerAddress) + if($field instanceof InformationFieldName) { - $peerAddress = $peerAddress->getAddress(); + $field = $field->value; } - if($identifiedAs instanceof PeerAddress) - { - $identifiedAs = $identifiedAs->getAddress(); - } - - return Peer::fromArray($this->sendRequest( - new RpcRequest(StandardMethods::RESOLVE_PEER, Utilities::randomCrc32(), [ - 'peer' => $peerAddress - ]), true, $identifiedAs + return new InformationFieldState($this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_GET_INFORMATION_FIELD, parameters: [ + 'field' => $field + ]) )->getResponse()->getResult()); } /** - * Resolves the signing key of a peer. Note that this is a decentralized method call, so passing on a peer - * that does not belong to the host server will result in the host server resolving the key externally on - * its end. + * Retrieves a list of information fields from the peer's profile, returns an array of InformationFieldState objects. * - * @param string|PeerAddress $peerAddress The peer address as a string or an instance of PeerAddress. - * @param string $signatureUuid The UUID of the signature to resolve. - * @return SigningKey|null The resolved signing key. Null if the resolved key was not found - * @throws RpcException Thrown if the RPC request fails. + * @return InformationFieldState[] An array of InformationFieldState objects + * @throws RpcException Thrown if there was an error with the RPC request */ - public function resolvePeerSignature(string|PeerAddress $peerAddress, string $signatureUuid): ?SigningKey + public function settingsGetInformationFields(): array { - if($peerAddress instanceof PeerAddress) + return array_map(fn($informationFieldState) => new InformationFieldState($informationFieldState), $this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_GET_INFORMATION_FIELDS) + )->getResponse()->getResult()); + } + + /** + * Returns the existing Signature of a signature UUID associated with the peer's profile + * + * @param string $uuid The UUID of the signature to retrieve + * @return Signature The Signature object + * @throws RpcException Thrown if there was an error with the RPC request + */ + public function settingsGetSignature(string $uuid): Signature + { + return new Signature($this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_GET_SIGNATURE, parameters: [ + 'uuid' => $uuid + ]) + )->getResponse()->getResult()); + } + + /** + * Retrieves a list of public signatures from the peer's profile, returns an array of Signature objects. + * + * @return Signature[] An array of Signature objects + * @throws RpcException Thrown if there was an error with the RPC request + */ + public function settingsGetSignatures(): array + { + return array_map(fn($signatures) => new Signature($signatures), $this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_GET_INFORMATION_FIELDS) + )->getResponse()->getResult()); + } + + /** + * Checks if an information field exists in the peer's profile, returns True if the field exists. + * + * @param InformationFieldName|string $field The name of the field to check + * @return bool Returns True if the field exists + * @throws RpcException Thrown if there was an error with the RPC request + */ + public function settingsInformationFieldExists(InformationFieldName|string $field): bool + { + if($field instanceof InformationFieldName) { - $peerAddress = $peerAddress->getAddress(); + $field = $field->value; } - $result = $this->sendRequest( - new RpcRequest(StandardMethods::RESOLVE_PEER_SIGNATURE, Utilities::randomCrc32(), [ - 'peer' => $peerAddress, - 'uuid' => $signatureUuid + return $this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_INFORMATION_FIELD_EXISTS, parameters: [ + 'field' => $field ]) )->getResponse()->getResult(); + } - // Conditional null-return - return $result ? SigningKey::fromArray($result) : null; + /** + * Sets the OTP for the peer's profile, returns True if the OTP was set. + * + * @param string|null $password Optional. If a password is set to the account, this is used to verify the operation + * @param bool $hash Optional. Whether to hash the password + * @return string Returns True if the OTP was set + * @throws RpcException Thrown if there was an error with the RPC request + */ + public function settingsSetOtp(?string $password=null, bool $hash=true): string + { + if($hash && $password != null) + { + $password = hash('sha512', $password); + } + + return $this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_SET_OTP, parameters: [ + 'password' => $password + ]) + )->getResponse()->getResult(); + } + + /** + * Sets the password for the peer's profile, returns True if the password was set. + * + * @param string $password The password to set + * @param bool $hash Optional. Whether to hash the password + * @return bool Returns True if the password was set + * @throws CryptographyException Thrown if there was an error while hashing the password + * @throws RpcException Thrown if there was an error with the RPC request + */ + public function settingsSetPassword(string $password, bool $hash=true): bool + { + if($hash) + { + $password = Cryptography::hashPassword($password); + } + + return $this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_SET_PASSWORD, parameters: [ + 'password' => $password + ]) + )->getResponse()->getResult(); + } + + /** + * Checks if a signature exists in the peer's profile, returns True if the signature exists. + * + * @param string $uuid The UUID of the signature to check for it's existence + * @return bool Returns True if the signature exists, False otherwise + * @throws RpcException Thrown if there was an error with the RPC request + */ + public function settingsSignatureExists(string $uuid): bool + { + return $this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_SIGNATURE_EXISTS, parameters: [ + 'uuid' => $uuid + ]) + )->getResponse()->getResult(); + } + + /** + * Updates the value of an information field in the peer's profile, returns True if the field was updated. + * + * @param InformationFieldName|string $field The name of the field to update + * @param string $value The value to update the field to + * @return bool Returns True if the field was updated + * @throws RpcException Thrown if there was an error with the RPC request + */ + public function settingsUpdateInformationField(InformationFieldName|string $field, string $value): bool + { + if($field instanceof InformationFieldName) + { + $field = $field->value; + } + + return $this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_UPDATE_INFORMATION_FIELD, parameters: [ + 'field' => $field, + 'value' => $value, + ]) + )->getResponse()->getResult(); + } + + /** + * Updates the privacy of an information field in the peer's profile, returns True if the field was updated. + * + * @param InformationFieldName|string $field The name of the field to update + * @param PrivacyState|string $privacy The privacy state to update the field to + * @return bool Returns True if the field was updated + * @throws RpcException Thrown if there was an error with the RPC request + */ + public function settingsUpdateInformationPrivacy(InformationFieldName|string $field, PrivacyState|string $privacy): bool + { + if($field instanceof InformationFieldName) + { + $field = $field->value; + } + + if($privacy instanceof PrivacyState) + { + $privacy = $privacy->value; + } + + return $this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_UPDATE_INFORMATION_PRIVACY, parameters: [ + 'field' => $field, + 'privacy' => $privacy + ]) + )->getResponse()->getResult(); + } + + /** + * Updates the existing password configuration on the account, requires the existing password to verify + * the operation. Returns True upon success + * + * @param string $password The new password to set + * @param string $existingPassword The existing password already tied to the account + * @param bool $hash If True, the password inputs will be hashed, false will be sent as is. + * @return bool Returns True if the password was updated successfully + * @throws CryptographyException Thrown if there was an error while hashing the passwords + * @throws RpcException Thrown if there was an error with the RPC request + */ + public function settingsUpdatePassword(string $password, string $existingPassword, bool $hash=true): bool + { + if($hash) + { + $existingPassword = hash('sha512', $password); + $password = Cryptography::hashPassword($password); + } + + return $this->sendRequest( + new RpcRequest(StandardMethods::SETTINGS_UPDATE_PASSWORD, parameters: [ + 'password' => $password, + 'existing_password' => $existingPassword + ]) + )->getResponse()->getResult(); + } + + /** + * Submits an answer for an image captcha problem, returns True if the answer is correct, False otherwise + * + * @param string $answer The answer for the captcha + * @return bool Returns True if the answer is correct, False otherwise. + * @throws RpcException Thrown if there was an error with the RPC request + */ + public function verificationAnswerImageCaptcha(string $answer): bool + { + return $this->sendRequest( + new RpcRequest(StandardMethods::VERIFICATION_ANSWER_IMAGE_CAPTCHA, parameters: [ + 'answer' => $answer + ]) + )->getResponse()->getResult(); + } + + /** + * Authenticates the verification process, returns True if the authentication is successful. + * This method is usually used for server-to-server communication. + * + * @return bool Returns True if the authentication is successful + * @throws RpcException Thrown if there was an error with the RPC request + */ + public function verificationAuthenticate(): bool + { + return $this->sendRequest( + new RpcRequest(StandardMethods::VERIFICATION_AUTHENTICATE) + )->getResponse()->getResult(); + } + + /** + * Retrieves an image captcha problem, returns the problem as an ImageCaptchaVerification object. + * + * @return ImageCaptchaVerification The problem as an ImageCaptchaVerification object + * @throws RpcException Thrown if there was an error with the RPC request + */ + public function verificationGetImageCaptcha(): ImageCaptchaVerification + { + return new ImageCaptchaVerification($this->sendRequest( + new RpcRequest(StandardMethods::VERIFICATION_GET_IMAGE_CAPTCHA) + )->getResponse()->getResult()); + } + + /** + * Verifies a one-time password (OTP) authentication code, returns True if the code is correct, False otherwise. + * + * @param int $code The OTP code to verify + * @return bool Returns True if the code is correct, False otherwise + * @throws RpcException Thrown if there was an error with the RPC request + */ + public function verificationOtpAuthentication(int $code): bool + { + return $this->sendRequest( + new RpcRequest(StandardMethods::VERIFICATION_OTP_AUTHENTICATION, parameters: [ + 'code' => $code + ]) + )->getResponse()->getResult(); + } + + /** + * Verifies a password authentication, returns True if the password is correct, False otherwise. + * + * @param string $password The password to verify + * @param bool $hash Optional. Whether to hash the password + * @return bool Returns True if the password is correct, False otherwise + * @throws RpcException Thrown if there was an error with the RPC request + */ + public function verificationPasswordAuthentication(string $password, bool $hash=true): bool + { + if($hash) + { + $password = hash('sha512', $password); + } + + return $this->sendRequest( + new RpcRequest(StandardMethods::VERIFICATION_PASSWORD_AUTHENTICATION, parameters: [ + 'password' => $password + ]) + )->getResponse()->getResult(); } } \ No newline at end of file From d82e7042a6faa6291e4e0c102b9cb27710ab5833 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 11 Feb 2025 14:50:57 -0500 Subject: [PATCH 364/420] Added client method createSignature --- src/Socialbox/SocialClient.php | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/Socialbox/SocialClient.php b/src/Socialbox/SocialClient.php index 2d19b77..e05b80c 100644 --- a/src/Socialbox/SocialClient.php +++ b/src/Socialbox/SocialClient.php @@ -16,6 +16,7 @@ use Socialbox\Exceptions\ResolutionException; use Socialbox\Exceptions\RpcException; use Socialbox\Objects\Client\ExportedSession; + use Socialbox\Objects\Client\SignatureKeyPair; use Socialbox\Objects\PeerAddress; use Socialbox\Objects\RpcRequest; use Socialbox\Objects\Standard\Contact; @@ -44,6 +45,35 @@ parent::__construct($identifiedAs, $server, $exportedSession); } + /** + * Create a new signing keypair, sends it to the server and saves it locally with the session so that the client + * can use the SigningKey pair for signing and verifying signatures in the future. + * + * This is not a server-side operation, the server only stores the public key and associates it with the peer's + * profile. The private key is stored locally and is never sent to the server. + * + * @param string|null $name Optional. The name of the signature + * @param int|null $expires Optional. The Unix timestamp of the expiration time + * @return string The UUID of the signature + * @throws CryptographyException Thrown if there was an error while generating the signing key pair + * @throws RpcException Thrown if there was an error with the RPC request + */ + public function createSignature(?string $name=null, ?int $expires=null): string + { + $signature = Cryptography::generateSigningKeyPair(); + $uuid = $this->settingsAddSignature($signature->getPublicKey(), $name, $expires); + + $this->addSigningKey(new SignatureKeyPair([ + 'uuid' => $uuid, + 'name' => $name, + 'public_key' => $signature->getPublicKey(), + 'private_key' => $signature->getPrivateKey(), + 'expires' => $expires + ])); + + return $uuid; + } + /** * Adds a new peer to the AddressBook, returns True upon success or False if the contact already exists in * the address book. From 6001e85160eba76019239416296b303a23e9643a Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 19 Feb 2025 14:50:31 -0500 Subject: [PATCH 365/420] Improved signature verification procedure to be simplified for server-side verification purposes. --- src/Socialbox/Socialbox.php | 93 ++++++++----------------------------- 1 file changed, 19 insertions(+), 74 deletions(-) diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index 4c5cee8..2ab0ada 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -720,7 +720,7 @@ try { $client = new SocialClient(self::getServerAddress(), $domain); - $client->authenticate(); + $client->verificationAuthenticate(); } catch (Exception $e) { @@ -758,49 +758,25 @@ * * @param PeerAddress|string $signingPeer The peer address or string identifier of the signing peer * @param string $signatureUuid The UUID of the signature key to be resolved - * @param string $signatureKey The public key of the signature that was used to sign the message * @param string $signature The signature to be verified * @param string $messageHash The SHA-512 hash of the message that was signed * @param int $signatureTime The time at which the message was signed * @return SignatureVerificationStatus The status of the signature verification */ - public static function verifyTimedSignature(PeerAddress|string $signingPeer, string $signatureUuid, string $signatureKey, string $signature, string $messageHash, int $signatureTime): SignatureVerificationStatus + public static function verifyTimedSignature(PeerAddress|string $signingPeer, string $signatureUuid, string $signature, string $messageHash, int $signatureTime): SignatureVerificationStatus { - try - { - if (!Cryptography::verifyTimedMessage($messageHash, $signature, $signatureKey, $signatureTime, false)) - { - return SignatureVerificationStatus::INVALID; - } - } - catch (CryptographyException) - { - return SignatureVerificationStatus::INVALID; - } - // Resolve the peer signature key try { $signingKey = self::resolvePeerSignature($signingPeer, $signatureUuid); + if($signingKey === null) + { + return SignatureVerificationStatus::NOT_FOUND; + } } catch(StandardRpcException) { - return SignatureVerificationStatus::UNVERIFIED; - } - - if($signingKey === null) - { - return SignatureVerificationStatus::UNVERIFIED; - } - - if($signingKey->getPublicKey() !== $signatureKey) - { - return SignatureVerificationStatus::PUBLIC_KEY_MISMATCH; - } - - if($signingKey->getUuid() !== $signatureUuid) - { - return SignatureVerificationStatus::UUID_MISMATCH; + return SignatureVerificationStatus::RESOLUTION_ERROR; } if(time() > $signingKey->getExpires()) @@ -818,7 +794,7 @@ } catch (CryptographyException) { - return SignatureVerificationStatus::INVALID; + return SignatureVerificationStatus::ERROR; } return SignatureVerificationStatus::VERIFIED; @@ -832,66 +808,36 @@ * * @param PeerAddress|string $signingPeer The peer address or string identifier of the signing peer * @param string $signatureUuid The UUID of the signature key to be resolved - * @param string $signatureKey The public key of the signature that was used to sign the message - * @param string $signature The signature to be verified + * @param string $signature The signature to be verified * @param string $messageHash The SHA-512 hash of the message that was signed * @return SignatureVerificationStatus The status of the signature verification */ - public static function verifySignature(PeerAddress|string $signingPeer, string $signatureUuid, string $signatureKey, string $signature, string $messageHash): SignatureVerificationStatus + public static function verifySignature(PeerAddress|string $signingPeer, string $signatureUuid, string $signature, string $messageHash): SignatureVerificationStatus { - try - { - if (!Cryptography::verifyMessage($messageHash, $signature, $signatureKey, false)) - { - return SignatureVerificationStatus::INVALID; - } - } - catch (CryptographyException) - { - return SignatureVerificationStatus::INVALID; - } - - // Resolve the peer signature key try { $signingKey = self::resolvePeerSignature($signingPeer, $signatureUuid); + if($signingKey === null) + { + return SignatureVerificationStatus::NOT_FOUND; + } } catch(StandardRpcException) { - return SignatureVerificationStatus::UNVERIFIED; - } - - if($signingKey === null) - { - return SignatureVerificationStatus::UNVERIFIED; - } - - if($signingKey->getPublicKey() !== $signatureKey) - { - return SignatureVerificationStatus::PUBLIC_KEY_MISMATCH; - } - - if($signingKey->getUuid() !== $signatureUuid) - { - return SignatureVerificationStatus::UUID_MISMATCH; - } - - if(time() > $signingKey->getExpires()) - { - return SignatureVerificationStatus::EXPIRED; + return SignatureVerificationStatus::RESOLUTION_ERROR; } // Verify the signature with the resolved key try { - if (!Cryptography::verifyTimedMessage($messageHash, $signature, $signingKey->getPublicKey(), false)) + if (!Cryptography::verifyMessage($messageHash, $signature, $signingKey->getPublicKey(), false)) { return SignatureVerificationStatus::INVALID; } } catch (CryptographyException) { - return SignatureVerificationStatus::INVALID; + return SignatureVerificationStatus::ERROR; } return SignatureVerificationStatus::VERIFIED; @@ -1142,7 +1088,6 @@ try { $peer = RegisteredPeerManager::getPeerByAddress($peerAddress); - if($peer === null) { throw new StandardRpcException('The requested peer was not found', StandardError::PEER_NOT_FOUND); @@ -1160,7 +1105,7 @@ } catch (DatabaseOperationException $e) { - throw new StandardRpcException('Failed to resolve peer information: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to resolve local peer information', StandardError::INTERNAL_SERVER_ERROR, $e); } // If there's an identifier, we can resolve more information fields if the target peer has added the caller @@ -1194,7 +1139,7 @@ } catch (DatabaseOperationException $e) { - throw new StandardRpcException('Failed to resolve peer information: ' . $e->getMessage(), StandardError::INTERNAL_SERVER_ERROR, $e); + throw new StandardRpcException('Failed to resolve local peer information', StandardError::INTERNAL_SERVER_ERROR, $e); } } } From 0a773101d2a8137b232f3232d385e94176d458f7 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 19 Feb 2025 14:51:13 -0500 Subject: [PATCH 366/420] Reordered the logger statement in the case where the rest of the execution fails for whatever reason --- src/Socialbox/Socialbox.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index 2ab0ada..77a9a1a 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -676,6 +676,11 @@ */ private static function returnError(int $responseCode, StandardError $standardError, ?string $message=null, ?Throwable $e=null): void { + if($e !== null) + { + Logger::getLogger()->error($message, $e); + } + if($message === null) { $message = $standardError->getMessage(); @@ -690,11 +695,6 @@ { print(PHP_EOL . PHP_EOL . Utilities::throwableToString($e)); } - - if($e !== null) - { - Logger::getLogger()->error($message, $e); - } } /** From 2f59c9c16c0445d7430ae7c6a200c7aaf7bd8fdb Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 19 Feb 2025 14:51:18 -0500 Subject: [PATCH 367/420] Reordered the logger statement in the case where the rest of the execution fails for whatever reason --- src/Socialbox/SocialClient.php | 119 ++++++++++++++++++++++++++++++++- 1 file changed, 118 insertions(+), 1 deletion(-) diff --git a/src/Socialbox/SocialClient.php b/src/Socialbox/SocialClient.php index e05b80c..fbce246 100644 --- a/src/Socialbox/SocialClient.php +++ b/src/Socialbox/SocialClient.php @@ -4,6 +4,7 @@ namespace Socialbox; + use InvalidArgumentException; use Socialbox\Classes\Cryptography; use Socialbox\Classes\RpcClient; use Socialbox\Enums\PrivacyState; @@ -15,6 +16,7 @@ use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\ResolutionException; use Socialbox\Exceptions\RpcException; + use Socialbox\Objects\Client\EncryptionChannelSecret; use Socialbox\Objects\Client\ExportedSession; use Socialbox\Objects\Client\SignatureKeyPair; use Socialbox\Objects\PeerAddress; @@ -58,7 +60,7 @@ * @throws CryptographyException Thrown if there was an error while generating the signing key pair * @throws RpcException Thrown if there was an error with the RPC request */ - public function createSignature(?string $name=null, ?int $expires=null): string + public function newSignature(?string $name=null, ?int $expires=null): string { $signature = Cryptography::generateSigningKeyPair(); $uuid = $this->settingsAddSignature($signature->getPublicKey(), $name, $expires); @@ -74,6 +76,53 @@ return $uuid; } + /** + * Creates a new encryption channel with the given peer, using the signature UUID to identify the calling peer. + * This method is a wrapper around the encryptionCreateChannel method, and is used to simplify the process of + * creating a new encryption channel. The signature UUID is used to identify the calling peer, and the public key + * of the signature is used as the encryption key. + * + * @param PeerAddress|string $receivingPeer + * @param string $signatureUuid + * @param string $transportEncryptionAlgorithm + * @return string + * @throws CryptographyException + * @throws RpcException + */ + public function newEncryptionChannel(PeerAddress|string $receivingPeer, string $signatureUuid, string $transportEncryptionAlgorithm): string + { + if(!$this->signingKeyExists($signatureUuid)) + { + throw new InvalidArgumentException('The signature UUID does not exist in the client'); + } + + $signature = $this->getSigningKey($signatureUuid); + if($signature === null) + { + throw new InvalidArgumentException('The signature UUID does not exist in the client'); + } + + $encryptionKeypair = Cryptography::generateEncryptionKeyPair(); + $channelUuid = $this->encryptionCreateChannel( + receivingPeer: $receivingPeer, + signatureUUid: $signature->getUuid(), + encryptionPublicKey: $encryptionKeypair->getPublicKey(), + transportEncryptionAlgorithm: $transportEncryptionAlgorithm + ); + + $this->addEncryptionChannelSecret(new EncryptionChannelSecret([ + 'uuid' => $channelUuid, + 'receiver' => $receivingPeer->getAddress(), + 'signature_uuid' => $signature->getUuid(), + 'public_encryption_key' => $encryptionKeypair->getPublicKey(), + 'private_encryption_key' => $encryptionKeypair->getPrivateKey(), + 'transport_encryption_algorithm' => $transportEncryptionAlgorithm, + 'transport_encryption_key' => null + ])); + + return $channelUuid; + } + /** * Adds a new peer to the AddressBook, returns True upon success or False if the contact already exists in * the address book. @@ -410,6 +459,74 @@ )->getResponse()->getResult()) ?? SignatureVerificationStatus::INVALID; } + public function encryptionAcceptChannel(string $channelUuid, string $signatureUuid, string $encryptionPublicKey, string $encryptedTransportEncryptionKey, null|PeerAddress|string $identifiedAs=null): true + { + if($identifiedAs instanceof PeerAddress) + { + $identifiedAs = $identifiedAs->getAddress(); + } + + return $this->sendRequest( + new RpcRequest(StandardMethods::ENCRYPTION_ACCEPT_CHANNEL, parameters: [ + 'channel_uuid' => $channelUuid, + 'signature_uuid' => $signatureUuid, + 'encryption_public_key' => $encryptionPublicKey, + 'encrypted_transport_encryption_key' => $encryptedTransportEncryptionKey + ]), + identifiedAs: $identifiedAs + )->getResponse()->getResult(); + } + + public function encryptionCloseChannel(string $channelUuid, null|PeerAddress|string $identifiedAs=null): true + { + if($identifiedAs instanceof PeerAddress) + { + $identifiedAs = $identifiedAs->getAddress(); + } + + return $this->sendRequest( + new RpcRequest(StandardMethods::ENCRYPTION_CLOSE_CHANNEL, parameters: [ + 'channel_uuid' => $channelUuid, + ]), + identifiedAs: $identifiedAs + )->getResponse()->getResult(); + } + + /** + * Accepts an encryption channel request, returns True if the channel was accepted. + * + * @param PeerAddress|string $receivingPeer The address of the receiving peer that the channel is being requested to + * @param string $signatureUUid The UUID of the calling signature + * @param string $encryptionPublicKey The public key of the calling encryption key + * @param string $transportEncryptionAlgorithm The transport encryption algorithm to use + * @param string|null $identifyAs Optional. The address of the peer to identify as + * @param string|null $channelUuid Optional. If calling to an external server, the server must provide the other server the UUID to use + * @return string Returns True if the channel was accepted + * @throws RpcException Thrown if there was an error with the RPC request + */ + public function encryptionCreateChannel( + PeerAddress|string $receivingPeer, string $signatureUUid, + string $encryptionPublicKey, string $transportEncryptionAlgorithm='xchacha20', + ?string $identifyAs=null, ?string $channelUuid=null + ): string + { + if($receivingPeer instanceof PeerAddress) + { + $receivingPeer = $receivingPeer->getAddress(); + } + + return $this->sendRequest( + new RpcRequest(StandardMethods::ENCRYPTION_CREATE_CHANNEL, parameters: [ + 'receiving_peer' => $receivingPeer, + 'signature_uuid' => $signatureUUid, + 'encryption_public_key' => $encryptionPublicKey, + 'transport_encryption_algorithm' => $transportEncryptionAlgorithm, + 'channel_uuid' => $channelUuid + ]), + identifiedAs: $identifyAs + )->getResponse()->getResult(); + } + /** * Accepts the community guidelines, returns True if the guidelines were accepted. * From 1f9890bba0ddd146a592c25fac4a82bb39c611d5 Mon Sep 17 00:00:00 2001 From: netkas Date: Wed, 19 Feb 2025 14:57:47 -0500 Subject: [PATCH 368/420] Updated standard error code --- .../ServerDocuments/AcceptTermsOfService.php | 2 +- src/Socialbox/Managers/SessionManager.php | 25 +++++++++++-------- src/Socialbox/Socialbox.php | 13 +++++++++- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/ServerDocuments/AcceptTermsOfService.php b/src/Socialbox/Classes/StandardMethods/ServerDocuments/AcceptTermsOfService.php index 3c11fa1..7605f7f 100644 --- a/src/Socialbox/Classes/StandardMethods/ServerDocuments/AcceptTermsOfService.php +++ b/src/Socialbox/Classes/StandardMethods/ServerDocuments/AcceptTermsOfService.php @@ -32,7 +32,7 @@ if(!$session->flagExists(SessionFlags::VER_TERMS_OF_SERVICE)) { - return $rpcRequest->produceError(StandardError::FORBIDDEN, 'Terms of service has already been accepted'); + return $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, 'Terms of service has already been accepted'); } try diff --git a/src/Socialbox/Managers/SessionManager.php b/src/Socialbox/Managers/SessionManager.php index 7d2c980..3cd7d4d 100644 --- a/src/Socialbox/Managers/SessionManager.php +++ b/src/Socialbox/Managers/SessionManager.php @@ -179,11 +179,10 @@ * Retrieves a session record by its unique identifier. * * @param string $uuid The unique identifier of the session. - * @return SessionRecord The session record corresponding to the given UUID. + * @return SessionRecord|null The session record corresponding to the given UUID. * @throws DatabaseOperationException If the session record cannot be found or if there is an error during retrieval. - * @throws StandardRpcException */ - public static function getSession(string $uuid): SessionRecord + public static function getSession(string $uuid): ?SessionRecord { Logger::getLogger()->verbose(sprintf("Retrieving session %s from the database", $uuid)); @@ -196,7 +195,7 @@ if ($data === false) { - throw new StandardRpcException(sprintf("The requested session '%s' does not exist", $uuid), StandardError::SESSION_NOT_FOUND); + return null; } // Convert the timestamp fields to DateTime objects @@ -308,7 +307,6 @@ * * @param string $uuid The UUID of the session to retrieve flags for. * @return SessionFlags[] An array of flags associated with the specified session. - * @throws StandardRpcException If the specified session does not exist. * @throws DatabaseOperationException If there */ private static function getFlags(string $uuid): array @@ -324,7 +322,7 @@ if ($data === false) { - throw new StandardRpcException(sprintf("The requested session '%s' does not exist", $uuid), StandardError::SESSION_NOT_FOUND); + throw new DatabaseOperationException(sprintf("The session '%s' does not exist", $uuid)); } return SessionFlags::fromString($data['flags']); @@ -372,7 +370,7 @@ * @param string $uuid The UUID of the session from which the flags will be removed. * @param SessionFlags[] $flags An array of flags to be removed from the session. * @return void - * @throws DatabaseOperationException|StandardRpcException If there is an error while updating the session in the database. + * @throws DatabaseOperationException If there is an error while updating the session in the database. */ public static function removeFlags(string $uuid, array $flags): void { @@ -429,7 +427,6 @@ * @param array $flagsToRemove An array of flags to remove from the session if it is marked as complete. * @return void * @throws DatabaseOperationException If there is an error while updating the session in the database. - * @throws StandardRpcException If the session record cannot be found or if there is an error during retrieval. */ public static function updateFlow(SessionRecord $session, array $flagsToRemove=[]): void { @@ -447,13 +444,19 @@ // Remove & update the session flags self::removeFlags($session->getUuid(), $flagsToRemove); - $session = self::getSession($session->getUuid()); + $sessionUuid = $session->getUuid(); + $session = self::getSession($sessionUuid); + + if($session === null) + { + throw new DatabaseOperationException(sprintf('The session %s was not found', $sessionUuid)); + } // Check if all registration/authentication requirements are met if(SessionFlags::isComplete($session->getFlags())) { - SessionManager::removeFlags($session->getUuid(), [SessionFlags::REGISTRATION_REQUIRED, SessionFlags::AUTHENTICATION_REQUIRED]); // Remove the registration/authentication flags - SessionManager::setAuthenticated($session->getUuid(), true); // Mark the session as authenticated + SessionManager::removeFlags($sessionUuid, [SessionFlags::REGISTRATION_REQUIRED, SessionFlags::AUTHENTICATION_REQUIRED]); // Remove the registration/authentication flags + SessionManager::setAuthenticated($sessionUuid, true); // Mark the session as authenticated RegisteredPeerManager::enablePeer($session->getPeerUuid()); // Enable the peer } } diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index 77a9a1a..c70bc17 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -523,14 +523,25 @@ // If the client has provided an identification header, further validation and resolution is required if($clientRequest->getIdentifyAs() !== null) { + try + { + $peer = $clientRequest->getPeer(); + } + catch (DatabaseOperationException $e) + { + self::returnError(500, StandardError::INTERNAL_SERVER_ERROR, 'Failed to resolve host peer', $e); + } + // First check if the client is identifying as the host - if($clientRequest->getPeer()->getAddress() !== ReservedUsernames::HOST->value) + if($peer->getAddress() !== ReservedUsernames::HOST->value) { // TODO: Maybe allow user client to change identification but within an RPC method rather than the headers self::returnError(403, StandardError::FORBIDDEN, 'Unauthorized: Not allowed to identify as a different peer'); return; } + if($clientRequest->getIdentifyAs()->getDomain() != $) + // Synchronize the peer try { From 29a3d42538cc9d84725882ac12fa0c223a875990 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 20 Feb 2025 00:34:07 -0500 Subject: [PATCH 369/420] Major changes, revamp required --- src/Socialbox/Classes/Configuration.php | 4 + .../Configuration/PoliciesConfiguration.php | 48 +++ src/Socialbox/Classes/RpcClient.php | 83 ++++- ...PeerSignature.php => ResolveSignature.php} | 34 +- ...yPeerSignature.php => VerifySignature.php} | 17 +- .../Encryption/EncryptionAcceptChannel.php | 31 -- .../Encryption/EncryptionCreateChannel.php | 317 ------------------ src/Socialbox/Enums/StandardMethods.php | 30 +- .../Status/SignatureVerificationStatus.php | 18 +- .../Managers/EncryptionChannelManager.php | 54 ++- .../Managers/RegisteredPeerManager.php | 10 +- .../Client/EncryptionChannelSecret.php | 129 +++++++ .../Objects/Client/ExportedSession.php | 62 +++- src/Socialbox/Objects/ClientRequest.php | 32 +- .../Objects/Database/ChannelMessageRecord.php | 1 - .../Database/EncryptionChannelRecord.php | 14 - src/Socialbox/Objects/PeerAddress.php | 2 +- src/Socialbox/Objects/RpcRequest.php | 20 ++ src/Socialbox/SocialClient.php | 115 ------- src/Socialbox/Socialbox.php | 164 ++++----- 20 files changed, 523 insertions(+), 662 deletions(-) rename src/Socialbox/Classes/StandardMethods/Core/{ResolvePeerSignature.php => ResolveSignature.php} (59%) rename src/Socialbox/Classes/StandardMethods/Core/{VerifyPeerSignature.php => VerifySignature.php} (81%) delete mode 100644 src/Socialbox/Classes/StandardMethods/Encryption/EncryptionAcceptChannel.php delete mode 100644 src/Socialbox/Classes/StandardMethods/Encryption/EncryptionCreateChannel.php create mode 100644 src/Socialbox/Objects/Client/EncryptionChannelSecret.php diff --git a/src/Socialbox/Classes/Configuration.php b/src/Socialbox/Classes/Configuration.php index af43b69..411a8dc 100644 --- a/src/Socialbox/Classes/Configuration.php +++ b/src/Socialbox/Classes/Configuration.php @@ -166,6 +166,10 @@ // value that exceeds this limit, the server will use this limit instead. // recommendation: 100 $config->setDefault('policies.get_contacts_limit', 100); + $config->setDefault('policies.get_encryption_channel_requests_limit', 100); + $config->setDefault('policies.get_encryption_channels_limit', 100); + $config->setDefault('policies.get_encryption_channel_incoming_limit', 100); + $config->setDefault('policies.get_encryption_channel_outgoing_limit', 100); // Default privacy states for information fields associated with the peer $config->setDefault('policies.default_display_picture_privacy', 'PUBLIC'); diff --git a/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php b/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php index 329b92a..bd17e7f 100644 --- a/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php +++ b/src/Socialbox/Classes/Configuration/PoliciesConfiguration.php @@ -12,6 +12,10 @@ private int $imageCaptchaExpires; private int $peerSyncInterval; private int $getContactsLimit; + private int $getEncryptionChannelRequestsLimit; + private int $getEncryptionChannelsLimit; + private int $getEncryptionChannelIncomingLimit; + private int $getEncryptionChannelOutgoingLimit; private PrivacyState $defaultDisplayPicturePrivacy; private PrivacyState $defaultFirstNamePrivacy; private PrivacyState $defaultMiddleNamePrivacy; @@ -43,6 +47,10 @@ $this->imageCaptchaExpires = $data['image_captcha_expires']; $this->peerSyncInterval = $data['peer_sync_interval']; $this->getContactsLimit = $data['get_contacts_limit']; + $this->getEncryptionChannelRequestsLimit = $data['get_encryption_channel_requests_limit']; + $this->getEncryptionChannelsLimit = $data['get_encryption_channels_limit']; + $this->getEncryptionChannelIncomingLimit = $data['get_encryption_channel_incoming_limit']; + $this->getEncryptionChannelOutgoingLimit = $data['get_encryption_channel_outgoing_limit']; $this->defaultDisplayPicturePrivacy = PrivacyState::tryFrom($data['default_display_picture_privacy']) ?? PrivacyState::PRIVATE; $this->defaultFirstNamePrivacy = PrivacyState::tryFrom($data['default_first_name_privacy']) ?? PrivacyState::PRIVATE; $this->defaultMiddleNamePrivacy = PrivacyState::tryFrom($data['default_middle_name_privacy']) ?? PrivacyState::PRIVATE; @@ -110,6 +118,46 @@ return $this->getContactsLimit; } + /** + * Returns the maximum number of encryption channel requests that can be retrieved in a single request + * + * @return int + */ + public function getEncryptionChannelRequestsLimit(): int + { + return $this->getEncryptionChannelRequestsLimit; + } + + /** + * Returns the maximum number of encryption channels that can be retrieved in a single request + * + * @return int + */ + public function getEncryptionChannelsLimit(): int + { + return $this->getEncryptionChannelsLimit; + } + + /** + * Returns the maximum number of incoming encryption channels that can be retrieved in a single request + * + * @return int + */ + public function getEncryptionChannelIncomingLimit(): int + { + return $this->getEncryptionChannelIncomingLimit; + } + + /** + * Returns the maximum number of outgoing encryption channels that can be retrieved in a single request + * + * @return int + */ + public function getEncryptionChannelOutgoingLimit(): int + { + return $this->getEncryptionChannelOutgoingLimit; + } + /** * Returns the default privacy state for the display picture * diff --git a/src/Socialbox/Classes/RpcClient.php b/src/Socialbox/Classes/RpcClient.php index e1ad9e1..92d117c 100644 --- a/src/Socialbox/Classes/RpcClient.php +++ b/src/Socialbox/Classes/RpcClient.php @@ -10,6 +10,7 @@ use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\ResolutionException; use Socialbox\Exceptions\RpcException; + use Socialbox\Objects\Client\EncryptionChannelSecret; use Socialbox\Objects\Client\ExportedSession; use Socialbox\Objects\Client\SignatureKeyPair; use Socialbox\Objects\KeyPair; @@ -39,6 +40,7 @@ private string $sessionUuid; private ?string $defaultSigningKey; private array $signingKeys; + private array $encryptionChannelSecrets; /** * Constructs a new instance with the specified peer address. @@ -78,6 +80,7 @@ $this->serverTransportEncryptionKey = $exportedSession->getServerTransportEncryptionKey(); $this->signingKeys = $exportedSession->getSigningKeys(); $this->defaultSigningKey = $exportedSession->getDefaultSigningKey(); + $this->encryptionChannelSecrets = $exportedSession->getEncryptionChannelSecrets(); // Still solve the server information $this->serverInformation = self::getServerInformation(); @@ -107,6 +110,7 @@ // Set the initial properties $this->signingKeys = []; + $this->encryptionChannelSecrets = []; $this->defaultSigningKey = null; $this->identifiedAs = $identifiedAs; $this->remoteServer = $server ?? $identifiedAs->getDomain(); @@ -771,6 +775,17 @@ return $this->signingKeys[$uuid] ?? null; } + /** + * Deletes a signing key from the current instance. + * + * @param string $uuid The UUID of the signing key to be deleted. + * @return void + */ + public function deleteSigningKey(string $uuid): void + { + unset($this->signingKeys[$uuid]); + } + /** * Retrieves the default signing key associated with the current instance. * @@ -797,6 +812,71 @@ $this->defaultSigningKey = $uuid; } + /** + * Retrieves the encryption channel keys associated with the current instance. + * + * @return EncryptionChannelSecret[] The encryption channel keys. + */ + public function getEncryptionChannelSecrets(): array + { + return $this->encryptionChannelSecrets; + } + + /** + * Adds a new encryption channel key to the current instance. + * + * @param EncryptionChannelSecret $key The encryption channel key to be added. + * @return void + */ + public function addEncryptionChannelSecret(EncryptionChannelSecret $key): void + { + $this->encryptionChannelSecrets[$key->getChannelUuid()] = $key; + } + + /** + * Removes an encryption channel key from the current instance. + * + * @param string $uuid The UUID of the encryption channel key to be removed. + * @return void + */ + public function removeEncryptionChannelKey(string $uuid): void + { + unset($this->encryptionChannelSecrets[$uuid]); + } + + /** + * Retrieves the encryption channel key associated with the specified UUID. + * + * @param string $uuid The UUID of the encryption channel key to be retrieved. + * @return EncryptionChannelSecret|null The encryption channel key associated with the UUID, or null if not found. + */ + public function getEncryptionChannelKey(string $uuid): ?EncryptionChannelSecret + { + return $this->encryptionChannelSecrets[$uuid] ?? null; + } + + /** + * Checks if an encryption channel key exists with the specified UUID. + * + * @param string $uuid The UUID of the encryption channel key to check. + * @return bool True if the encryption channel key exists, false otherwise. + */ + public function encryptionChannelKeyExists(string $uuid): bool + { + return isset($this->encryptionChannelSecrets[$uuid]); + } + + /** + * Deletes an encryption channel key from the current instance. + * + * @param string $uuid The UUID of the encryption channel key to be deleted. + * @return void + */ + public function deleteEncryptionChannelKey(string $uuid): void + { + unset($this->encryptionChannelSecrets[$uuid]); + } + /** * Exports the current session details into an ExportedSession object. * @@ -821,7 +901,8 @@ 'client_transport_encryption_key' => $this->clientTransportEncryptionKey, 'server_transport_encryption_key' => $this->serverTransportEncryptionKey, 'default_signing_key' => $this->defaultSigningKey, - 'signing_keys' => array_map(fn(SignatureKeyPair $key) => $key->toArray(), $this->signingKeys) + 'signing_keys' => array_map(fn(SignatureKeyPair $key) => $key->toArray(), $this->signingKeys), + 'encryption_channel_secrets' => array_map(fn(EncryptionChannelSecret $key) => $key->toArray(), $this->encryptionChannelSecrets) ]); } } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/Core/ResolvePeerSignature.php b/src/Socialbox/Classes/StandardMethods/Core/ResolveSignature.php similarity index 59% rename from src/Socialbox/Classes/StandardMethods/Core/ResolvePeerSignature.php rename to src/Socialbox/Classes/StandardMethods/Core/ResolveSignature.php index d9248f4..002d264 100644 --- a/src/Socialbox/Classes/StandardMethods/Core/ResolvePeerSignature.php +++ b/src/Socialbox/Classes/StandardMethods/Core/ResolveSignature.php @@ -5,6 +5,7 @@ use Exception; use InvalidArgumentException; use Socialbox\Abstracts\Method; + use Socialbox\Classes\Validator; use Socialbox\Enums\StandardError; use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; use Socialbox\Exceptions\Standard\MissingRpcArgumentException; @@ -16,7 +17,7 @@ use Socialbox\Socialbox; use Symfony\Component\Uid\Uuid; - class ResolvePeerSignature extends Method + class ResolveSignature extends Method { /** @@ -30,30 +31,17 @@ throw new MissingRpcArgumentException('peer'); } - if(!$rpcRequest->containsParameter('uuid')) + if(!$rpcRequest->containsParameter('signature_uuid')) { - throw new MissingRpcArgumentException('uuid'); + throw new MissingRpcArgumentException('signature_uuid'); + } + elseif(!Validator::validateUuid($rpcRequest->getParameter('signature_uuid'))) + { + throw new InvalidRpcArgumentException('signature_uuid', 'Invalid UUID V4'); } - try - { - $uuid = Uuid::fromString($rpcRequest->getParameter('uuid')); - } - catch(InvalidArgumentException $e) - { - throw new InvalidRpcArgumentException('uuid', $e); - } - - // Parse the peer address - try - { - $peerAddress = PeerAddress::fromAddress($rpcRequest->getParameter('peer')); - } - catch(InvalidArgumentException $e) - { - throw new InvalidRpcArgumentException('peer', $e); - } - - return $rpcRequest->produceResponse(Socialbox::resolvePeerSignature($peerAddress, $uuid->toRfc4122())); + return $rpcRequest->produceResponse(Socialbox::resolvePeerSignature( + $rpcRequest->getParameter('peer'), $rpcRequest->getParameter('signature_uuid') + )); } } \ No newline at end of file diff --git a/src/Socialbox/Classes/StandardMethods/Core/VerifyPeerSignature.php b/src/Socialbox/Classes/StandardMethods/Core/VerifySignature.php similarity index 81% rename from src/Socialbox/Classes/StandardMethods/Core/VerifyPeerSignature.php rename to src/Socialbox/Classes/StandardMethods/Core/VerifySignature.php index d78c79b..f8e2633 100644 --- a/src/Socialbox/Classes/StandardMethods/Core/VerifyPeerSignature.php +++ b/src/Socialbox/Classes/StandardMethods/Core/VerifySignature.php @@ -14,7 +14,7 @@ use Socialbox\Objects\RpcRequest; use Socialbox\Socialbox; - class VerifyPeerSignature extends Method + class VerifySignature extends Method { /** @@ -37,11 +37,6 @@ throw new InvalidRpcArgumentException('signature_uuid', 'Invalid UUID V4'); } - if(!$rpcRequest->containsParameter('signature_public_key')) - { - throw new MissingRpcArgumentException('signature_public_key'); - } - if(!$rpcRequest->containsParameter('signature')) { throw new MissingRpcArgumentException('signature'); @@ -66,27 +61,25 @@ throw new InvalidRpcArgumentException('peer', $e); } - if($rpcRequest->containsParameter('signature_time')) + if($rpcRequest->containsParameter('time')) { - if(!is_numeric($rpcRequest->getParameter('signature_time'))) + if(!is_numeric($rpcRequest->getParameter('time'))) { - throw new InvalidRpcArgumentException('signature_time', 'Invalid timestamp, must be a Unix Timestamp'); + throw new InvalidRpcArgumentException('time', 'Invalid timestamp, must be a Unix Timestamp'); } return $rpcRequest->produceResponse(Socialbox::verifyTimedSignature( signingPeer: $peerAddress, signatureUuid: $rpcRequest->getParameter('signature_uuid'), - signatureKey: $rpcRequest->getParameter('signature_public_key'), signature: $rpcRequest->getParameter('signature'), messageHash: $rpcRequest->getParameter('sha512'), - signatureTime: (int)$rpcRequest->getParameter('signature_time') + signatureTime: (int)$rpcRequest->getParameter('time') )->value); } return $rpcRequest->produceResponse(Socialbox::verifySignature( signingPeer: $peerAddress, signatureUuid: $rpcRequest->getParameter('signature_uuid'), - signatureKey: $rpcRequest->getParameter('signature_public_key'), signature: $rpcRequest->getParameter('signature'), messageHash: $rpcRequest->getParameter('sha512'), )->value); diff --git a/src/Socialbox/Classes/StandardMethods/Encryption/EncryptionAcceptChannel.php b/src/Socialbox/Classes/StandardMethods/Encryption/EncryptionAcceptChannel.php deleted file mode 100644 index df946c4..0000000 --- a/src/Socialbox/Classes/StandardMethods/Encryption/EncryptionAcceptChannel.php +++ /dev/null @@ -1,31 +0,0 @@ -containsParameter('calling_encryption_public_key')) - { - throw new MissingRpcArgumentException('calling_encryption_public_key'); - } - if(!Cryptography::validatePublicEncryptionKey($rpcRequest->getParameter('calling_encryption_public_key'))) - { - throw new InvalidRpcArgumentException('calling_encryption_public_key', 'Invalid calling encryption public key'); - } - - // Transport Algorithm Validation - if(!$rpcRequest->containsParameter('transport_encryption_algorithm')) - { - throw new MissingRpcArgumentException('transport_encryption_algorithm'); - } - if(!Cryptography::isSupportedAlgorithm($rpcRequest->getParameter('transport_encryption_algorithm'))) - { - throw new InvalidRpcArgumentException('transport_encryption_algorithm', 'Unsupported Transport Encryption Algorithm'); - } - - // Create/Import the encryption channel - try - { - $channelUuid = EncryptionChannelManager::createChannel( - callingPeer: $callingPeer, - receivingPeer: $receivingPeer, - signatureUuid: $callingPeerSignature->getUuid(), - signingPublicKey: $callingPeerSignature->getPublicKey(), - encryptionPublicKey: $rpcRequest->getParameter('calling_encryption_public_key'), - transportEncryptionAlgorithm: $rpcRequest->getParameter('transport_encryption_algorithm'), - uuid: $channelUuid - ); - } - catch (DatabaseOperationException $e) - { - throw new StandardRpcException('Failed to create the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); - } - - // If the receiving peer resides on an external server, then we need to tell the external server - // about the encryption channel so that the receiving peer can see it. - if($receivingPeer->getDomain() !== Configuration::getInstanceConfiguration()->getDomain()) - { - $rpcClient = Socialbox::getExternalSession($receivingPeer->getDomain()); - - } - - return $rpcRequest->produceResponse($channelUuid); - } - - /** - * Returns the PeerAddress of the calling peer, if a server is making a request then the server must provide - * both the UUID of the encryption channel and the PeerAddress of the calling peer to prevent UUID conflicts - * - * Otherwise, the calling peer is assumed to be the authenticated user and no UUID is required - * - * @param ClientRequest $request The full client request - * @param RpcRequest $rpcRequest The focused RPC request - * @return PeerAddress The calling peer - * @throws StandardRpcException If the calling peer cannot be resolved - */ - private static function getCallingPeer(ClientRequest $request, RpcRequest $rpcRequest): PeerAddress - { - if($request->getIdentifyAs() !== null) - { - try - { - // Prevent UUID conflicts if the server is trying to use an UUID that already exists on this server - if (EncryptionChannelManager::channelExists($rpcRequest->getParameter('uuid'))) - { - throw new StandardRpcException('UUID Conflict, a channel with this UUID already exists', StandardError::UUID_CONFLICT); - } - } - catch (DatabaseOperationException $e) - { - throw new StandardRpcException('Failed to resolve channel UUID', StandardError::INTERNAL_SERVER_ERROR, $e); - } - - if($request->getIdentifyAs()->getUsername() == ReservedUsernames::HOST) - { - throw new StandardRpcException('The identifier cannot be a host', StandardError::BAD_REQUEST); - } - - if($request->getIdentifyAs()->getDomain() !== Configuration::getInstanceConfiguration()->getDomain()) - { - Socialbox::resolvePeer($request->getIdentifyAs()); - } - - return $request->getIdentifyAs(); - } - - try - { - return PeerAddress::fromAddress($request->getPeer()->getAddress()); - } - catch(StandardRpcException $e) - { - throw $e; - } - catch(Exception $e) - { - throw new StandardRpcException('The calling peer cannot be resolved', StandardError::INTERNAL_SERVER_ERROR, $e); - } - } - - /** - * Resolves and returns the calling peer's signing key, if the calling peer is coming from an external server - * then the signature returned is the resolved signature from the external server, otherwise the signature - * is locally resolved and returned - * - * @param PeerAddress $callingPeer The calling peer - * @param RpcRequest $rpcRequest The focused RPC request - * @return Signature The resolved signing key - * @throws InvalidRpcArgumentException If one or more RPC parameters are invalid - * @throws MissingRpcArgumentException If one or more RPC parameters are missing - * @throws StandardRpcException If the calling signature cannot be resolved - */ - private static function getCallingSignature(PeerAddress $callingPeer, RpcRequest $rpcRequest): Signature - { - // Caller signature verification - if(!$rpcRequest->containsParameter('calling_signature_uuid')) - { - throw new MissingRpcArgumentException('calling_signature_uuid'); - } - if(!Validator::validateUuid($rpcRequest->getParameter('calling_signature_uuid'))) - { - throw new InvalidRpcArgumentException('calling_signature_uuid', 'Invalid UUID V4'); - } - if(!$rpcRequest->containsParameter('calling_signature_public_key')) - { - throw new MissingRpcArgumentException('calling_signature_public_key'); - } - if(!Cryptography::validatePublicSigningKey($rpcRequest->getParameter('calling_signature_public_key'))) - { - throw new InvalidRpcArgumentException('calling_signature_public_key', 'Invalid Public Key'); - } - - // Resolve the signature - $resolvedCallingSignature = Socialbox::resolvePeerSignature($callingPeer, $rpcRequest->getParameter('calling_signature_uuid')); - if($resolvedCallingSignature->getPublicKey() !== $rpcRequest->getParameter('calling_signature_public_key')) - { - throw new InvalidRpcArgumentException('calling_signature_public_key', 'Public signing key of the calling peer does not match the resolved signature'); - } - if($resolvedCallingSignature->getState() === SigningKeyState::EXPIRED) - { - throw new StandardRpcException('The public signing key of the calling peer has expired', StandardError::EXPIRED); - } - - $resolvedSignature = Socialbox::resolvePeerSignature($callingPeer, $rpcRequest->getParameter('calling_signature_uuid')); - if($resolvedSignature === null) - { - throw new StandardRpcException('The calling peer signature could not be resolved', StandardError::NOT_FOUND); - } - - return $resolvedSignature; - } - - /** - * Returns the PeerAddress of the receiving peer, if the receiving peer is from an external server then the - * receiving peer is resolved and returned, otherwise the receiving peer is locally resolved and returned - * - * @param RpcRequest $rpcRequest The focused RPC request - * @return PeerAddress The receiving peer - * @throws InvalidRpcArgumentException If one or more RPC parameters are invalid - * @throws MissingRpcArgumentException If one or more RPC parameters are missing - * @throws StandardRpcException If the receiving peer cannot be resolved - */ - private static function getReceivingPeer(RpcRequest $rpcRequest): PeerAddress - { - if(!$rpcRequest->containsParameter('receiving_peer')) - { - throw new MissingRpcArgumentException('receiving_peer'); - } - - try - { - $receivingPeer = PeerAddress::fromAddress($rpcRequest->getParameter('receiving_peer')); - } - catch(InvalidArgumentException $e) - { - throw new InvalidRpcArgumentException('receiving_peer', $e); - } - - if($receivingPeer->getUsername() == ReservedUsernames::HOST) - { - throw new InvalidRpcArgumentException('receiving_peer', 'Hosts cannot receive channels'); - } - - // Resolve the receiving peer if it's from an external server - if($receivingPeer->getDomain() !== Configuration::getInstanceConfiguration()->getDomain()) - { - Socialbox::resolvePeer($receivingPeer); - } - - return $receivingPeer; - } - - /** - * @param PeerAddress $receivingPeer - * @param RpcRequest $rpcRequest - * @return Signature - * @throws InvalidRpcArgumentException - * @throws MissingRpcArgumentException - * @throws StandardRpcException - */ - private static function getReceivingSignature(PeerAddress $receivingPeer, RpcRequest $rpcRequest): Signature - { - // Receiving signature verification - if(!$rpcRequest->containsParameter('receiving_signature_uuid')) - { - throw new MissingRpcArgumentException('receiving_signature_uuid'); - } - if(!Validator::validateUuid($rpcRequest->getParameter('receiving_signature_uuid'))) - { - throw new InvalidRpcArgumentException('receiving_signature_uuid', 'Invalid UUID V4'); - } - if(!$rpcRequest->containsParameter('receiving_signature_public_key')) - { - throw new MissingRpcArgumentException('receiving_signature_public_key'); - } - if(!Cryptography::validatePublicSigningKey($rpcRequest->getParameter('receiving_signature_public_key'))) - { - throw new InvalidRpcArgumentException('receiving_signature_public_key', 'Invalid Public Key'); - } - - // Resolve the signature - $resolvedReceivingSignature = Socialbox::resolvePeerSignature($receivingPeer, $rpcRequest->getParameter('receiving_signature_uuid')); - if($resolvedReceivingSignature->getPublicKey() !== $rpcRequest->getParameter('receiving_signature_public_key')) - { - throw new InvalidRpcArgumentException('receiving_signature_public_key', 'Public signing key of the receiving peer does not match the resolved signature'); - } - if($resolvedReceivingSignature->getState() === SigningKeyState::EXPIRED) - { - throw new StandardRpcException('The public signing key of the receiving peer has expired', StandardError::EXPIRED); - } - - $resolvedSignature = Socialbox::resolvePeerSignature($receivingPeer, $rpcRequest->getParameter('receiving_signature_uuid')); - if($resolvedSignature === null) - { - throw new StandardRpcException('The receiving peer signature could not be resolved', StandardError::NOT_FOUND); - } - - return $resolvedSignature; - } - - /** - * @param ClientRequest $request - * @param RpcRequest $rpcRequest - * @return string|null - * @throws InvalidRpcArgumentException - * @throws MissingRpcArgumentException - */ - private static function getChannelUuid(ClientRequest $request, RpcRequest $rpcRequest): ?string - { - if($request->getIdentifyAs() !== null) - { - if(!$rpcRequest->containsParameter('uuid')) - { - throw new MissingRpcArgumentException('uuid'); - } - - if(!Validator::validateUuid($rpcRequest->getParameter('uuid'))) - { - throw new InvalidRpcArgumentException('uuid', 'Invalid UUID V4'); - } - - if(EncryptionChannelManager::channelExists($rpcRequest->getParameter('uuid'))) - { - throw new StandardRpcException('UUID Conflict, a channel with this UUID already exists', StandardError::UUID_CONFLICT); - } - - return $rpcRequest->getParameter('uuid'); - } - - return null; - } - } \ No newline at end of file diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index 137c02a..b07adaf 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -16,8 +16,11 @@ use Socialbox\Classes\StandardMethods\Core\GetSessionState; use Socialbox\Classes\StandardMethods\Core\Ping; use Socialbox\Classes\StandardMethods\Core\ResolvePeer; - use Socialbox\Classes\StandardMethods\Core\ResolvePeerSignature; - use Socialbox\Classes\StandardMethods\Core\VerifyPeerSignature; + use Socialbox\Classes\StandardMethods\Core\ResolveSignature; + use Socialbox\Classes\StandardMethods\Core\VerifySignature; + use Socialbox\Classes\StandardMethods\Encryption\EncryptionAcceptChannel; + use Socialbox\Classes\StandardMethods\Encryption\EncryptionCloseChannel; + use Socialbox\Classes\StandardMethods\Encryption\EncryptionCreateChannel; use Socialbox\Classes\StandardMethods\ServerDocuments\AcceptCommunityGuidelines; use Socialbox\Classes\StandardMethods\ServerDocuments\AcceptPrivacyPolicy; use Socialbox\Classes\StandardMethods\ServerDocuments\AcceptTermsOfService; @@ -127,14 +130,6 @@ // MISC case GET_STATE = 'getState'; - // End-to-End channels for communication purposes - case END_TO_END_CREATE_REQUEST = 'e2eCreateRequest'; - case END_TO_END_GET_REQUESTS = 'e2eGetRequests'; - case END_TO_END_ACCEPT_REQUEST = 'e2eAcceptRequest'; - case END_TO_END_REJECT_REQUEST = 'e2eRejectRequest'; - case END_TO_END_GET_CHANNELS = 'e2eGetChannels'; - case END_TO_END_CLOSE_CHANNEL = 'e2eCloseChannel'; - // Messaging methods case MESSAGES_GET_INBOX = 'messagesGetInbox'; case MESSAGES_GET_UNTRUSTED = 'messagesGetUntrusted'; @@ -189,8 +184,8 @@ self::GET_SESSION_STATE => GetSessionState::execute($request, $rpcRequest), self::PING => Ping::execute($request, $rpcRequest), self::RESOLVE_PEER => ResolvePeer::execute($request, $rpcRequest), - self::RESOLVE_PEER_SIGNATURE => ResolvePeerSignature::execute($request, $rpcRequest), - self::VERIFY_PEER_SIGNATURE => VerifyPeerSignature::execute($request, $rpcRequest), + self::RESOLVE_PEER_SIGNATURE => ResolveSignature::execute($request, $rpcRequest), + self::VERIFY_PEER_SIGNATURE => VerifySignature::execute($request, $rpcRequest), // Server Document Methods self::ACCEPT_PRIVACY_POLICY => AcceptPrivacyPolicy::execute($request, $rpcRequest), @@ -235,18 +230,13 @@ * Checks if the access method is allowed for the given client request. * * @param ClientRequest $clientRequest The client request instance to check access against. - * @return void * @throws DatabaseOperationException If an error occurs while checking the database for session information. * @throws StandardRpcException If the method is not allowed for the given client request. + * @return bool */ - public function checkAccess(ClientRequest $clientRequest): void + public function checkAccess(ClientRequest $clientRequest): bool { - if(in_array($this, self::getAllowedMethods($clientRequest))) - { - return; - } - - throw new StandardRpcException(StandardError::METHOD_NOT_ALLOWED->getMessage(), StandardError::METHOD_NOT_ALLOWED); + return in_array($this, self::getAllowedMethods($clientRequest)); } /** diff --git a/src/Socialbox/Enums/Status/SignatureVerificationStatus.php b/src/Socialbox/Enums/Status/SignatureVerificationStatus.php index aa5d277..0ee7710 100644 --- a/src/Socialbox/Enums/Status/SignatureVerificationStatus.php +++ b/src/Socialbox/Enums/Status/SignatureVerificationStatus.php @@ -5,32 +5,32 @@ enum SignatureVerificationStatus : string { /** - * The provided signature does not match the expected signature. + * Returned if the signature is invalid */ case INVALID = 'INVALID'; /** - * The provided signature was valid, but the public key used to verify the signature was not the expected public key. + * Returned if one or more of the parameters are invalid resulting in a failure to verify the signature */ - case PUBLIC_KEY_MISMATCH = 'PUBLIC_KEY_MISMATCH'; + case ERROR = 'ERROR'; /** - * The provided signature was valid, but the UUID used to verify the signature was not the expected UUID. + * Returned if the signing key is not found */ - case UUID_MISMATCH = 'UUID_MISMATCH'; + case NOT_FOUND = 'NOT_FOUND'; /** - * The provided signature was valid, but the signing key has expired. + * Returned if the signature has expired */ case EXPIRED = 'EXPIRED'; /** - * The provided signature was valid but unable to be verified against the peer's known public key. + * Returned if there was an error while trying to resolve the signature locally or externally */ - case UNVERIFIED = 'UNVERIFIED'; + case RESOLUTION_ERROR = 'RESOLUTION_ERROR'; /** - * The provided signature was valid and verified locally and against the peer's known public key successfully. + * Returned if the signature has been successfully verified */ case VERIFIED = 'VERIFIED'; } diff --git a/src/Socialbox/Managers/EncryptionChannelManager.php b/src/Socialbox/Managers/EncryptionChannelManager.php index badcede..29e36c4 100644 --- a/src/Socialbox/Managers/EncryptionChannelManager.php +++ b/src/Socialbox/Managers/EncryptionChannelManager.php @@ -1,5 +1,6 @@ prepare('INSERT INTO encryption_channels (uuid, calling_peer, calling_signature_uuid, calling_signature_public_key, calling_encryption_public_key, receiving_peer, transport_encryption_algorithm) VALUES (:uuid, :calling_peer, :calling_signature_uuid, :calling_signature_public_key, :calling_encryption_public_key, :receiving_peer, :transport_encryption_algorithm)'); + $stmt = Database::getConnection()->prepare('INSERT INTO encryption_channels (uuid, calling_peer, calling_signature_uuid, calling_encryption_public_key, receiving_peer, transport_encryption_algorithm) VALUES (:uuid, :calling_peer, :calling_signature_uuid, :calling_encryption_public_key, :receiving_peer, :transport_encryption_algorithm)'); $stmt->bindParam(':uuid', $uuid); $callingPeerAddress = $callingPeer->getAddress(); $stmt->bindParam(':calling_peer', $callingPeerAddress); $stmt->bindParam(':calling_signature_uuid', $signatureUuid); - $stmt->bindParam(':calling_signature_public_key', $signingPublicKey); $stmt->bindParam(':calling_encryption_public_key', $encryptionPublicKey); $receivingPeerAddress = $receivingPeer->getAddress(); $stmt->bindParam(':receiving_peer', $receivingPeerAddress); @@ -96,7 +90,6 @@ * @param int $page The page of channels to retrieve. * @return EncryptionChannelRecord[] The incoming channels for the peer. * @throws DatabaseOperationException If an error occurs while retrieving the channels. - * @throws \DateMalformedStringException If the created date is not a valid date string. */ public static function getChannels(string|PeerAddress $peerAddress, int $limit=100, int $page=0): array { @@ -137,7 +130,6 @@ * @param int $page The page of channels to retrieve. * @return EncryptionChannelRecord[] The incoming channels for the peer. * @throws DatabaseOperationException If an error occurs while retrieving the channels. - * @throws \DateMalformedStringException If the created date is not a valid date string. */ public static function getRequests(string|PeerAddress $peerAddress, int $limit=100, int $page=0): array { @@ -180,7 +172,6 @@ * @param int $page The page of channels to retrieve. * @return EncryptionChannelRecord[] The incoming channels for the peer. * @throws DatabaseOperationException If an error occurs while retrieving the channels. - * @throws \DateMalformedStringException If the created date is not a valid date string. */ public static function getIncomingChannels(string|PeerAddress $peerAddress, int $limit=100, int $page=0): array { @@ -221,7 +212,6 @@ * @param int $page The page of channels to retrieve. * @return EncryptionChannelRecord[] The outgoing channels for the specified peer. * @throws DatabaseOperationException If an error occurs while retrieving the channels. - * @throws \DateMalformedStringException If the created date is not a valid date string. */ public static function getOutgoingChannels(string|PeerAddress $peerAddress, int $limit=100, int $page=0): array { @@ -281,23 +271,19 @@ * * @param string $channelUuid The UUID of the channel to accept. * @param string $signatureUuid The UUID of the signature used to create the channel. - * @param string $signaturePublicKey The public key used for signing. * @param string $encryptionPublicKey The public key used for encryption. - * @param string $transportEncryptionAlgorithm The algorithm used for transport encryption. * @param string $encryptedTransportEncryptionKey The encrypted transport encryption key. * @throws DatabaseOperationException If an error occurs while accepting the channel. */ - public static function acceptChannel(string $channelUuid, string $signatureUuid, string $signaturePublicKey, string $encryptionPublicKey, string $transportEncryptionAlgorithm, string $encryptedTransportEncryptionKey): void + public static function acceptChannel(string $channelUuid, string $signatureUuid, string $encryptionPublicKey, string $encryptedTransportEncryptionKey): void { try { - $stmt = Database::getConnection()->prepare('UPDATE encryption_channels SET state=:state, receiving_signature_uuid=:receiving_signature_uuid, receiving_signature_public_key=:receiving_signature_public_key, receiving_encryption_public_key=:receiving_encryption_public_key, transport_encryption_algorithm=:transport_encryption_algorithm, transport_encryption_key=:transport_encryption_key WHERE uuid=:uuid'); + $stmt = Database::getConnection()->prepare('UPDATE encryption_channels SET state=:state, receiving_signature_uuid=:receiving_signature_uuid, receiving_encryption_public_key=:receiving_encryption_public_key, transport_encryption_algorithm=:transport_encryption_algorithm, transport_encryption_key=:transport_encryption_key WHERE uuid=:uuid'); $state = EncryptionChannelState::OPENED->value; $stmt->bindParam(':state', $state); $stmt->bindParam(':receiving_signature_uuid', $signatureUuid); - $stmt->bindParam(':receiving_signature_public_key', $signaturePublicKey); $stmt->bindParam(':receiving_encryption_public_key', $encryptionPublicKey); - $stmt->bindParam(':transport_encryption_algorithm', $transportEncryptionAlgorithm); $stmt->bindParam(':transport_encryption_key', $encryptedTransportEncryptionKey); $stmt->bindParam(':uuid', $channelUuid); $stmt->execute(); @@ -314,7 +300,6 @@ * @param string $channelUuid The UUID of the channel to retrieve. * @return EncryptionChannelRecord|null The record of the encryption channel. Null if the channel does not exist. * @throws DatabaseOperationException If an error occurs while retrieving the channel. - * @throws \DateMalformedStringException If the created date is not a valid date string. */ public static function getChannel(string $channelUuid): ?EncryptionChannelRecord { @@ -343,7 +328,7 @@ * * @param string $channelUuid The UUID of the channel to delete. * @return void - *@throws DatabaseOperationException If an error occurs while deleting the channel. + * @throws DatabaseOperationException If an error occurs while deleting the channel. */ public static function deleteChannel(string $channelUuid): void { @@ -503,7 +488,6 @@ * @param string $messageUuid The UUID of the message to retrieve. * @return ChannelMessageRecord|null The message with the specified UUID. Null if the message does not exist. * @throws DatabaseOperationException If an error occurs while retrieving the message. - * @throws \DateMalformedStringException If the created date is not a valid date string. */ public static function getData(string $channelUuid, string $messageUuid): ?ChannelMessageRecord { @@ -531,23 +515,29 @@ /** * Imports the specified message data into the database. * - * @param EncryptionChannelMessage|ChannelMessageRecord $message The message data to import. + * @param ChannelMessageRecord $message The message data to import. * @throws DatabaseOperationException If an error occurs while importing the message. */ - public static function importData(EncryptionChannelMessage|ChannelMessageRecord $message): void + public static function importData(ChannelMessageRecord $message): void { - $data = $message->toArray(); try { $stmt = Database::getConnection()->prepare('INSERT INTO channel_com (uuid, channel_uuid, recipient, message, signature, received, timestamp) VALUES (:uuid, :channel_uuid, :recipient, :message, :signature, :received, :timestamp)'); - $stmt->bindParam(':uuid', $data['uuid']); - $stmt->bindParam(':channel_uuid', $data['channel_uuid']); - $stmt->bindParam(':recipient', $data['recipient']); - $stmt->bindParam(':message', $data['message']); - $stmt->bindParam(':signature', $data['signature']); - $stmt->bindParam(':received', $data['received']); - $stmt->bindParam(':timestamp', $data['timestamp']); + $uuid = $message->getUuid(); + $stmt->bindParam(':uuid', $uuid); + $channelUuid = $message->getChannelUuid(); + $stmt->bindParam(':channel_uuid', $channelUuid); + $recipient = $message->getRecipient()->value; + $stmt->bindParam(':recipient', $recipient); + $messageData = $message->getMessage(); + $stmt->bindParam(':message', $messageData); + $signature = $message->getSignature(); + $stmt->bindParam(':signature', $signature); + $received = $message->isReceived() ? 1 : 0; + $stmt->bindParam(':received', $received); + $timestamp = $message->getTimestamp(); + $stmt->bindParam(':timestamp', $timestamp); $stmt->execute(); } catch(PDOException $e) diff --git a/src/Socialbox/Managers/RegisteredPeerManager.php b/src/Socialbox/Managers/RegisteredPeerManager.php index 55e6192..91622fa 100644 --- a/src/Socialbox/Managers/RegisteredPeerManager.php +++ b/src/Socialbox/Managers/RegisteredPeerManager.php @@ -34,8 +34,8 @@ try { - $statement = Database::getConnection()->prepare('SELECT COUNT(*) FROM peers WHERE username=?'); - $statement->bindParam(1, $username); + $statement = Database::getConnection()->prepare("SELECT COUNT(*) FROM peers WHERE username=:username AND server='host'"); + $statement->bindParam(':username', $username); $statement->execute(); $result = $statement->fetchColumn(); @@ -163,9 +163,9 @@ try { - $statement = Database::getConnection()->prepare('SELECT * FROM peers WHERE username=? AND server=?'); + $statement = Database::getConnection()->prepare('SELECT * FROM peers WHERE username=:username AND server=:server'); $username = $address->getUsername(); - $statement->bindParam(1, $username); + $statement->bindParam(':username', $username); $server = $address->getDomain(); // Convert to 'host' if the domain is the same as the server's host @@ -174,7 +174,7 @@ $server = 'host'; } - $statement->bindParam(2, $server); + $statement->bindParam(':server', $server); $statement->execute(); $result = $statement->fetch(PDO::FETCH_ASSOC); diff --git a/src/Socialbox/Objects/Client/EncryptionChannelSecret.php b/src/Socialbox/Objects/Client/EncryptionChannelSecret.php new file mode 100644 index 0000000..42f10b9 --- /dev/null +++ b/src/Socialbox/Objects/Client/EncryptionChannelSecret.php @@ -0,0 +1,129 @@ +channelUuid = $data['uuid']; + $this->receiver = PeerAddress::fromAddress($data['receiver']); + $this->signatureUuid = $data['signature_uuid']; + $this->publicEncryptionKey = $data['public_encryption_key']; + $this->privateEncryptionKey = $data['private_encryption_key']; + $this->transportEncryptionAlgorithm = $data['transport_encryption_algorithm']; + $this->transportEncryptionKey = $data['transport_encryption_key'] ?? null; + } + + /** + * Returns the UUID of the key pair + * + * @return string The UUID of the key pair + */ + public function getChannelUuid(): string + { + return $this->channelUuid; + } + + /** + * @return PeerAddress + */ + public function getReceiver(): PeerAddress + { + return $this->receiver; + } + + /** + * Returns the UUID of the signature + * + * @return string The UUID of the signature + */ + public function getSignatureUuid(): string + { + return $this->signatureUuid; + } + + /** + * Returns the public key of the key pair + * + * @return string The public key of the key pair + */ + public function getPublicEncryptionKey(): string + { + return $this->publicEncryptionKey; + } + + /** + * Returns the private key of the key pair + * + * @return string The private key of the key pair + */ + public function getPrivateEncryptionKey(): string + { + return $this->privateEncryptionKey; + } + + /** + * @return string + */ + public function getTransportEncryptionAlgorithm(): string + { + return $this->transportEncryptionAlgorithm; + } + + /** + * @return string|null + */ + public function getTransportEncryptionKey(): ?string + { + return $this->transportEncryptionKey; + } + + /** + * @param string|null $transportEncryptionKey + */ + public function setTransportEncryptionKey(?string $transportEncryptionKey): void + { + $this->transportEncryptionKey = $transportEncryptionKey; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): EncryptionChannelSecret + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'uuid' => $this->channelUuid, + 'receiver' => $this->receiver->getAddress(), + 'signature_uuid' => $this->signatureUuid, + 'public_key' => $this->publicEncryptionKey, + 'private_key' => $this->privateEncryptionKey, + 'transport_encryption_algorithm' => $this->transportEncryptionAlgorithm, + 'transport_encryption_key' => $this->transportEncryptionKey + ]; + } + } \ No newline at end of file diff --git a/src/Socialbox/Objects/Client/ExportedSession.php b/src/Socialbox/Objects/Client/ExportedSession.php index db69534..8a18d36 100644 --- a/src/Socialbox/Objects/Client/ExportedSession.php +++ b/src/Socialbox/Objects/Client/ExportedSession.php @@ -29,6 +29,10 @@ * @var SignatureKeyPair[] */ private array $signingKeys; + /** + * @var EncryptionChannelSecret[] + */ + private array $encryptionChannelSecrets; /** * Constructor method to initialize class properties from the provided data array. @@ -62,6 +66,7 @@ $this->serverTransportEncryptionKey = $data['server_transport_encryption_key']; $this->defaultSigningKey = $data['default_signing_key'] ?? null; $this->signingKeys = array_map(fn($key) => SignatureKeyPair::fromArray($key), $data['signing_keys']); + $this->encryptionChannelSecrets = array_map(fn($key) => EncryptionChannelSecret::fromArray($key), $data['encryption_channel_secrets']); } /** @@ -234,6 +239,60 @@ return $this->signingKeys; } + /** + * Retrieves the encrypted channel keys associated with the current instance. + * + * @return EncryptionChannelSecret[] The encrypted channel keys. + */ + public function getEncryptionChannelSecrets(): array + { + return $this->encryptionChannelSecrets; + } + + /** + * Retrieves the signing key associated with the provided UUID. + * + * @param string $uuid The UUID of the signing key. + * @return SignatureKeyPair|null The signing key. + */ + public function getEncryptionChannelSecret(string $uuid): ?EncryptionChannelSecret + { + return $this->encryptionChannelSecrets[$uuid] ?? null; + } + + /** + * Adds a new signing key to the current instance. + * + * @param EncryptionChannelSecret $key The signing key to add. + * @return void + */ + public function addEncryptionChannelSecret(EncryptionChannelSecret $key): void + { + $this->encryptionChannelSecrets[$key->getChannelUuid()] = $key; + } + + /** + * Removes the signing key associated with the provided UUID. + * + * @param string $uuid The UUID of the signing key to remove. + * @return void + */ + public function removeEncryptionChannelSecret(string $uuid): void + { + unset($this->encryptionChannelSecrets[$uuid]); + } + + /** + * Checks if a signing key exists for the provided UUID. + * + * @param string $uuid The UUID of the signing key. + * @return bool True if the signing key exists, false otherwise. + */ + public function encryptionChannelSecretExists(string $uuid): bool + { + return isset($this->encryptionChannelSecrets[$uuid]); + } + /** * @inheritDoc */ @@ -256,7 +315,8 @@ 'client_transport_encryption_key' => $this->clientTransportEncryptionKey, 'server_transport_encryption_key' => $this->serverTransportEncryptionKey, 'default_signing_key' => $this->defaultSigningKey, - 'signing_keys' => array_map(fn($key) => $key->toArray(), $this->signingKeys) + 'signing_keys' => array_map(fn($key) => $key->toArray(), $this->signingKeys), + 'encryption_channel_secrets' => array_map(fn($key) => $key->toArray(), $this->encryptionChannelSecrets), ]; } diff --git a/src/Socialbox/Objects/ClientRequest.php b/src/Socialbox/Objects/ClientRequest.php index 157feba..6f35306 100644 --- a/src/Socialbox/Objects/ClientRequest.php +++ b/src/Socialbox/Objects/ClientRequest.php @@ -170,7 +170,6 @@ * * @return SessionRecord|null Returns the associated SessionRecord if the session UUID exists, or null if no session UUID is set. * @throws DatabaseOperationException Thrown if an error occurs while retrieving the session. - * @throws StandardRpcException Thrown if the session UUID is invalid. */ public function getSession(): ?SessionRecord { @@ -187,7 +186,6 @@ * * @return PeerDatabaseRecord|null Returns the associated RegisteredPeerRecord if available, or null if no session exists. * @throws DatabaseOperationException Thrown if an error occurs while retrieving the peer. - * @throws StandardRpcException Thrown if the session UUID is invalid. */ public function getPeer(): ?PeerDatabaseRecord { @@ -201,6 +199,36 @@ return RegisteredPeerManager::getPeer($session->getPeerUuid()); } + /** + * Returns the Peer Database Record of the identified peer of the request + * + * @return PeerDatabaseRecord|null The Peer Database Record of the identified peer or null if not set + * @throws DatabaseOperationException Thrown if an error occurs while retrieving the peer. + */ + public function getIdentifiedAsPeer(): ?PeerDatabaseRecord + { + $identifiedAs = $this->getIdentifyAs(); + if($identifiedAs === null) + { + return null; + } + + return RegisteredPeerManager::getPeerByAddress($identifiedAs); + } + + /** + * Returns whether the request is external or not. As in, if the request is coming from server rather than + * a client. + * + * @return bool True if the request is external, false otherwise. + * @throws DatabaseOperationException Thrown if an error occurs while retrieving the peer. + * @throws StandardRpcException Thrown if the session UUID is invalid. + */ + public function isExternal(): bool + { + return $this->getPeer()->isExternal(); + } + /** * Retrieves the signature value. * diff --git a/src/Socialbox/Objects/Database/ChannelMessageRecord.php b/src/Socialbox/Objects/Database/ChannelMessageRecord.php index 99c7c5a..33f5d24 100644 --- a/src/Socialbox/Objects/Database/ChannelMessageRecord.php +++ b/src/Socialbox/Objects/Database/ChannelMessageRecord.php @@ -30,7 +30,6 @@ * - 'signature' (string): The signature. * - 'received' (bool): Whether the message has been received. * - 'timestamp' (int|string|\DateTime): The timestamp of the message. - * @throws DateMalformedStringException If the timestamp is a string that cannot be parsed. */ public function __construct(array $data) { diff --git a/src/Socialbox/Objects/Database/EncryptionChannelRecord.php b/src/Socialbox/Objects/Database/EncryptionChannelRecord.php index 92a4246..49c44b9 100644 --- a/src/Socialbox/Objects/Database/EncryptionChannelRecord.php +++ b/src/Socialbox/Objects/Database/EncryptionChannelRecord.php @@ -17,7 +17,6 @@ private string $callingEncryptionPublicKey; private PeerAddress $receivingPeer; private ?string $receivingSignatureUuid; - private ?string $receivingSignaturePublicKey; private ?string $receivingEncryptionPublicKey; private string $transportEncryptionAlgorithm; private ?string $transportEncryptionKey; @@ -28,7 +27,6 @@ * Public Constructor for the encryption channel record * * @param array $data - * @throws \DateMalformedStringException */ public function __construct(array $data) { @@ -78,7 +76,6 @@ } $this->receivingSignatureUuid = $data['receiving_signature_uuid'] ?? null; - $this->receivingSignaturePublicKey = $data['receiving_signature_public_key'] ?? null; $this->receivingEncryptionPublicKey = $data['receiving_encryption_public_key'] ?? null; $this->transportEncryptionAlgorithm = $data['transport_encryption_algorithm']; $this->transportEncryptionKey = $data['transport_encryption_key'] ?? null; @@ -169,16 +166,6 @@ return $this->receivingSignatureUuid; } - /** - * Returns the public key of the signing keypair that the receiver is using - * - * @return string|null - */ - public function getReceivingSignaturePublicKey(): ?string - { - return $this->receivingSignaturePublicKey; - } - /** * Returns the public key of the encryption keypair that the receiver is using * @@ -249,7 +236,6 @@ 'calling_encryption_public_key' => $this->callingEncryptionPublicKey, 'receiving_peer' => $this->receivingPeer->getAddress(), 'receiving_signature_uuid' => $this->receivingSignatureUuid, - 'receiving_signature_public_key' => $this->receivingSignaturePublicKey, 'receiving_encryption_public_key' => $this->receivingEncryptionPublicKey, 'transport_encryption_algorithm' => $this->transportEncryptionAlgorithm, 'transport_encryption_key' => $this->transportEncryptionKey, diff --git a/src/Socialbox/Objects/PeerAddress.php b/src/Socialbox/Objects/PeerAddress.php index 4372e11..27e7b22 100644 --- a/src/Socialbox/Objects/PeerAddress.php +++ b/src/Socialbox/Objects/PeerAddress.php @@ -83,7 +83,7 @@ return false; } - return $this->username === ReservedUsernames::HOST->value; + return $this->domain !== Configuration::getInstanceConfiguration()->getDomain(); } /** diff --git a/src/Socialbox/Objects/RpcRequest.php b/src/Socialbox/Objects/RpcRequest.php index a785d83..070645d 100644 --- a/src/Socialbox/Objects/RpcRequest.php +++ b/src/Socialbox/Objects/RpcRequest.php @@ -87,6 +87,26 @@ return isset($this->parameters[$parameter]); } + /** + * Checks if the parameters exist within the RPC request + * + * @param array $parameters The parameters to check + * @param bool $nullAllowed True if the parameter value can be null, False otherwise. + * @return bool True if the parameters exist, False otherwise. + */ + public function containsParameters(array $parameters, bool $nullAllowed=false): bool + { + foreach($parameters as $parameter) + { + if(!$this->containsParameter($parameter, $nullAllowed)) + { + return false; + } + } + + return true; + } + /** * Returns the parameter value from the RPC request * diff --git a/src/Socialbox/SocialClient.php b/src/Socialbox/SocialClient.php index fbce246..516ba87 100644 --- a/src/Socialbox/SocialClient.php +++ b/src/Socialbox/SocialClient.php @@ -76,53 +76,6 @@ return $uuid; } - /** - * Creates a new encryption channel with the given peer, using the signature UUID to identify the calling peer. - * This method is a wrapper around the encryptionCreateChannel method, and is used to simplify the process of - * creating a new encryption channel. The signature UUID is used to identify the calling peer, and the public key - * of the signature is used as the encryption key. - * - * @param PeerAddress|string $receivingPeer - * @param string $signatureUuid - * @param string $transportEncryptionAlgorithm - * @return string - * @throws CryptographyException - * @throws RpcException - */ - public function newEncryptionChannel(PeerAddress|string $receivingPeer, string $signatureUuid, string $transportEncryptionAlgorithm): string - { - if(!$this->signingKeyExists($signatureUuid)) - { - throw new InvalidArgumentException('The signature UUID does not exist in the client'); - } - - $signature = $this->getSigningKey($signatureUuid); - if($signature === null) - { - throw new InvalidArgumentException('The signature UUID does not exist in the client'); - } - - $encryptionKeypair = Cryptography::generateEncryptionKeyPair(); - $channelUuid = $this->encryptionCreateChannel( - receivingPeer: $receivingPeer, - signatureUUid: $signature->getUuid(), - encryptionPublicKey: $encryptionKeypair->getPublicKey(), - transportEncryptionAlgorithm: $transportEncryptionAlgorithm - ); - - $this->addEncryptionChannelSecret(new EncryptionChannelSecret([ - 'uuid' => $channelUuid, - 'receiver' => $receivingPeer->getAddress(), - 'signature_uuid' => $signature->getUuid(), - 'public_encryption_key' => $encryptionKeypair->getPublicKey(), - 'private_encryption_key' => $encryptionKeypair->getPrivateKey(), - 'transport_encryption_algorithm' => $transportEncryptionAlgorithm, - 'transport_encryption_key' => null - ])); - - return $channelUuid; - } - /** * Adds a new peer to the AddressBook, returns True upon success or False if the contact already exists in * the address book. @@ -459,74 +412,6 @@ )->getResponse()->getResult()) ?? SignatureVerificationStatus::INVALID; } - public function encryptionAcceptChannel(string $channelUuid, string $signatureUuid, string $encryptionPublicKey, string $encryptedTransportEncryptionKey, null|PeerAddress|string $identifiedAs=null): true - { - if($identifiedAs instanceof PeerAddress) - { - $identifiedAs = $identifiedAs->getAddress(); - } - - return $this->sendRequest( - new RpcRequest(StandardMethods::ENCRYPTION_ACCEPT_CHANNEL, parameters: [ - 'channel_uuid' => $channelUuid, - 'signature_uuid' => $signatureUuid, - 'encryption_public_key' => $encryptionPublicKey, - 'encrypted_transport_encryption_key' => $encryptedTransportEncryptionKey - ]), - identifiedAs: $identifiedAs - )->getResponse()->getResult(); - } - - public function encryptionCloseChannel(string $channelUuid, null|PeerAddress|string $identifiedAs=null): true - { - if($identifiedAs instanceof PeerAddress) - { - $identifiedAs = $identifiedAs->getAddress(); - } - - return $this->sendRequest( - new RpcRequest(StandardMethods::ENCRYPTION_CLOSE_CHANNEL, parameters: [ - 'channel_uuid' => $channelUuid, - ]), - identifiedAs: $identifiedAs - )->getResponse()->getResult(); - } - - /** - * Accepts an encryption channel request, returns True if the channel was accepted. - * - * @param PeerAddress|string $receivingPeer The address of the receiving peer that the channel is being requested to - * @param string $signatureUUid The UUID of the calling signature - * @param string $encryptionPublicKey The public key of the calling encryption key - * @param string $transportEncryptionAlgorithm The transport encryption algorithm to use - * @param string|null $identifyAs Optional. The address of the peer to identify as - * @param string|null $channelUuid Optional. If calling to an external server, the server must provide the other server the UUID to use - * @return string Returns True if the channel was accepted - * @throws RpcException Thrown if there was an error with the RPC request - */ - public function encryptionCreateChannel( - PeerAddress|string $receivingPeer, string $signatureUUid, - string $encryptionPublicKey, string $transportEncryptionAlgorithm='xchacha20', - ?string $identifyAs=null, ?string $channelUuid=null - ): string - { - if($receivingPeer instanceof PeerAddress) - { - $receivingPeer = $receivingPeer->getAddress(); - } - - return $this->sendRequest( - new RpcRequest(StandardMethods::ENCRYPTION_CREATE_CHANNEL, parameters: [ - 'receiving_peer' => $receivingPeer, - 'signature_uuid' => $signatureUUid, - 'encryption_public_key' => $encryptionPublicKey, - 'transport_encryption_algorithm' => $transportEncryptionAlgorithm, - 'channel_uuid' => $channelUuid - ]), - identifiedAs: $identifyAs - )->getResponse()->getResult(); - } - /** * Accepts the community guidelines, returns True if the guidelines were accepted. * diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index c70bc17..ce667d3 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -200,7 +200,7 @@ // Prevent the peer from identifying as the host unless it's coming from an external domain if($clientRequest->getIdentifyAs()->getUsername() === ReservedUsernames::HOST->value) { - self::returnError(403, StandardError::FORBIDDEN, 'Unauthorized: Not allowed to identify as the host'); + self::returnError(403, StandardError::FORBIDDEN, 'Forbidden: Not allowed to identify as the host'); return; } } @@ -225,7 +225,7 @@ } catch (Exception $e) { - self::returnError(502, StandardError::RESOLUTION_FAILED, 'Conflict: Failed to resolve the host domain: ' . $e->getMessage(), $e); + self::returnError(502, StandardError::RESOLUTION_FAILED, 'Conflict: Failed to resolve incoming host, ' . $e->getMessage(), $e); return; } } @@ -258,14 +258,6 @@ RegisteredPeerManager::enablePeer($peerUuid); $registeredPeer = RegisteredPeerManager::getPeer($peerUuid); } - else - { - // If the host is registered, but disabled, enable it - if(!$registeredPeer->isEnabled()) - { - RegisteredPeerManager::enablePeer($registeredPeer->getUuid()); - } - } } if($registeredPeer === null) @@ -355,7 +347,16 @@ return; } - $session = $clientRequest->getSession(); + try + { + $session = $clientRequest->getSession(); + } + catch (DatabaseOperationException $e) + { + self::returnError(500, StandardError::INTERNAL_SERVER_ERROR, 'Failed to retrieve session', $e); + return; + } + if($session === null) { self::returnError(404, StandardError::SESSION_NOT_FOUND, 'Session not found'); @@ -525,36 +526,41 @@ { try { - $peer = $clientRequest->getPeer(); + $hostPeer = $clientRequest->getPeer(); } catch (DatabaseOperationException $e) { self::returnError(500, StandardError::INTERNAL_SERVER_ERROR, 'Failed to resolve host peer', $e); - } - - // First check if the client is identifying as the host - if($peer->getAddress() !== ReservedUsernames::HOST->value) - { - // TODO: Maybe allow user client to change identification but within an RPC method rather than the headers - self::returnError(403, StandardError::FORBIDDEN, 'Unauthorized: Not allowed to identify as a different peer'); return; } - if($clientRequest->getIdentifyAs()->getDomain() != $) + // If for some reason the host peer was not found, this shouldn't happen. + if($hostPeer === null) + { + self::returnError(404, StandardError::INTERNAL_SERVER_ERROR, 'The host peer was not found in the system'); + return; + } + // First check if the client is identifying as the host + elseif($hostPeer->getAddress() !== ReservedUsernames::HOST->value) + { + self::returnError(403, StandardError::FORBIDDEN, 'Forbidden: External servers must identify as a host'); + return; + } + // Secondly, check if the peer's server belongs to another server than the server is identified as + elseif($hostPeer->getServer() !== $clientRequest->getIdentifyAs()->getDomain()) + { + self::returnError(403, StandardError::FORBIDDEN, 'Forbidden: Not allowed to identify as a peer outside from the host server'); + return; + } // Synchronize the peer try { self::resolvePeer($clientRequest->getIdentifyAs()); } - catch (DatabaseOperationException $e) + catch (StandardRpcException $e) { - self::returnError(500, StandardError::INTERNAL_SERVER_ERROR, 'Failed to synchronize external peer', $e); - return; - } - catch (Exception $e) - { - throw new ResolutionException(sprintf('Failed to synchronize external peer %s: %s', $clientRequest->getIdentifyAs()->getAddress(), $e->getMessage()), $e->getCode(), $e); + throw new ResolutionException(sprintf('Failed to resolve peer %s: %s', $clientRequest->getIdentifyAs()->getAddress(), $e->getMessage()), $e->getCode(), $e); } } @@ -568,68 +574,76 @@ return; } + if(count($clientRequests) === 0) + { + Logger::getLogger()->warning(sprintf('Received no RPC requests from %s', $_SERVER['REMOTE_ADDR'])); + http_response_code(204); + return; + } + Logger::getLogger()->verbose(sprintf('Received %d RPC request(s) from %s', count($clientRequests), $_SERVER['REMOTE_ADDR'])); $results = []; foreach($clientRequests as $rpcRequest) { $method = StandardMethods::tryFrom($rpcRequest->getMethod()); - - try - { - $method->checkAccess($clientRequest); - } - catch (StandardRpcException $e) - { - $response = $e->produceError($rpcRequest); - $results[] = $response->toArray(); - continue; - } - if($method === false) { Logger::getLogger()->warning('The requested method does not exist'); - $response = $rpcRequest->produceError(StandardError::RPC_METHOD_NOT_FOUND, 'The requested method does not exist'); - } - else - { - try - { - Logger::getLogger()->debug(sprintf('Processing RPC request for method %s', $rpcRequest->getMethod())); - $response = $method->execute($clientRequest, $rpcRequest); - Logger::getLogger()->debug(sprintf('%s method executed successfully', $rpcRequest->getMethod())); - } - catch(StandardRpcException $e) - { - Logger::getLogger()->error('An error occurred while processing the RPC request', $e); - $response = $e->produceError($rpcRequest); - } - catch(Exception $e) - { - Logger::getLogger()->error('An internal error occurred while processing the RPC request', $e); - if(Configuration::getSecurityConfiguration()->isDisplayInternalExceptions()) - { - $response = $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, Utilities::throwableToString($e)); - } - else - { - $response = $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR); - } - } + $results[] = $rpcRequest->produceError(StandardError::RPC_METHOD_NOT_FOUND, 'The requested method does not exist'); + continue; } - if($response !== null) + try { - Logger::getLogger()->debug(sprintf('Producing response for method %s', $rpcRequest->getMethod())); - $results[] = $response->toArray(); + if (!$method->checkAccess($clientRequest)) + { + $results[] = $rpcRequest->produceError(StandardError::METHOD_NOT_ALLOWED, 'Insufficient access requirements to invoke the session'); + continue; + } + } + catch (DatabaseOperationException $e) + { + Logger::getLogger()->error('Failed to check method access', $e); + $results[] = $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, 'Failed to check method access due to an internal server error'); + continue; + } + catch (StandardRpcException $e) + { + $results[] = $e->produceError($rpcRequest); + continue; + } + + try + { + Logger::getLogger()->debug(sprintf('Processing RPC request for method %s', $rpcRequest->getMethod())); + $results[] = $method->execute($clientRequest, $rpcRequest); + Logger::getLogger()->debug(sprintf('%s method executed successfully', $rpcRequest->getMethod())); + } + catch(StandardRpcException $e) + { + Logger::getLogger()->error('An error occurred while processing the RPC request', $e); + $results[] = $e->produceError($rpcRequest); + } + catch(Exception $e) + { + Logger::getLogger()->error('An internal error occurred while processing the RPC request', $e); + if(Configuration::getSecurityConfiguration()->isDisplayInternalExceptions()) + { + $results[] = $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, Utilities::throwableToString($e)); + } + else + { + $results[] = $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR); + } } } - $response = null; - + $results = array_map(fn($result) => $result->toArray(), $results); if(count($results) == 0) { - $response = null; + http_response_code(204); + return; } elseif(count($results) == 1) { @@ -640,12 +654,6 @@ $response = json_encode($results, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } - if($response === null) - { - http_response_code(204); - return; - } - $session = $clientRequest->getSession(); try From e2b5bfab32ab583f3c18e5bf24ab7a92dd5b3466 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 20 Feb 2025 00:39:58 -0500 Subject: [PATCH 370/420] Removed unused Encryption channel objects --- .../Managers/EncryptionChannelManager.php | 578 ------------------ .../Objects/Database/ChannelMessageRecord.php | 160 ----- .../Database/EncryptionChannelRecord.php | 256 -------- .../Objects/Standard/EncryptionChannel.php | 222 ------- .../Standard/EncryptionChannelMessage.php | 145 ----- 5 files changed, 1361 deletions(-) delete mode 100644 src/Socialbox/Objects/Database/ChannelMessageRecord.php delete mode 100644 src/Socialbox/Objects/Database/EncryptionChannelRecord.php delete mode 100644 src/Socialbox/Objects/Standard/EncryptionChannel.php delete mode 100644 src/Socialbox/Objects/Standard/EncryptionChannelMessage.php diff --git a/src/Socialbox/Managers/EncryptionChannelManager.php b/src/Socialbox/Managers/EncryptionChannelManager.php index 29e36c4..945d3b5 100644 --- a/src/Socialbox/Managers/EncryptionChannelManager.php +++ b/src/Socialbox/Managers/EncryptionChannelManager.php @@ -3,586 +3,8 @@ namespace Socialbox\Managers; - use InvalidArgumentException; - use ncc\ThirdParty\Symfony\Uid\UuidV4; - use PDO; - use PDOException; - use Socialbox\Classes\Cryptography; - use Socialbox\Classes\Database; - use Socialbox\Enums\Status\EncryptionChannelState; - use Socialbox\Enums\Types\CommunicationRecipientType; - use Socialbox\Exceptions\DatabaseOperationException; - use Socialbox\Objects\Database\ChannelMessageRecord; - use Socialbox\Objects\Database\EncryptionChannelRecord; - use Socialbox\Objects\PeerAddress; class EncryptionChannelManager { - /** - * Creates a new encryption channel between two peers. - * - * @param PeerAddress|string $callingPeer The peer that is creating the channel. - * @param PeerAddress|string $receivingPeer The peer that is receiving the channel. - * @param string $signatureUuid The UUID of the signature used to create the channel. - * @param string $encryptionPublicKey The public key used for encryption. - * @param string $transportEncryptionAlgorithm The algorithm used for transport encryption. - * @return string The UUID of the created channel. - * @throws DatabaseOperationException If an error occurs while creating the channel. - */ - public static function createChannel(PeerAddress|string $callingPeer, PeerAddress|string $receivingPeer, - string $signatureUuid, string $encryptionPublicKey, string $transportEncryptionAlgorithm, - ?string $uuid=null - ): string - { - if(is_string($callingPeer)) - { - $callingPeer = PeerAddress::fromAddress($callingPeer); - } - if(is_string($receivingPeer)) - { - $receivingPeer = PeerAddress::fromAddress($receivingPeer); - } - - - if(!Cryptography::validatePublicEncryptionKey($encryptionPublicKey)) - { - throw new InvalidArgumentException('Invalid encryption public key provided'); - } - - $transportEncryptionAlgorithm = strtolower($transportEncryptionAlgorithm); - if(!Cryptography::isSupportedAlgorithm($transportEncryptionAlgorithm)) - { - throw new InvalidArgumentException('Unsupported transport encryption algorithm'); - } - - if($uuid === null) - { - $uuid = UuidV4::v4()->toRfc4122(); - } - - try - { - $stmt = Database::getConnection()->prepare('INSERT INTO encryption_channels (uuid, calling_peer, calling_signature_uuid, calling_encryption_public_key, receiving_peer, transport_encryption_algorithm) VALUES (:uuid, :calling_peer, :calling_signature_uuid, :calling_encryption_public_key, :receiving_peer, :transport_encryption_algorithm)'); - $stmt->bindParam(':uuid', $uuid); - $callingPeerAddress = $callingPeer->getAddress(); - $stmt->bindParam(':calling_peer', $callingPeerAddress); - $stmt->bindParam(':calling_signature_uuid', $signatureUuid); - $stmt->bindParam(':calling_encryption_public_key', $encryptionPublicKey); - $receivingPeerAddress = $receivingPeer->getAddress(); - $stmt->bindParam(':receiving_peer', $receivingPeerAddress); - $stmt->bindParam(':transport_encryption_algorithm', $transportEncryptionAlgorithm); - $stmt->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to create the encryption channel', $e); - } - - return $uuid; - } - - /** - * Retrieves the incoming encryption channels for the specified peer. - * - * @param string|PeerAddress $peerAddress The peer to retrieve the channels for. - * @param int $limit The maximum number of channels to retrieve. - * @param int $page The page of channels to retrieve. - * @return EncryptionChannelRecord[] The incoming channels for the peer. - * @throws DatabaseOperationException If an error occurs while retrieving the channels. - */ - public static function getChannels(string|PeerAddress $peerAddress, int $limit=100, int $page=0): array - { - if($peerAddress instanceof PeerAddress) - { - $peerAddress = $peerAddress->getAddress(); - } - - try - { - $stmt = Database::getConnection()->prepare('SELECT * FROM encryption_channels WHERE calling_peer=:address OR receiving_peer=:address LIMIT :limit OFFSET :offset'); - $stmt->bindParam(':address', $peerAddress); - $stmt->bindParam(':limit', $limit, PDO::PARAM_INT); - $offset = $page * $limit; - $stmt->bindParam(':offset', $offset, PDO::PARAM_INT); - $stmt->execute(); - $results = $stmt->fetchAll(); - - $channels = []; - foreach($results as $result) - { - $channels[] = new EncryptionChannelRecord($result); - } - - return $channels; - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to retrieve the encryption channels', $e); - } - } - - /** - * Retrieves the incoming encryption channels for the specified peer. - * - * @param string|PeerAddress $peerAddress The peer to retrieve the channels for. - * @param int $limit The maximum number of channels to retrieve. - * @param int $page The page of channels to retrieve. - * @return EncryptionChannelRecord[] The incoming channels for the peer. - * @throws DatabaseOperationException If an error occurs while retrieving the channels. - */ - public static function getRequests(string|PeerAddress $peerAddress, int $limit=100, int $page=0): array - { - if($peerAddress instanceof PeerAddress) - { - $peerAddress = $peerAddress->getAddress(); - } - - try - { - $stmt = Database::getConnection()->prepare('SELECT * FROM encryption_channels WHERE receiving_peer=:address AND state=:state LIMIT :limit OFFSET :offset'); - $stmt->bindParam(':address', $peerAddress); - $state = EncryptionChannelState::AWAITING_RECEIVER->value; - $stmt->bindParam(':state', $state); - $stmt->bindParam(':limit', $limit, PDO::PARAM_INT); - $offset = $page * $limit; - $stmt->bindParam(':offset', $offset, PDO::PARAM_INT); - $stmt->execute(); - $results = $stmt->fetchAll(); - - $channels = []; - foreach($results as $result) - { - $channels[] = new EncryptionChannelRecord($result); - } - - return $channels; - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to retrieve the encryption channels', $e); - } - } - - /** - * Retrieves the incoming encryption channels for the specified peer. - * - * @param string|PeerAddress $peerAddress The peer to retrieve the channels for. - * @param int $limit The maximum number of channels to retrieve. - * @param int $page The page of channels to retrieve. - * @return EncryptionChannelRecord[] The incoming channels for the peer. - * @throws DatabaseOperationException If an error occurs while retrieving the channels. - */ - public static function getIncomingChannels(string|PeerAddress $peerAddress, int $limit=100, int $page=0): array - { - if($peerAddress instanceof PeerAddress) - { - $peerUuid = $peerAddress->getAddress(); - } - - try - { - $stmt = Database::getConnection()->prepare('SELECT * FROM encryption_channels WHERE receiving_peer=:address LIMIT :limit OFFSET :offset'); - $stmt->bindParam(':address', $peerUuid); - $stmt->bindParam(':limit', $limit, PDO::PARAM_INT); - $offset = $page * $limit; - $stmt->bindParam(':offset', $offset, PDO::PARAM_INT); - $stmt->execute(); - $results = $stmt->fetchAll(); - - $channels = []; - foreach($results as $result) - { - $channels[] = new EncryptionChannelRecord($result); - } - - return $channels; - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to retrieve the encryption channels', $e); - } - } - - /** - * Retrieves the outgoing channels for the specified peer. - * - * @param string|PeerAddress $peerAddress The peer to retrieve the channels for. - * @param int $limit The maximum number of channels to retrieve. - * @param int $page The page of channels to retrieve. - * @return EncryptionChannelRecord[] The outgoing channels for the specified peer. - * @throws DatabaseOperationException If an error occurs while retrieving the channels. - */ - public static function getOutgoingChannels(string|PeerAddress $peerAddress, int $limit=100, int $page=0): array - { - if($peerAddress instanceof PeerAddress) - { - $peerAddress = $peerAddress->getAddress(); - } - - try - { - $stmt = Database::getConnection()->prepare('SELECT * FROM encryption_channels WHERE calling_peer=:address LIMIT :limit OFFSET :offset'); - $stmt->bindParam(':address', $peerAddress); - $stmt->bindParam(':limit', $limit, PDO::PARAM_INT); - $offset = $page * $limit; - $stmt->bindParam(':offset', $offset, PDO::PARAM_INT); - $stmt->execute(); - $results = $stmt->fetchAll(); - - $channels = []; - foreach($results as $result) - { - $channels[] = new EncryptionChannelRecord($result); - } - - return $channels; - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to retrieve the encryption channels', $e); - } - } - - /** - * Declines the encryption channel with the specified UUID. - * - * @param string $channelUuid The UUID of the channel to decline. - * @throws DatabaseOperationException If an error occurs while declining the channel. - */ - public static function declineChannel(string $channelUuid): void - { - try - { - $stmt = Database::getConnection()->prepare('UPDATE encryption_channels SET state=:state WHERE uuid=:uuid'); - $state = EncryptionChannelState::DECLINED->value; - $stmt->bindParam(':state', $state); - $stmt->bindParam(':uuid', $channelUuid); - $stmt->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to decline the encryption channel', $e); - } - } - - /** - * Accepts the encryption channel with the specified UUID. - * - * @param string $channelUuid The UUID of the channel to accept. - * @param string $signatureUuid The UUID of the signature used to create the channel. - * @param string $encryptionPublicKey The public key used for encryption. - * @param string $encryptedTransportEncryptionKey The encrypted transport encryption key. - * @throws DatabaseOperationException If an error occurs while accepting the channel. - */ - public static function acceptChannel(string $channelUuid, string $signatureUuid, string $encryptionPublicKey, string $encryptedTransportEncryptionKey): void - { - try - { - $stmt = Database::getConnection()->prepare('UPDATE encryption_channels SET state=:state, receiving_signature_uuid=:receiving_signature_uuid, receiving_encryption_public_key=:receiving_encryption_public_key, transport_encryption_algorithm=:transport_encryption_algorithm, transport_encryption_key=:transport_encryption_key WHERE uuid=:uuid'); - $state = EncryptionChannelState::OPENED->value; - $stmt->bindParam(':state', $state); - $stmt->bindParam(':receiving_signature_uuid', $signatureUuid); - $stmt->bindParam(':receiving_encryption_public_key', $encryptionPublicKey); - $stmt->bindParam(':transport_encryption_key', $encryptedTransportEncryptionKey); - $stmt->bindParam(':uuid', $channelUuid); - $stmt->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to accept the encryption channel', $e); - } - } - - /** - * Retrieves the encryption channel with the specified UUID. - * - * @param string $channelUuid The UUID of the channel to retrieve. - * @return EncryptionChannelRecord|null The record of the encryption channel. Null if the channel does not exist. - * @throws DatabaseOperationException If an error occurs while retrieving the channel. - */ - public static function getChannel(string $channelUuid): ?EncryptionChannelRecord - { - try - { - $stmt = Database::getConnection()->prepare('SELECT * FROM encryption_channels WHERE uuid=:uuid'); - $stmt->bindParam(':uuid', $channelUuid); - $stmt->execute(); - $result = $stmt->fetch(); - - if($result === false) - { - return null; - } - - return new EncryptionChannelRecord($result); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to retrieve the encryption channel', $e); - } - } - - /** - * Deletes the encryption channel with the specified UUID. - * - * @param string $channelUuid The UUID of the channel to delete. - * @return void - * @throws DatabaseOperationException If an error occurs while deleting the channel. - */ - public static function deleteChannel(string $channelUuid): void - { - try - { - $stmt = Database::getConnection()->prepare('DELETE FROM encryption_channels WHERE uuid=:uuid'); - $stmt->bindParam(':uuid', $channelUuid); - $stmt->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to delete the encryption channel', $e); - } - } - - /** - * Updates the state of the encryption channel with the specified UUID. - * - * @param string $channelUuid The UUID of the channel to update. - * @return EncryptionChannelState The current state of the channel. - * @throws DatabaseOperationException If an error occurs while updating the channel state. - */ - public static function getChannelState(string $channelUuid): EncryptionChannelState - { - try - { - $stmt = Database::getConnection()->prepare('SELECT state FROM encryption_channels WHERE uuid=:uuid'); - $stmt->bindParam(':uuid', $channelUuid); - $stmt->execute(); - - return EncryptionChannelState::from($stmt->fetchColumn()); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to retrieve the encryption channel state', $e); - } - } - - /** - * Updates the state of the encryption channel with the specified UUID. - * - * @param string $channelUuid The UUID of the channel to update. - * @param EncryptionChannelState $state The new state of the channel. - * @return void The current state of the channel. - * @throws DatabaseOperationException If an error occurs while updating the channel state. - */ - public static function updateChannelState(string $channelUuid, EncryptionChannelState $state): void - { - try - { - $stmt = Database::getConnection()->prepare('UPDATE encryption_channels SET state=:state WHERE uuid=:uuid'); - $state = $state->value; - $stmt->bindParam(':state', $state); - $stmt->bindParam(':uuid', $channelUuid); - $stmt->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to update the encryption channel state', $e); - } - } - - /** - * Checks if a channel with the provided UUID exists. - * - * @param string $uuid The UUID of the channel to check. - * @return bool True if the channel exists, False otherwise. - * @throws DatabaseOperationException If an error occurs while checking the channel. - */ - public static function channelExists(string $uuid): bool - { - try - { - $stmt = Database::getConnection()->prepare('SELECT COUNT(*) FROM encryption_channels WHERE uuid=:uuid'); - $stmt->bindParam(':uuid', $uuid); - $stmt->execute(); - - return $stmt->fetchColumn() > 0; - } - catch(PDOException $e) - { - throw new DatabaseOperationException('There was an error while trying to check if the channel UUID exists', $e); - } - } - - /** - * Sends data to the specified channel. - * - * @param string $channelUuid The UUID of the channel to send the data to. - * @param string $message The message to send. - * @param string $signature The signature of the message. - * @param CommunicationRecipientType $recipient The recipient type. - * @return string The UUID of the sent message. - * @throws DatabaseOperationException If an error occurs while sending the message. - */ - public static function sendData(string $channelUuid, string $message, string $signature, CommunicationRecipientType $recipient): string - { - $uuid = UuidV4::v4()->toRfc4122(); - - try - { - $stmt = Database::getConnection()->prepare('INSERT INTO channel_com (uuid, channel_uuid, recipient, message, signature) VALUES (:uuid, :channel_uuid, :recipient, :message, :signature)'); - $stmt->bindParam(':uuid', $uuid); - $stmt->bindParam(':channel_uuid', $channelUuid); - $recipient = $recipient->value; - $stmt->bindParam(':recipient', $recipient); - $stmt->bindParam(':message', $message); - $stmt->bindParam(':signature', $signature); - - $stmt->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to send the message', $e); - } - - return $uuid; - } - - /** - * Retrieves the messages for the specified channel and recipient. - * - * @param string $channelUuid The UUID of the channel to retrieve the messages for. - * @param CommunicationRecipientType $recipient The recipient type to retrieve the messages for. - * @return ChannelMessageRecord[] The messages for the specified channel and recipient. - * @throws DatabaseOperationException If an error occurs while retrieving the messages. - */ - public static function receiveData(string $channelUuid, CommunicationRecipientType $recipient): array - { - try - { - $stmt = Database::getConnection()->prepare('SELECT * FROM channel_com WHERE channel_uuid=:channel_uuid AND recipient=:recipient AND received=0 ORDER BY timestamp'); - $stmt->bindParam(':channel_uuid', $channelUuid); - $recipient = $recipient->value; - $stmt->bindParam(':recipient', $recipient); - $stmt->execute(); - $results = $stmt->fetchAll(); - - $messages = []; - foreach($results as $result) - { - $messages[] = new ChannelMessageRecord($result); - } - - return $messages; - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to retrieve the messages', $e); - } - } - - /** - * Retrieves the message with the specified UUID. - * - * @param string $channelUuid The UUID of the channel to retrieve the message for. - * @param string $messageUuid The UUID of the message to retrieve. - * @return ChannelMessageRecord|null The message with the specified UUID. Null if the message does not exist. - * @throws DatabaseOperationException If an error occurs while retrieving the message. - */ - public static function getData(string $channelUuid, string $messageUuid): ?ChannelMessageRecord - { - try - { - $stmt = Database::getConnection()->prepare('SELECT * FROM channel_com WHERE channel_uuid=:channel_uuid AND uuid=:uuid'); - $stmt->bindParam(':channel_uuid', $channelUuid); - $stmt->bindParam(':uuid', $messageUuid); - $stmt->execute(); - $result = $stmt->fetch(); - - if($result === false) - { - return null; - } - - return new ChannelMessageRecord($result); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to retrieve the message', $e); - } - } - - /** - * Imports the specified message data into the database. - * - * @param ChannelMessageRecord $message The message data to import. - * @throws DatabaseOperationException If an error occurs while importing the message. - */ - public static function importData(ChannelMessageRecord $message): void - { - - try - { - $stmt = Database::getConnection()->prepare('INSERT INTO channel_com (uuid, channel_uuid, recipient, message, signature, received, timestamp) VALUES (:uuid, :channel_uuid, :recipient, :message, :signature, :received, :timestamp)'); - $uuid = $message->getUuid(); - $stmt->bindParam(':uuid', $uuid); - $channelUuid = $message->getChannelUuid(); - $stmt->bindParam(':channel_uuid', $channelUuid); - $recipient = $message->getRecipient()->value; - $stmt->bindParam(':recipient', $recipient); - $messageData = $message->getMessage(); - $stmt->bindParam(':message', $messageData); - $signature = $message->getSignature(); - $stmt->bindParam(':signature', $signature); - $received = $message->isReceived() ? 1 : 0; - $stmt->bindParam(':received', $received); - $timestamp = $message->getTimestamp(); - $stmt->bindParam(':timestamp', $timestamp); - $stmt->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to import the message', $e); - } - } - - /** - * Marks the message with the specified UUID as received. - * - * @param string $uuid The UUID of the message to mark as received. - * @throws DatabaseOperationException If an error occurs while marking the message as received. - */ - public static function markDataAsReceived(string $uuid): void - { - try - { - $stmt = Database::getConnection()->prepare('UPDATE channel_com SET received=1 WHERE uuid=:uuid'); - $stmt->bindParam(':uuid', $uuid); - $stmt->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to mark the message as received', $e); - } - } - - /** - * Deletes the message with the specified UUID. - * - * @param string $uuid The UUID of the message to delete. - * @throws DatabaseOperationException If an error occurs while deleting the message. - */ - public static function deleteData(string $uuid): void - { - try - { - $stmt = Database::getConnection()->prepare('DELETE FROM channel_com WHERE uuid=:uuid'); - $stmt->bindParam(':uuid', $uuid); - $stmt->execute(); - } - catch(PDOException $e) - { - throw new DatabaseOperationException('Failed to delete the message', $e); - } - } } \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/ChannelMessageRecord.php b/src/Socialbox/Objects/Database/ChannelMessageRecord.php deleted file mode 100644 index 33f5d24..0000000 --- a/src/Socialbox/Objects/Database/ChannelMessageRecord.php +++ /dev/null @@ -1,160 +0,0 @@ -uuid = $data['uuid']; - $this->channelUuid = $data['channel_uuid']; - $this->recipient = CommunicationRecipientType::from($data['recipient']); - $this->message = $data['message']; - $this->signature = $data['signature']; - $this->received = (bool)$data['received']; - - if($data['timestamp'] instanceof DateTime) - { - $this->timestamp = $data['timestamp']; - } - elseif(is_int($data['timestamp'])) - { - $this->timestamp = (new DateTime())->setTimestamp($data['timestamp']); - } - elseif(is_string($data['timestamp'])) - { - $this->timestamp = new DateTime($data['timestamp']); - } - else - { - throw new InvalidArgumentException('Invalid timestamp type, got ' . gettype($data['timestamp'])); - } - } - - /** - * Returns the unique identifier for the message. - * - * @return string - */ - public function getUuid(): string - { - return $this->uuid; - } - - /** - * Returns the UUID of the channel that the message belongs to. - * - * @return string - */ - public function getChannelUuid(): string - { - return $this->channelUuid; - } - - /** - * Returns the recipient type of the message. - * - * @return CommunicationRecipientType - */ - public function getRecipient(): CommunicationRecipientType - { - return $this->recipient; - } - - /** - * Returns the message content. - * - * @return string - */ - public function getMessage(): string - { - return $this->message; - } - - /** - * Returns the signature of the message. - * - * @return string - */ - public function getSignature(): string - { - return $this->signature; - } - - /** - * Returns whether the message has been received. - * - * @return bool - */ - public function isReceived(): bool - { - return $this->received; - } - - /** - * Returns the timestamp of the message. - * - * @return DateTime - */ - public function getTimestamp(): DateTime - { - return $this->timestamp; - } - - /** - * @inheritDoc - */ - public static function fromArray(array $data): ChannelMessageRecord - { - return new self($data); - } - - /** - * @inheritDoc - */ - public function toArray(): array - { - return [ - 'uuid' => $this->uuid, - 'channel_uuid' => $this->channelUuid, - 'recipient' => $this->recipient->value, - 'message' => $this->message, - 'signature' => $this->signature, - 'received' => $this->received, - 'timestamp' => $this->timestamp->format('Y-m-d H:i:s') - ]; - } - - - public function toStandard(): EncryptionChannelMessage - { - return new EncryptionChannelMessage($this->toArray()); - } - } \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/EncryptionChannelRecord.php b/src/Socialbox/Objects/Database/EncryptionChannelRecord.php deleted file mode 100644 index 49c44b9..0000000 --- a/src/Socialbox/Objects/Database/EncryptionChannelRecord.php +++ /dev/null @@ -1,256 +0,0 @@ -uuid = $data['uuid']; - - if(!isset($data['calling_peer'])) - { - throw new InvalidArgumentException('Missing property calling_peer'); - } - else - { - if(is_string($data['calling_peer'])) - { - $this->callingPeer = PeerAddress::fromAddress($data['calling_peer']); - } - elseif($data['calling_peer'] instanceof PeerAddress) - { - $this->callingPeer = $data['calling_peer']; - } - else - { - throw new InvalidArgumentException('Unexpected calling_peer type, got ' . gettype($data['calling_peer'])); - } - } - - $this->callingSignatureUuid = $data['calling_signature_uuid']; - $this->callingEncryptionPublicKey = $data['calling_encryption_public_key']; - - if(!isset($data['receiving_peer'])) - { - throw new InvalidArgumentException('Missing property receiving_peer'); - } - else - { - if(is_string($data['receiving_peer'])) - { - $this->receivingPeer = PeerAddress::fromAddress($data['receiving_peer']); - } - elseif($data['receiving_peer'] instanceof PeerAddress) - { - $this->receivingPeer = $data['receiving_peer']; - } - else - { - throw new InvalidArgumentException('Unexpected receiving_peer type, got ' . gettype($data['receiving_peer'])); - } - } - - $this->receivingSignatureUuid = $data['receiving_signature_uuid'] ?? null; - $this->receivingEncryptionPublicKey = $data['receiving_encryption_public_key'] ?? null; - $this->transportEncryptionAlgorithm = $data['transport_encryption_algorithm']; - $this->transportEncryptionKey = $data['transport_encryption_key'] ?? null; - $this->state = EncryptionChannelState::tryFrom($data['state']) ?? EncryptionChannelState::ERROR; - - if(!isset($data['created'])) - { - throw new InvalidArgumentException('Missing property created'); - } - else - { - if(is_string($data['created'])) - { - $this->created = new DateTime($data['created']); - } - elseif(is_int($data['created'])) - { - $this->created = (new DateTime())->setTimestamp($data['created']); - } - elseif($data['created'] instanceof DateTime) - { - $this->created = $data['created']; - } - else - { - throw new InvalidArgumentException('Unexpected created type, got ' . gettype($data['created'])); - } - } - } - - /** - * Returns the Unique Universal Identifier for the encryption record - * - * @return string - */ - public function getUuid(): string - { - return $this->uuid; - } - - /** - * Returns the address of the calling peer - * - * @return PeerAddress - */ - public function getCallingPeer(): PeerAddress - { - return $this->callingPeer; - } - - /** - * Returns the UUID of the signing keypair that the caller is using - * - * @return string - */ - public function getCallingSignatureUuid(): string - { - return $this->callingSignatureUuid; - } - - /** - * Returns the public key of the encryption keypair that the caller is using - * - * @return string - */ - public function getCallingEncryptionPublicKey(): string - { - return $this->callingEncryptionPublicKey; - } - - /** - * Returns the address of the receiving peer - * - * @return PeerAddress - */ - public function getReceivingPeer(): PeerAddress - { - return $this->receivingPeer; - } - - /** - * Returns the UUID of the signing keypair that the receiver is using - * - * @return string|null - */ - public function getReceivingSignatureUuid(): ?string - { - return $this->receivingSignatureUuid; - } - - /** - * Returns the public key of the encryption keypair that the receiver is using - * - * @return string|null - */ - public function getReceivingEncryptionPublicKey(): ?string - { - return $this->receivingEncryptionPublicKey; - } - - /** - * Returns the algorithm used for transport encryption - * - * @return string - */ - public function getTransportEncryptionAlgorithm(): string - { - return $this->transportEncryptionAlgorithm; - } - - /** - * Returns the key used for transport encryption - * - * @return string|null - */ - public function getTransportEncryptionKey(): ?string - { - return $this->transportEncryptionKey; - } - - /** - * Returns the current state of the encryption channel - * - * @return EncryptionChannelState - */ - public function getState(): EncryptionChannelState - { - return $this->state; - } - - /** - * Returns the creation date of the encryption channel - * - * @return DateTime - */ - public function getCreated(): DateTime - { - return $this->created; - } - - /** - * @inheritDoc - */ - public static function fromArray(array $data): EncryptionChannelRecord - { - return new self($data); - } - - /** - * @inheritDoc - */ - public function toArray(): array - { - return [ - 'uuid' => $this->uuid, - 'calling_peer' => $this->callingPeer->getAddress(), - 'calling_signature_uuid' => $this->callingSignatureUuid, - 'calling_encryption_public_key' => $this->callingEncryptionPublicKey, - 'receiving_peer' => $this->receivingPeer->getAddress(), - 'receiving_signature_uuid' => $this->receivingSignatureUuid, - 'receiving_encryption_public_key' => $this->receivingEncryptionPublicKey, - 'transport_encryption_algorithm' => $this->transportEncryptionAlgorithm, - 'transport_encryption_key' => $this->transportEncryptionKey, - 'state' => $this->state->value, - 'created' => $this->created->format('Y-m-d H:i:s') - ]; - } - - /** - * Converts the Encryption Channel Record to a Standard Encryption Channel - * - * @return EncryptionChannel - */ - public function toStandard(): EncryptionChannel - { - return new EncryptionChannel($this->toArray()); - } - } \ No newline at end of file diff --git a/src/Socialbox/Objects/Standard/EncryptionChannel.php b/src/Socialbox/Objects/Standard/EncryptionChannel.php deleted file mode 100644 index f0df342..0000000 --- a/src/Socialbox/Objects/Standard/EncryptionChannel.php +++ /dev/null @@ -1,222 +0,0 @@ -uuid = $data['uuid']; - $this->callingPeer = $data['calling_peer']; - $this->callingSignatureUuid = $data['calling_signature_uuid']; - $this->callingSignaturePublicKey = $data['calling_signature_public_key']; - $this->callingEncryptionPublicKey = $data['calling_encryption_public_key']; - $this->receivingPeer = $data['receiving_peer']; - $this->receivingSignatureUuid = $data['receiving_signature_uuid']; - $this->receivingSignaturePublicKey = $data['receiving_signature_public_key']; - $this->receivingEncryptionPublicKey = $data['receiving_encryption_public_key']; - $this->transportEncryptionAlgorithm = $data['transport_encryption_algorithm']; - $this->transportEncryptionKey = $data['transport_encryption_key']; - $this->state = EncryptionChannelState::from($data['state']); - - if($data['created'] instanceof DateTime) - { - $this->created = $data['created']->getTimestamp(); - } - elseif(is_int($data['created'])) - { - $this->created = $data['created']; - } - elseif(is_string($data['created'])) - { - $this->created = strtotime($data['created']) ?: throw new InvalidArgumentException('Invalid date format'); - } - else - { - throw new InvalidArgumentException('Invalid date format, got type: ' . gettype($data['created'])); - } - } - - /** - * Returns the Unique Universal Identifier of the Encryption Channel - * - * @return string The UUID of the Encryption Channel - */ - public function getUuid(): string - { - return $this->uuid; - } - - /** - * Returns the Peer address that initiated the Encryption Channel - * - * @return string The Peer address that initiated the Encryption Channel - */ - public function getCallingPeer(): string - { - return $this->callingPeer; - } - - /** - * Returns the Unique Universal Identifier of the Signature used by the calling Peer - * - * @return string The UUID of the Signature used by the calling Peer - */ - public function getCallingSignatureUuid(): string - { - return $this->callingSignatureUuid; - } - - /** - * Returns the Public Key of the Signature used by the calling Peer - * - * @return string The Public Key of the Signature used by the calling Peer - */ - public function getCallingSignaturePublicKey(): string - { - return $this->callingSignaturePublicKey; - } - - /** - * Returns the Public Key of the Encryption used by the calling Peer - * - * @return string The Public Key of the Encryption used by the calling Peer - */ - public function getCallingEncryptionPublicKey(): string - { - return $this->callingEncryptionPublicKey; - } - - /** - * Returns the Peer address that received the Encryption Channel - * - * @return string The Peer address that received the Encryption Channel - */ - public function getReceivingPeer(): string - { - return $this->receivingPeer; - } - - /** - * Returns the Unique Universal Identifier of the Signature used by the receiving Peer - * - * @return string|null The UUID of the Signature used by the receiving Peer, or null if not set - */ - public function getReceivingSignatureUuid(): ?string - { - return $this->receivingSignatureUuid; - } - - /** - * Returns the Public Key of the Signature used by the receiving Peer - * - * @return string|null The Public Key of the Signature used by the receiving Peer, or null if not set - */ - public function getReceivingSignaturePublicKey(): ?string - { - return $this->receivingSignaturePublicKey; - } - - /** - * Returns the Public Key of the Encryption used by the receiving Peer - * - * @return string|null The Public Key of the Encryption used by the receiving Peer, or null if not set - */ - public function getReceivingEncryptionPublicKey(): ?string - { - return $this->receivingEncryptionPublicKey; - } - - /** - * Returns the Algorithm used for the Transport Encryption - * - * @return string The Algorithm used for the Transport Encryption - */ - public function getTransportEncryptionAlgorithm(): string - { - return $this->transportEncryptionAlgorithm; - } - - /** - * Returns the Key used for the Transport Encryption - * - * @return string|null The Key used for the Transport Encryption, or null if not set - */ - public function getTransportEncryptionKey(): ?string - { - return $this->transportEncryptionKey; - } - - /** - * Returns the State of the Encryption Channel - * - * @return EncryptionChannelState The State of the Encryption Channel - */ - public function getState(): EncryptionChannelState - { - return $this->state; - } - - /** - * Returns the Unix Timestamp of the creation date of the Encryption Channel - * - * @return int The Unix Timestamp of the creation date of the Encryption Channel - */ - public function getCreated(): int - { - return $this->created; - } - - /** - * @inheritDoc - */ - public static function fromArray(array $data): EncryptionChannel - { - return new self($data); - } - - /** - * @inheritDoc - */ - public function toArray(): array - { - return [ - 'uuid' => $this->uuid, - 'calling_peer' => $this->callingPeer, - 'calling_signature_uuid' => $this->callingSignatureUuid, - 'calling_encryption_public_key' => $this->callingEncryptionPublicKey, - 'receiving_peer' => $this->receivingPeer, - 'receiving_signature_uuid' => $this->receivingSignatureUuid, - 'receiving_signature_public_key' => $this->receivingSignaturePublicKey, - 'receiving_encryption_public_key' => $this->receivingEncryptionPublicKey, - 'transport_encryption_algorithm' => $this->transportEncryptionAlgorithm, - 'transport_encryption_key' => $this->transportEncryptionKey, - 'state' => $this->state->value, - 'created' => $this->created - ]; - } - } \ No newline at end of file diff --git a/src/Socialbox/Objects/Standard/EncryptionChannelMessage.php b/src/Socialbox/Objects/Standard/EncryptionChannelMessage.php deleted file mode 100644 index ccd8573..0000000 --- a/src/Socialbox/Objects/Standard/EncryptionChannelMessage.php +++ /dev/null @@ -1,145 +0,0 @@ -uuid = $data['uuid']; - $this->channelUuid = $data['channel_uuid']; - $this->recipient = CommunicationRecipientType::from($data['recipient']); - $this->message = $data['message']; - $this->signature = $data['signature']; - $this->received = (bool)$data['received']; - - if($data['timestamp'] instanceof DateTime) - { - $this->timestamp = $data['timestamp']->getTimestamp(); - } - elseif(is_int($data['timestamp'])) - { - $this->timestamp = $data['timestamp']; - } - elseif(is_string($data['timestamp'])) - { - $this->timestamp = strtotime($data['timestamp']) ?: throw new InvalidArgumentException('Invalid date format'); - } - else - { - throw new InvalidArgumentException('Invalid date format, got type: ' . gettype($data['timestamp'])); - } - } - - /** - * The Unique Universal Identifier of the message. - * - * @return string The UUID of the message. - */ - public function getUuid(): string - { - return $this->uuid; - } - - /** - * The Unique Universal Identifier of the channel. - * - * @return string The UUID of the channel. - */ - public function getChannelUuid(): string - { - return $this->channelUuid; - } - - /** - * The recipient of the message. - * - * @return CommunicationRecipientType The recipient of the message. - */ - public function getRecipient(): CommunicationRecipientType - { - return $this->recipient; - } - - /** - * The encrypted message. - * - * @return string The message. - */ - public function getMessage(): string - { - return $this->message; - } - - /** - * The signature of the decrypted message. - * - * @return string The signature of the message. - */ - public function getSignature(): string - { - return $this->signature; - } - - /** - * Whether the message has been received. - * - * @return bool Whether the message has been received. - */ - public function isReceived(): bool - { - return $this->received; - } - - /** - * The timestamp of the message. - * - * @return int The timestamp of the message. - */ - public function getTimestamp(): int - { - return $this->timestamp; - } - - /** - * @inheritDoc - */ - public static function fromArray(array $data): EncryptionChannelMessage - { - return new self($data); - } - - /** - * @inheritDoc - */ - public function toArray(): array - { - return [ - 'uuid' => $this->uuid, - 'channel_uuid' => $this->channelUuid, - 'recipient' => $this->recipient->value, - 'message' => $this->message, - 'signature' => $this->signature, - 'received' => $this->received, - 'timestamp' => $this->timestamp - ]; - } - } \ No newline at end of file From 1f5a04d04eb52194a9802cce9a3ab43c6f724024 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 21 Feb 2025 22:58:04 -0500 Subject: [PATCH 371/420] Updated PhpDoc --- src/Socialbox/Exceptions/DatabaseOperationException.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Socialbox/Exceptions/DatabaseOperationException.php b/src/Socialbox/Exceptions/DatabaseOperationException.php index c37cd86..fe9f188 100644 --- a/src/Socialbox/Exceptions/DatabaseOperationException.php +++ b/src/Socialbox/Exceptions/DatabaseOperationException.php @@ -7,6 +7,12 @@ class DatabaseOperationException extends Exception { + /** + * DatabaseOperationException constructor. + * + * @param string $message + * @param Throwable|null $throwable + */ public function __construct(string $message, ?Throwable $throwable=null) { parent::__construct($message, 500, $throwable); From 4a2a307fede635f40652c9f6b19a75159293b577 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 21 Feb 2025 22:58:45 -0500 Subject: [PATCH 372/420] Renamed EncryptionChannelState.php to EncryptionChannelStatus.php --- ...ncryptionChannelState.php => EncryptionChannelStatus.php} | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) rename src/Socialbox/Enums/Status/{EncryptionChannelState.php => EncryptionChannelStatus.php} (59%) diff --git a/src/Socialbox/Enums/Status/EncryptionChannelState.php b/src/Socialbox/Enums/Status/EncryptionChannelStatus.php similarity index 59% rename from src/Socialbox/Enums/Status/EncryptionChannelState.php rename to src/Socialbox/Enums/Status/EncryptionChannelStatus.php index fd46611..1a9b37f 100644 --- a/src/Socialbox/Enums/Status/EncryptionChannelState.php +++ b/src/Socialbox/Enums/Status/EncryptionChannelStatus.php @@ -2,11 +2,12 @@ namespace Socialbox\Enums\Status; - enum EncryptionChannelState : string + enum EncryptionChannelStatus : string { case AWAITING_RECEIVER = 'AWAITING_RECEIVER'; + case SERVER_REJECTED = 'SERVER_REJECTED'; + case PEER_REJECTED = 'PEER_REJECTED'; case ERROR = 'ERROR'; - case DECLINED = 'DECLINED'; case OPENED = 'OPENED'; case CLOSED = 'CLOSED'; } From 28a5cb9b077202deb7c9df40acb34c4ba748fa31 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 21 Feb 2025 22:59:10 -0500 Subject: [PATCH 373/420] Renamed CommunicationRecipientType.php to EncryptionMessageRecipient.php --- .../Types/CommunicationRecipientType.php | 9 -------- .../Types/EncryptionMessageRecipient.php | 23 +++++++++++++++++++ 2 files changed, 23 insertions(+), 9 deletions(-) delete mode 100644 src/Socialbox/Enums/Types/CommunicationRecipientType.php create mode 100644 src/Socialbox/Enums/Types/EncryptionMessageRecipient.php diff --git a/src/Socialbox/Enums/Types/CommunicationRecipientType.php b/src/Socialbox/Enums/Types/CommunicationRecipientType.php deleted file mode 100644 index b7aca3f..0000000 --- a/src/Socialbox/Enums/Types/CommunicationRecipientType.php +++ /dev/null @@ -1,9 +0,0 @@ - self::RECEIVER, + self::RECEIVER => self::SENDER + }; + } + } From 1c95fb93eac58d596cd2c6ba9982a997631d6570 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 21 Feb 2025 22:59:20 -0500 Subject: [PATCH 374/420] Refactored EncryptionChannelManager --- .../Managers/EncryptionChannelManager.php | 554 ++++++++++++++++++ 1 file changed, 554 insertions(+) diff --git a/src/Socialbox/Managers/EncryptionChannelManager.php b/src/Socialbox/Managers/EncryptionChannelManager.php index 945d3b5..8800ce3 100644 --- a/src/Socialbox/Managers/EncryptionChannelManager.php +++ b/src/Socialbox/Managers/EncryptionChannelManager.php @@ -4,7 +4,561 @@ namespace Socialbox\Managers; + use InvalidArgumentException; + use PDOException; + use Socialbox\Classes\Cryptography; + use Socialbox\Classes\Database; + use Socialbox\Classes\Validator; + use Socialbox\Enums\Status\EncryptionChannelStatus; + use Socialbox\Enums\Types\EncryptionMessageRecipient; + use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Objects\Database\EncryptionChannelMessageRecord; + use Socialbox\Objects\Database\EncryptionChannelRecord; + use Socialbox\Objects\PeerAddress; + use Symfony\Component\Uid\Uuid; + class EncryptionChannelManager { + /** + * Checks if an encryption channel with the specified UUID exists. + * + * @param string $channelUuid The UUID of the channel. + * @return bool True if the channel exists, false otherwise. + * @throws DatabaseOperationException If the database operation fails. + */ + public static function channelUuidExists(string $channelUuid): bool + { + try + { + $stmt = Database::getConnection()->prepare('SELECT COUNT(*) FROM encryption_channels WHERE uuid=:uuid'); + $stmt->bindParam(':uuid', $channelUuid); + $stmt->execute(); + return $stmt->fetchColumn() > 0; + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to check if channel UUID exists', $e); + } + } + + /** + * Deletes an encryption channel with the specified UUID. + * + * @param string $channelUuid The UUID of the channel. + * @return void + * @throws DatabaseOperationException If the database operation fails. + */ + public static function deleteChannel(string $channelUuid): void + { + if(!Validator::validateUuid($channelUuid)) + { + throw new InvalidArgumentException('Invalid UUID V4'); + } + + try + { + $stmt = Database::getConnection()->prepare('DELETE FROM encryption_channels WHERE uuid=:uuid LIMIT 1'); + $stmt->bindParam(':uuid', $channelUuid); + $stmt->execute(); + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to delete encryption channel', $e); + } + } + + /** + * Creates a new encryption channel by inserting the caller's request information into the database. + * + * @param string|PeerAddress $callingPeer The peer address of the caller. + * @param string|PeerAddress $receivingPeer The peer address of the receiver. + * @param string $callingPublicEncryptionKey The public encryption key of the caller. + * @param string|null $uuid The UUID of the channel. If not provided, a new UUID will be generated. + * @return string The UUID of the created channel. + * @throws DatabaseOperationException If the database operation fails. + */ + public static function createChannel(string|PeerAddress $callingPeer, string|PeerAddress $receivingPeer, + string $callingPublicEncryptionKey, ?string $uuid=null): string + { + if($uuid === null) + { + $uuid = Uuid::v4()->toRfc4122(); + } + elseif(!Validator::validateUuid($uuid)) + { + throw new InvalidArgumentException('Invalid UUID V4'); + } + + if($callingPeer instanceof PeerAddress) + { + $callingPeer = $callingPeer->getAddress(); + } + elseif(!Validator::validatePeerAddress($callingPeer)) + { + throw new InvalidArgumentException('Invalid calling peer address'); + } + + if($receivingPeer instanceof PeerAddress) + { + $receivingPeer = $receivingPeer->getAddress(); + } + elseif(!Validator::validatePeerAddress($receivingPeer)) + { + throw new InvalidArgumentException('Invalid receiving peer address'); + } + + if(!Cryptography::validatePublicEncryptionKey($callingPublicEncryptionKey)) + { + throw new InvalidArgumentException('Invalid public encryption key'); + } + + try + { + $uuid = $uuid ?? Uuid::v4()->toRfc4122(); + $stmt = Database::getConnection()->prepare('INSERT INTO encryption_channels (uuid, calling_peer_address, receiving_peer_address, calling_peer_address, calling_public_encryption_key) VALUES (:uuid, :calling_peer_address, :receiving_peer_address, :calling_peer_address, :calling_public_encryption_key)'); + $stmt->bindParam(':uuid', $uuid); + $stmt->bindParam(':calling_peer_address', $callingPeer); + $stmt->bindParam(':receiving_peer_address', $receivingPeer); + $stmt->bindParam(':calling_public_encryption_key', $callingPublicEncryptionKey); + $stmt->execute(); + + return $uuid; + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to create encryption channel', $e); + } + } + + /** + * Declines an encryption channel with the specified UUID. + * + * @param string $channelUuid The UUID of the channel. + * @param bool $isServer True if the server is declining the channel, false if the peer is declining the channel. + * @return void + * @throws DatabaseOperationException If the database operation fails. + */ + public static function declineChannel(string $channelUuid, bool $isServer=false): void + { + if(!Validator::validateUuid($channelUuid)) + { + throw new InvalidArgumentException('Invalid UUID V4'); + } + + try + { + $status = $isServer ? EncryptionChannelStatus::SERVER_REJECTED->value : EncryptionChannelStatus::PEER_REJECTED->value; + + $stmt = Database::getConnection()->prepare('UPDATE encryption_channels SET status=:status WHERE uuid=:uuid LIMIT 1'); + $stmt->bindParam(':uuid', $channelUuid); + $stmt->bindParam(':status', $status); + $stmt->execute(); + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to decline encryption channel', $e); + } + } + + /** + * Accepts an incoming channel as the receiver, requires the receiver's generated public encryption key + * so that both sides can preform a DHE and get the shared secret + * + * @param string $channelUuid The Unique Universal Identifier for the channel + * @param string $publicEncryptionKey The public encryption key of the receiver + * @return void + * @throws DatabaseOperationException Thrown if there was a database error while trying to accept the chanel + */ + public static function acceptChannel(string $channelUuid, string $publicEncryptionKey): void + { + if(!Validator::validateUuid($channelUuid)) + { + throw new InvalidArgumentException('Invalid UUID V4'); + } + + if(!Cryptography::validatePublicEncryptionKey($publicEncryptionKey)) + { + throw new InvalidArgumentException('Invalid public encryption key'); + } + + try + { + $stmt = Database::getConnection()->prepare('UPDATE encryption_channels SET status=:status, receiving_public_encryption_key=:public_encryption_key WHERE uuid=:uuid LIMIT 1'); + $status = EncryptionChannelStatus::OPENED->value; + $stmt->bindParam(':uuid', $channelUuid); + $stmt->bindParam(':public_encryption_key', $publicEncryptionKey); + $stmt->bindParam(':status', $status); + $stmt->execute(); + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to decline encryption channel', $e); + } + } + + /** + * Closes an encryption channel with the specified UUID. + * + * @param string $channelUuid The UUID of the channel. + * @return void + * @throws DatabaseOperationException If the database operation fails. + */ + public static function closeChannel(string $channelUuid): void + { + if(!Validator::validateUuid($channelUuid)) + { + throw new InvalidArgumentException('Invalid Channel UUID V4'); + } + + try + { + $stmt = Database::getConnection()->prepare('UPDATE encryption_channels SET status=:status WHERE uuid=:uuid LIMIT 1'); + $status = EncryptionChannelStatus::CLOSED->value; + $stmt->bindParam(':uuid', $channelUuid); + $stmt->bindParam(':status', $status); + $stmt->execute(); + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to decline encryption channel', $e); + } + } + + /** + * Returns an existing encryption channel from the database. + * + * @param string $channelUuid The + * @return EncryptionChannelRecord|null + * @throws DatabaseOperationException + */ + public static function getChannel(string $channelUuid): ?EncryptionChannelRecord + { + if(!Validator::validateUuid($channelUuid)) + { + throw new InvalidArgumentException('Invalid Channel UUID V4'); + } + + try + + { + $stmt = Database::getConnection()->prepare('SELECT * FROM encryption_channels WHERE uuid=:uuid LIMIT 1'); + $stmt->bindParam(':uuid', $channelUuid); + $stmt->execute(); + + if($result = $stmt->fetch()) + { + return EncryptionChannelRecord::fromArray($result); + } + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to retrieve encryption channel', $e); + } + + return null; + } + + /** + * Returns an array of channels that are outgoing from the specified peer address + * + * @param string $peerAddress The Peer Address of the caller + * @return EncryptionChannelRecord[] An array of channel records + * @throws DatabaseOperationException Thrown if there was a database error while retrieving the records + */ + public static function getIncomingChannels(string $peerAddress): array + { + if(!Validator::validatePeerAddress($peerAddress)) + { + throw new InvalidArgumentException('Invalid Peer Address'); + } + + try + { + $stmt = Database::getConnection()->prepare('SELECT * FROM encryption_channels WHERE receiving_peer_address=:peer_address'); + $stmt->bindParam(':peer_address', $peerAddress); + $stmt->execute(); + + $results = $stmt->fetchAll(); + + if(!$results) + { + return []; + } + + return array_map(fn($result) => EncryptionChannelRecord::fromArray($result), $results); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to retrieve incoming encryption channels', $e); + } + } + + /** + * Returns an array of outgoing channels for the given peer address + * + * @param string $peerAddress The Peer Address of the caller + * @return EncryptionChannelRecord[] An array of channel records + * @throws DatabaseOperationException Thrown if there was a database error while retrieving the records + */ + public static function getOutgoingChannels(string $peerAddress): array + { + if(!Validator::validatePeerAddress($peerAddress)) + { + throw new InvalidArgumentException('Invalid Peer Address'); + } + + try + { + $stmt = Database::getConnection()->prepare('SELECT * FROM encryption_channels WHERE calling_peer_address=:peer_address'); + $stmt->bindParam(':peer_address', $peerAddress); + $stmt->execute(); + + $results = $stmt->fetchAll(); + + if(!$results) + { + return []; + } + + return array_map(fn($result) => EncryptionChannelRecord::fromArray($result), $results); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to retrieve outgoing encryption channels', $e); + } + } + + /** + * Returns an array of channels that are awaiting the receiver to accept the channel + * + * @param string $peerAddress The Peer Address of the receiver + * @return EncryptionChannelRecord[] An array of channel records + * @throws DatabaseOperationException Thrown if there was a database error while retrieving the records + */ + public static function getChannelRequests(string $peerAddress): array + { + if(!Validator::validatePeerAddress($peerAddress)) + { + throw new InvalidArgumentException('Invalid Peer Address'); + } + + try + { + $stmt = Database::getConnection()->prepare('SELECT * FROM encryption_channels WHERE receiving_peer_address=:peer_address AND status=:status'); + $stmt->bindParam(':peer_address', $peerAddress); + $status = EncryptionChannelStatus::AWAITING_RECEIVER->value; + $stmt->bindParam(':status', $status); + $stmt->execute(); + + $results = $stmt->fetchAll(); + + if(!$results) + { + return []; + } + + return array_map(fn($result) => EncryptionChannelRecord::fromArray($result), $results); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to retrieve channel requests', $e); + } + } + + /** + * Submits data into the encryption channel + * + * @param string $channelUuid The Unique Universal Identifier of the encryption channel + * @param EncryptionMessageRecipient $recipient The recipient of the message + * @param string $checksum The SHA512 checksum of the decrypted data content + * @param string $data The encrypted data of the message + * @param string|null $uuid Optional. The UUID of the message, used for server-to-server replication + * @param int|null $timestamp Optional. The Timestamp of the message, used for server-to-server replication + * @return string Returns the UUID of the message, if $uuid was provided then it's value is returned. + * @throws DatabaseOperationException Thrown if there was a database error while inserting the record + */ + public static function sendMessage(string $channelUuid, EncryptionMessageRecipient $recipient, string $checksum, string $data, + ?string $uuid=null, ?int $timestamp=null): string + { + if($uuid === null) + { + $uuid = Uuid::v4()->toRfc4122(); + } + elseif(!Validator::validateUuid($uuid)) + { + throw new InvalidArgumentException('Invalid UUID V4 of the message'); + } + + if($timestamp === null) + { + $timestamp = time(); + } + + try + { + $stmt = Database::getConnection()->prepare('INSERT INTO encryption_channels_com (uuid, channel_uuid, recipient, checksum, data, timestamp) VALUES (:uuid, :channel_uuid, :recipient, :checksum, :data, :timestamp)'); + $stmt->bindParam(':uuid', $uuid); + $stmt->bindParam(':channel_uuid', $channelUuid); + $stmt->bindParam(':recipient', $recipient); + $stmt->bindParam(':checksum', $checksum); + $stmt->bindParam(':data', $data); + $stmt->bindParam(':timestamp', $timestamp); + + $stmt->execute(); + } + catch (PDOException $e) + { + throw new DatabaseOperationException('Failed to send data through the encryption channel', $e); + } + + return $uuid; + } + + /** + * Obtains a message record from the database + * + * @param string $channelUuid The Unique Universal Identifier for the channel + * @param string $messageUuid The Unique Universal Identifier for the message + * @return EncryptionChannelMessageRecord|null Returns the message record if found, null otherwise + * @throws DatabaseOperationException Thrown if there was a database operation error + */ + public static function getMessageRecord(string $channelUuid, string $messageUuid): ?EncryptionChannelMessageRecord + { + if(!Validator::validateUuid($channelUuid)) + { + throw new InvalidArgumentException('The given Channel UUID is not a valid V4 UUID'); + } + + if(!Validator::validateUuid($messageUuid)) + { + throw new InvalidArgumentException('The given Message UUID is not a valid V4 UUID'); + } + + try + { + $stmt = Database::getConnection()->prepare("SELECT * FROM encryption_channels_com WHERE channel_uuid=:channel_uuid AND uuid=:message_uuid LIMIT 1"); + $stmt->bindParam(':channel_uuid', $channelUuid); + $stmt->bindParam(':message_uuid', $messageUuid); + $stmt->execute(); + $result = $stmt->fetch(); + + return $result ? EncryptionChannelMessageRecord::fromArray($result) : null; + } + catch(PDOException $e) + { + throw new DatabaseOperationException(sprintf('Failed to retrieve requested message record %s (Channel UUID: %s) from the database', $channelUuid, $messageUuid), $e); + } + } + + /** + * Returns an array of EncryptionChannelMessageRecord objects sorted by the Timestamp + * + * @param string $channelUuid The Unique Universal Identifier of the channel + * @param EncryptionMessageRecipient|string $recipient The recipient of the receiver + * @return EncryptionChannelMessageRecord[] An array of message objects returned + * @throws DatabaseOperationException Thrown if there was a database operation error + */ + public static function receiveData(string $channelUuid, EncryptionMessageRecipient|string $recipient): array + { + if(!Validator::validateUuid($channelUuid)) + { + throw new InvalidArgumentException('The given Channel UUID is not a valid V4 UUID'); + } + + if($recipient instanceof EncryptionMessageRecipient) + { + $recipient = $recipient->value; + } + + try + { + $stmt = Database::getConnection()->prepare('SELECT * FROM encryption_channels_com WHERE channel_uuid=:channel_uuid AND recipient=:recipient ORDER BY timestamp LIMIT 100'); + $stmt->bindParam(':channel_uuid', $channelUuid); + $stmt->bindParam(':recipient', $recipient); + $stmt->execute(); + $results = $stmt->fetchAll(); + + if(!$results) + { + return []; + } + + return array_map(fn($result) => EncryptionChannelMessageRecord::fromArray($result), $results); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('There was an error while trying to receive new data from the database', $e); + } + } + + /** + * Acknowledges the requested message was received + * + * @param string $channelUuid The Unique Universal identifier of the channel + * @param string $messageUuid The Unique Universal Identifier of the message + * @return void + * @throws DatabaseOperationException Thrown if there was an error with the database operation + */ + public static function acknowledgeMessage(string $channelUuid, string $messageUuid): void + { + if(!Validator::validateUuid($channelUuid)) + { + throw new InvalidArgumentException('The given Channel UUID is not a valid V4 UUID'); + } + + if(!Validator::validateUuid($messageUuid)) + { + throw new InvalidArgumentException('The given Message UUID is not a valid V4 uuid'); + } + + try + { + $stmt = Database::getConnection()->prepare("UPDATE encryption_channels_com SET status='RECEIVED' WHERE channel_uuid=:channel_uuid AND uuid=:message_uuid LIMIT 1"); + $stmt->bindParam(':channel_uuid', $channelUuid); + $stmt->bindParam(':message_uuid', $messageUuid); + $stmt->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('There was an error while acknowledging the message record', $e); + } + } + + /** + * Rejects the requested message + * + * @param string $channelUuid The Unique Universal Identifier of the channel + * @param string $messageUuid The Unique Universal Identifier of the message + * @param bool $isServer If True, the message will be rejected as "SERVER_REJECTED" otherwise "PEER_REJECTED" + * @return void + * @throws DatabaseOperationException Thrown if there was an error with the database operation + */ + public static function rejectMessage(string $channelUuid, string $messageUuid, bool $isServer=false): void + { + if(!Validator::validateUuid($channelUuid)) + { + throw new InvalidArgumentException('The given Channel UUID is not a valid V4 UUID'); + } + + if(!Validator::validateUuid($messageUuid)) + { + throw new InvalidArgumentException('The given Message UUId is not a valid V4 uuid'); + } + + try + { + $status = $isServer ? EncryptionChannelStatus::SERVER_REJECTED->value : EncryptionChannelStatus::PEER_REJECTED->value; + $stmt = Database::getConnection()->prepare("UPDATE encryption_channels_com SET status=:status WHERE channel_uuid=:channel_uuid AND uuid=:message_uuid LIMIT 1"); + $stmt->bindParam(':status', $status); + $stmt->bindParam(':channel_uuid', $channelUuid); + $stmt->bindParam(':message_uuid', $messageUuid); + + $stmt->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('There was an error while rejecting the message record', $e); + } + } } \ No newline at end of file From 94b6e1ff01f96c1366b58b9597ab22d410cccb1c Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 21 Feb 2025 22:59:42 -0500 Subject: [PATCH 375/420] Added EncryptionChannelMessageRecord and EncryptionChannelRecord --- .../EncryptionChannelMessageRecord.php | 171 ++++++++++++++++++ .../Database/EncryptionChannelRecord.php | 161 +++++++++++++++++ 2 files changed, 332 insertions(+) create mode 100644 src/Socialbox/Objects/Database/EncryptionChannelMessageRecord.php create mode 100644 src/Socialbox/Objects/Database/EncryptionChannelRecord.php diff --git a/src/Socialbox/Objects/Database/EncryptionChannelMessageRecord.php b/src/Socialbox/Objects/Database/EncryptionChannelMessageRecord.php new file mode 100644 index 0000000..c5e04fb --- /dev/null +++ b/src/Socialbox/Objects/Database/EncryptionChannelMessageRecord.php @@ -0,0 +1,171 @@ +uuid = $data['uuid']; + $this->channelUuid = $data['channel_uuid']; + $this->recipient = EncryptionMessageRecipient::from($data['recipient']); + $this->status = EncryptionChannelMessageStatus::from($data['status']); + $this->hash = $data['hash']; + $this->data = $data['data']; + + if($data['timestamp'] instanceof DateTime) + { + $this->timestamp = $data['timestamp']; + } + elseif(is_int($data['timestamp'])) + { + $this->timestamp = (new DateTime())->setTimestamp($data['timestamp']); + } + elseif(is_string($data['timestamp'])) + { + try + { + $this->timestamp = new DateTime($data['timestamp']); + } + catch (DateMalformedStringException $e) + { + throw new InvalidArgumentException('Invalid DateTime format for timestamp, got: ' . $data['timestamp'], $e->getCode(), $e); + } + } + else + { + throw new InvalidArgumentException('Invalid timestamp type, got: ' . gettype($data['timestamp'])); + } + } + + /** + * Returns the Unique Universal Identifier for the message + * + * @return string The Message's Unique Universal Identifier + */ + public function getUuid(): string + { + return $this->uuid; + } + + /** + * Returns the Unique Universal Identifier of the channel that this message belongs to + * + * @return string The Channel's Unique Universal Identifier + */ + public function getChannelUuid(): string + { + return $this->channelUuid; + } + + /** + * Returns the recipient of the message + * + * @return EncryptionMessageRecipient The recipient of the message + */ + public function getRecipient(): EncryptionMessageRecipient + { + return $this->recipient; + } + + /** + * Returns the status of the message + * + * @return EncryptionChannelMessageStatus The status of the message + */ + public function getStatus(): EncryptionChannelMessageStatus + { + return $this->status; + } + + /**try { + switch($fieldName) { + case InformationFieldName::DISPLAY_NAME: + SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_DISPLAY_NAME]); + break; + // Other cases... + } +} catch (Exception $e) { + try { + PeerInformationManager::deleteProperty($peer, $fieldName); + } catch (DatabaseOperationException $e) { + throw new StandardException('Failed to rollback the information field', StandardError::INTERNAL_SERVER_ERROR, $e); + } + if($e instanceof StandardException) { + throw $e; + } + throw new StandardException('Failed to update the session flow', StandardError::INTERNAL_SERVER_ERROR, $e); +} + * Returns the SHA512 hash of the decrypted content + * + * @return string The SHA512 hash of the decrypted content + */ + public function getHash(): string + { + return $this->hash; + } + + /** + * Returns the encrypted content of the message + * + * @return string The encrypted content of the message + */ + public function getData(): string + { + return $this->data; + } + + /** + * Returns the Timestamp for when this message was created + * + * @return DateTime The Timestamp for when the message was created + */ + public function getTimestamp(): DateTime + { + return $this->timestamp; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): EncryptionChannelMessageRecord + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'uuid' => $this->uuid, + 'channel_uuid' => $this->channelUuid, + 'recipient' => $this->recipient->value, + 'status' => $this->status->value, + 'hash' => $this->hash, + 'data' => $this->data, + 'timestamp' => $this->timestamp->getTimestamp() + ]; + } + } \ No newline at end of file diff --git a/src/Socialbox/Objects/Database/EncryptionChannelRecord.php b/src/Socialbox/Objects/Database/EncryptionChannelRecord.php new file mode 100644 index 0000000..c9440d2 --- /dev/null +++ b/src/Socialbox/Objects/Database/EncryptionChannelRecord.php @@ -0,0 +1,161 @@ +uuid = $data['uuid']; + $this->status = EncryptionChannelStatus::from($data['status']); + $this->callingPeerAddress = PeerAddress::fromAddress($data['calling_peer_address']); + $this->callingPublicEncryptionKey = $data['calling_public_encryption_key']; + $this->receivingPeerAddress = PeerAddress::fromAddress($data['receiving_peer_address']); + $this->receivingPublicEncryptionKey = $data['receiving_public_encryption_key'] ?? null; + + if($data['created'] instanceof DateTime) + { + $this->created = $data['created']; + } + elseif(is_int($data['created'])) + { + $this->created = (new DateTime())->setTimestamp($data['created']); + } + elseif(is_string($data['created'])) + { + try + { + $this->created = new DateTime($data['created']); + } + catch (DateMalformedStringException $e) + { + throw new InvalidArgumentException('Invalid DateTime given in created, got: ' . $data['created'], $e->getCode(), $e); + } + } + else + { + throw new InvalidArgumentException('Invalid created type, got: ' . gettype($data['created'])); + } + } + + /** + * Returns the Universal Unique Identifier of the encryption channel record + * + * @return string The UUID V4 + */ + public function getUuid(): string + { + return $this->uuid; + } + + /** + * Returns the current status of the encryption channel record + * + * @return EncryptionChannelStatus The current status of the encryption channel record + */ + public function getStatus(): EncryptionChannelStatus + { + return $this->status; + } + + /** + * Returns the PeerAddress of the calling peer for the encryption channel record + * + * @return PeerAddress The address of the calling peer + */ + public function getCallingPeerAddress(): PeerAddress + { + return $this->callingPeerAddress; + } + + /** + * Returns the public encryption key of the calling peer + * + * @return string The public encryption key of the caller + */ + public function getCallingPublicEncryptionKey(): string + { + return $this->callingPublicEncryptionKey; + } + + /** + * Returns the PeerAddress of the receiving peer for the encryption channel record + * + * @return PeerAddress + */ + public function getReceivingPeerAddress(): PeerAddress + { + return $this->receivingPeerAddress; + } + + /** + * Returns the public encryption key of the receiving peer + * + * @return string|null The public encryption key of the receiver + */ + public function getReceivingPublicEncryptionKey(): ?string + { + return $this->receivingPublicEncryptionKey; + } + + /** + * The DateTime object of when the record was created + * + * @return DateTime The DateTime object of the record's creation date + */ + public function getCreated(): DateTime + { + return $this->created; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): EncryptionChannelRecord + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'uuid' => $this->uuid, + 'status' => $this->status->value, + 'calling_peer_address' => $this->callingPeerAddress->getAddress(), + 'calling_public_encryption_key' => $this->callingPublicEncryptionKey, + 'receiving_peer_address' => $this->receivingPeerAddress->getAddress(), + 'created' => $this->created->getTimestamp() + ]; + } + } \ No newline at end of file From c018cc8c04e34d6f611e9461081928b4dc75834a Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Mar 2025 04:24:33 -0500 Subject: [PATCH 376/420] Renamed encrypted_channels.sql to encryption_channels.sql and added encryption_channels_com.sql --- .idea/sqldialects.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml index 8fb5dbe..68ae0fe 100644 --- a/.idea/sqldialects.xml +++ b/.idea/sqldialects.xml @@ -7,7 +7,8 @@ - + + From bb94ae8d7860e2e0214ce25a103e28e7636e4c73 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Mar 2025 04:25:30 -0500 Subject: [PATCH 377/420] Add .vscode to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e20b758..9daf5a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /build +/.vscode /.idea/gbrowser_project.xml .phpunit.result.cache /socialbox From f1dbf5a4d06fd2af1122133c3a8217401e2e754a Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Mar 2025 04:25:40 -0500 Subject: [PATCH 378/420] Handle InvalidArgumentException in RPC processing and update error messages --- src/Socialbox/Socialbox.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Socialbox/Socialbox.php b/src/Socialbox/Socialbox.php index ce667d3..4ca8a23 100644 --- a/src/Socialbox/Socialbox.php +++ b/src/Socialbox/Socialbox.php @@ -625,6 +625,11 @@ Logger::getLogger()->error('An error occurred while processing the RPC request', $e); $results[] = $e->produceError($rpcRequest); } + catch(InvalidArgumentException $e) + { + Logger::getLogger()->error('Caught invalid argument exception', $e); + $results[] = $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, $e->getMessage()); + } catch(Exception $e) { Logger::getLogger()->error('An internal error occurred while processing the RPC request', $e); @@ -634,7 +639,7 @@ } else { - $results[] = $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR); + $results[] = $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, 'Uncaught Exception'); } } } @@ -931,7 +936,7 @@ try { - return $client->resolvePeerSignature($peerAddress, $signatureUuid); + return $client->resolveSignature($peerAddress, $signatureUuid); } catch(RpcException $e) { From 27a0005468513f8a8369c19257b49c98d4714aca Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Mar 2025 04:26:05 -0500 Subject: [PATCH 379/420] Remove obsolete encrypted_channels.sql file --- .../Resources/database/encrypted_channels.sql | 36 ------------------- 1 file changed, 36 deletions(-) delete mode 100644 src/Socialbox/Classes/Resources/database/encrypted_channels.sql diff --git a/src/Socialbox/Classes/Resources/database/encrypted_channels.sql b/src/Socialbox/Classes/Resources/database/encrypted_channels.sql deleted file mode 100644 index f906d93..0000000 --- a/src/Socialbox/Classes/Resources/database/encrypted_channels.sql +++ /dev/null @@ -1,36 +0,0 @@ -create table encryption_channels -( - uuid varchar(36) not null comment 'The Unique Universal Identifier for the encryption channel' - primary key comment 'The Unique Index of the encryption channel UUID', - calling_peer varchar(320) not null comment 'The address of the calling peer', - calling_signature_uuid varchar(64) not null comment 'The UUID of the signing key that the calling peer is going to use to sign their messages', - calling_signature_public_key varchar(32) not null, - calling_encryption_public_key varchar(32) not null comment 'The public encryption key of the caller', - receiving_peer varchar(320) not null comment 'The address of the receiving peer', - receiving_signature_uuid varchar(256) null comment 'The UUID of the signature that the receiver peer will use to sign messages with', - receiving_signature_public_key varchar(32) null comment 'The public key of the receiver''s signing key', - receiving_encryption_public_key varchar(32) null comment 'The encryption key of the receiver', - transport_encryption_algorithm enum ('xchacha20', 'chacha20', 'aes256gcm') default 'xchacha20' not null comment 'The transport encryption algorithm used as selected by the caller', - transport_encryption_key varchar(256) null comment 'The transport encryption key encrypted using the caller''s public encryption key', - state enum ('AWAITING_RECEIVER', 'ERROR', 'DECLINED', 'AWAITING_DHE', 'OPENED', 'CLOSED') default 'AWAITING_RECEIVER' not null comment 'The current state of the encryption channel', - created timestamp default current_timestamp() not null comment 'The Timestamp for when this record was created', - constraint encryption_channels_uuid_uindex - unique (uuid) comment 'The Unique Index of the encryption channel UUID' -); - -create index encryption_channels_calling_peer_index - on encryption_channels (calling_peer) - comment 'The index of the calling peer address'; - -create index encryption_channels_created_index - on encryption_channels (created) - comment 'The Index for when the record was created'; - -create index encryption_channels_receiving_peer_index - on encryption_channels (receiving_peer) - comment 'The index of the receiving peer address'; - -create index encryption_channels_state_index - on encryption_channels (state) - comment 'The index for the state column'; - From 42b5ec5cdb1e544a3232ab793e3a2beb2650f23f Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Mar 2025 04:26:14 -0500 Subject: [PATCH 380/420] Add EncryptionCloseChannel method for managing encryption channel closure --- .../EncryptionCloseChannel.php | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionCloseChannel.php diff --git a/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionCloseChannel.php b/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionCloseChannel.php new file mode 100644 index 0000000..159348c --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionCloseChannel.php @@ -0,0 +1,170 @@ +containsParameter('channel_uuid')) + { + throw new MissingRpcArgumentException('channel_uuid'); + } + elseif(!Validator::validateUuid($rpcRequest->getParameter('channel_uuid'))) + { + throw new InvalidRpcArgumentException('channel_uuid', 'The given channel uuid is not a valid UUID V4'); + } + + if($request->isExternal()) + { + return self::handleExternal($request, $rpcRequest); + } + + return self::handleInternal($request, $rpcRequest); + } + + /** + * @param ClientRequest $request + * @param RpcRequest $rpcRequest + * @return SerializableInterface|null + * @throws StandardRpcException + */ + private static function handleInternal(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface + { + + try + { + $requestingPeer = $request->getPeer(); + $encryptionChannel = EncryptionChannelManager::getChannel($rpcRequest->getParameter('channel_uuid')); + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('There was an error while trying to obtain the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if($encryptionChannel === null) + { + return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The requested encryption channel was not found'); + } + elseif(!$encryptionChannel->isParticipant($requestingPeer->getAddress())) + { + return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The requested encryption channel is not accessible'); + } + elseif($encryptionChannel->getStatus() === EncryptionChannelStatus::CLOSED) + { + return $rpcRequest->produceResponse(false); + } + + try + { + EncryptionChannelManager::closeChannel($encryptionChannel->getUuid()); + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('An error occurred while trying to close the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + $externalPeer = $encryptionChannel->getExternalPeer(); + if($externalPeer !== null) + { + try + { + $rpcClient = Socialbox::getExternalSession($encryptionChannel->getCallingPeerAddress()->getDomain()); + $rpcClient->encryptionCloseChannel( + channelUuid: $rpcRequest->getParameter('channel_uuid'), + identifiedAs: $requestingPeer->getAddress() + ); + } + catch(Exception $e) + { + try + { + EncryptionChannelManager::declineChannel($rpcRequest->getParameter('channel_uuid'), true); + } + catch(DatabaseOperationException $e) + { + Logger::getLogger()->error('Error declining channel as server', $e); + } + + if($e instanceof RpcException) + { + throw StandardRpcException::fromRpcException($e); + } + + throw new StandardRpcException('There was an error while trying to notify the external server of the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); + } + } + + return $rpcRequest->produceResponse(true); + } + + /** + * @param ClientRequest $request + * @param RpcRequest $rpcRequest + * @return SerializableInterface|null + * @throws StandardRpcException + */ + private static function handleExternal(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface + { + if($request->getIdentifyAs() === null) + { + throw new StandardRpcException('The IdentifyAs field is required for external requests', StandardError::UNAUTHORIZED); + } + + try + { + $encryptionChannel = EncryptionChannelManager::getChannel($rpcRequest->getParameter('channel_uuid')); + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('An error occurred while trying to obtain the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if($encryptionChannel === null) + { + return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The requested encryption channel was not found'); + } + + if(!$encryptionChannel->isParticipant($request->getIdentifyAs())) + { + return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The requested encryption channel is not accessible'); + } + + if($encryptionChannel->getStatus() === EncryptionChannelStatus::CLOSED) + { + return $rpcRequest->produceResponse(false); + } + + try + { + EncryptionChannelManager::closeChannel($encryptionChannel->getUuid()); + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('An error occurred while trying to close the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file From d2e4ebd657b523ca015fd2ec0e78e7ebe5d27134 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Mar 2025 04:26:23 -0500 Subject: [PATCH 381/420] Rename ENCRYPTED_CHANNELS and CHANNEL_COM constants to ENCRYPTION_CHANNELS and ENCRYPTION_CHANNELS_COM for consistency --- src/Socialbox/Enums/DatabaseObjects.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Socialbox/Enums/DatabaseObjects.php b/src/Socialbox/Enums/DatabaseObjects.php index f2ad497..18b0115 100644 --- a/src/Socialbox/Enums/DatabaseObjects.php +++ b/src/Socialbox/Enums/DatabaseObjects.php @@ -18,8 +18,8 @@ case SIGNING_KEYS = 'signing_keys.sql'; case EXTERNAL_SESSIONS = 'external_sessions.sql'; - case ENCRYPTED_CHANNELS = 'encrypted_channels.sql'; - case CHANNEL_COM = 'channel_com.sql'; + case ENCRYPTION_CHANNELS = 'encryption_channels.sql'; + case ENCRYPTION_CHANNELS_COM = 'encryption_channels_com.sql'; case CONTACT_KNOWN_KEYS = 'contact_known_keys.sql'; @@ -46,8 +46,8 @@ self::SIGNING_KEYS, self::EXTERNAL_SESSIONS => 2, - self::ENCRYPTED_CHANNELS, - self::CHANNEL_COM, + self::ENCRYPTION_CHANNELS, + self::ENCRYPTION_CHANNELS_COM, self::CONTACT_KNOWN_KEYS => 3, }; } From ef9336ca544745ff9e0bada8034ebe57495d1e24 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Mar 2025 04:26:29 -0500 Subject: [PATCH 382/420] Refactor containsParameter method to improve null and empty string handling --- src/Socialbox/Objects/RpcRequest.php | 44 ++++++++++++++-------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/Socialbox/Objects/RpcRequest.php b/src/Socialbox/Objects/RpcRequest.php index 070645d..43f9d5a 100644 --- a/src/Socialbox/Objects/RpcRequest.php +++ b/src/Socialbox/Objects/RpcRequest.php @@ -74,37 +74,37 @@ * Checks if the parameter exists within the RPC request * * @param string $parameter The parameter to check - * @param bool $nullAllowed True if the parameter value can be null, False otherwise. + * @param bool $strict True if the parameter value cannot be null (or empty), False otherwise. * @return bool True if the parameter exists, False otherwise. */ - public function containsParameter(string $parameter, bool $nullAllowed=false): bool + public function containsParameter(string $parameter, bool $strict=true): bool { - if(!$nullAllowed) + if($strict) { - return isset($this->parameters[$parameter]) && $this->parameters[$parameter] !== null; - } - - return isset($this->parameters[$parameter]); - } - - /** - * Checks if the parameters exist within the RPC request - * - * @param array $parameters The parameters to check - * @param bool $nullAllowed True if the parameter value can be null, False otherwise. - * @return bool True if the parameters exist, False otherwise. - */ - public function containsParameters(array $parameters, bool $nullAllowed=false): bool - { - foreach($parameters as $parameter) - { - if(!$this->containsParameter($parameter, $nullAllowed)) + if(!isset($this->parameters[$parameter])) { return false; } + + if(is_string($this->parameters[$parameter]) && strlen($this->parameters[$parameter]) == 0) + { + return false; + } + + if(is_array($this->parameters[$parameter]) && count($this->parameters[$parameter]) == 0) + { + return false; + } + + if(is_null($this->parameters[$parameter])) + { + return false; + } + + return true; } - return true; + return isset($this->parameters[$parameter]); } /** From 7d63b6411deb336eb6629aae8dffd93a7e62cab5 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Mar 2025 04:26:37 -0500 Subject: [PATCH 383/420] Remove unnecessary exception documentation from isExternal method --- src/Socialbox/Objects/ClientRequest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Socialbox/Objects/ClientRequest.php b/src/Socialbox/Objects/ClientRequest.php index 6f35306..4e9ef07 100644 --- a/src/Socialbox/Objects/ClientRequest.php +++ b/src/Socialbox/Objects/ClientRequest.php @@ -222,7 +222,6 @@ * * @return bool True if the request is external, false otherwise. * @throws DatabaseOperationException Thrown if an error occurs while retrieving the peer. - * @throws StandardRpcException Thrown if the session UUID is invalid. */ public function isExternal(): bool { From 545c8aa3d5f63308e531d8747538ed12e56175ea Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Mar 2025 04:26:43 -0500 Subject: [PATCH 384/420] Add UUID_MISMATCH error case to StandardError enum --- src/Socialbox/Enums/StandardError.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Socialbox/Enums/StandardError.php b/src/Socialbox/Enums/StandardError.php index 0cf213d..9dc896c 100644 --- a/src/Socialbox/Enums/StandardError.php +++ b/src/Socialbox/Enums/StandardError.php @@ -19,6 +19,7 @@ case EXPIRED = -108; case CRYPTOGRAPHIC_ERROR = -109; case UUID_CONFLICT = -110; + case UUID_MISMATCH = -111; // RPC Errors case RPC_METHOD_NOT_FOUND = -1000; From a13703b633b2ddbf246b865aaf9e19fa63fe550c Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Mar 2025 04:26:48 -0500 Subject: [PATCH 385/420] Add encryption_channels_com table for managing encrypted communication messages --- .../database/encryption_channels_com.sql | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/Socialbox/Classes/Resources/database/encryption_channels_com.sql diff --git a/src/Socialbox/Classes/Resources/database/encryption_channels_com.sql b/src/Socialbox/Classes/Resources/database/encryption_channels_com.sql new file mode 100644 index 0000000..6214c22 --- /dev/null +++ b/src/Socialbox/Classes/Resources/database/encryption_channels_com.sql @@ -0,0 +1,26 @@ +create table encryption_channels_com +( + uuid varchar(36) default uuid() not null comment 'The Unique Universal Identifier of the message for the encryption channel', + channel_uuid varchar(36) not null comment 'The UUID of the channel that the message belongs to', + recipient enum ('CALLER', 'RECEIVER') not null comment 'The recipient of the message', + status enum ('SENT', 'RECEIVED', 'REJECTED') default 'SENT' not null comment 'The status of the message, SENT being the default, RECEIVED is when the recipient receives the message successfully and REJECTED is when the message cannot be decrypted, or the checksum failed.', + checksum varchar(64) not null comment 'The SHA512 hash of the decrypted message contents', + data text not null comment 'The data of the message', + timestamp timestamp default current_timestamp() not null comment 'The Timestamp of the message', + primary key (uuid, channel_uuid) comment 'The Unique Primary Index Pair for the channel_uuid and uuid of the message', + constraint encryption_channels_com_uuid_channel_uuid_uindex + unique (uuid, channel_uuid) comment 'The Unique Primary Index Pair for the channel_uuid and uuid of the message', + constraint encryption_channels_com_encryption_channels_uuid_fk + foreign key (channel_uuid) references encryption_channels (uuid) + on update cascade on delete cascade +) + comment 'The table for housing communication messages sent over encryption channels'; + +create index encryption_channels_com_recipient_index + on encryption_channels_com (recipient) + comment 'The index of the recipient column used for indexing'; + +create index encryption_channels_com_timestamp_index + on encryption_channels_com (timestamp) + comment 'The index of the Timestamp column'; + From 865dd966608df75c0cf35f1c5e71691e144e31d3 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Mar 2025 04:26:53 -0500 Subject: [PATCH 386/420] Add encryption_channels_com table for managing encrypted communication messages --- .../database/encryption_channels.sql | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/Socialbox/Classes/Resources/database/encryption_channels.sql diff --git a/src/Socialbox/Classes/Resources/database/encryption_channels.sql b/src/Socialbox/Classes/Resources/database/encryption_channels.sql new file mode 100644 index 0000000..6214c22 --- /dev/null +++ b/src/Socialbox/Classes/Resources/database/encryption_channels.sql @@ -0,0 +1,26 @@ +create table encryption_channels_com +( + uuid varchar(36) default uuid() not null comment 'The Unique Universal Identifier of the message for the encryption channel', + channel_uuid varchar(36) not null comment 'The UUID of the channel that the message belongs to', + recipient enum ('CALLER', 'RECEIVER') not null comment 'The recipient of the message', + status enum ('SENT', 'RECEIVED', 'REJECTED') default 'SENT' not null comment 'The status of the message, SENT being the default, RECEIVED is when the recipient receives the message successfully and REJECTED is when the message cannot be decrypted, or the checksum failed.', + checksum varchar(64) not null comment 'The SHA512 hash of the decrypted message contents', + data text not null comment 'The data of the message', + timestamp timestamp default current_timestamp() not null comment 'The Timestamp of the message', + primary key (uuid, channel_uuid) comment 'The Unique Primary Index Pair for the channel_uuid and uuid of the message', + constraint encryption_channels_com_uuid_channel_uuid_uindex + unique (uuid, channel_uuid) comment 'The Unique Primary Index Pair for the channel_uuid and uuid of the message', + constraint encryption_channels_com_encryption_channels_uuid_fk + foreign key (channel_uuid) references encryption_channels (uuid) + on update cascade on delete cascade +) + comment 'The table for housing communication messages sent over encryption channels'; + +create index encryption_channels_com_recipient_index + on encryption_channels_com (recipient) + comment 'The index of the recipient column used for indexing'; + +create index encryption_channels_com_timestamp_index + on encryption_channels_com (timestamp) + comment 'The index of the Timestamp column'; + From d18b00493dcf75e7707e9a0d1086920bb9ef59a8 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Mar 2025 14:37:07 -0500 Subject: [PATCH 387/420] Minor improvements --- .../Managers/EncryptionChannelManager.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Socialbox/Managers/EncryptionChannelManager.php b/src/Socialbox/Managers/EncryptionChannelManager.php index 8800ce3..272edc4 100644 --- a/src/Socialbox/Managers/EncryptionChannelManager.php +++ b/src/Socialbox/Managers/EncryptionChannelManager.php @@ -74,18 +74,18 @@ * @param string|PeerAddress $callingPeer The peer address of the caller. * @param string|PeerAddress $receivingPeer The peer address of the receiver. * @param string $callingPublicEncryptionKey The public encryption key of the caller. - * @param string|null $uuid The UUID of the channel. If not provided, a new UUID will be generated. + * @param string|null $channelUUid The UUID of the channel. If not provided, a new UUID will be generated. * @return string The UUID of the created channel. * @throws DatabaseOperationException If the database operation fails. */ public static function createChannel(string|PeerAddress $callingPeer, string|PeerAddress $receivingPeer, - string $callingPublicEncryptionKey, ?string $uuid=null): string + string $callingPublicEncryptionKey, ?string $channelUUid=null): string { - if($uuid === null) + if($channelUUid === null) { - $uuid = Uuid::v4()->toRfc4122(); + $channelUUid = Uuid::v4()->toRfc4122(); } - elseif(!Validator::validateUuid($uuid)) + elseif(!Validator::validateUuid($channelUUid)) { throw new InvalidArgumentException('Invalid UUID V4'); } @@ -115,15 +115,15 @@ try { - $uuid = $uuid ?? Uuid::v4()->toRfc4122(); + $channelUUid = $channelUUid ?? Uuid::v4()->toRfc4122(); $stmt = Database::getConnection()->prepare('INSERT INTO encryption_channels (uuid, calling_peer_address, receiving_peer_address, calling_peer_address, calling_public_encryption_key) VALUES (:uuid, :calling_peer_address, :receiving_peer_address, :calling_peer_address, :calling_public_encryption_key)'); - $stmt->bindParam(':uuid', $uuid); + $stmt->bindParam(':uuid', $channelUUid); $stmt->bindParam(':calling_peer_address', $callingPeer); $stmt->bindParam(':receiving_peer_address', $receivingPeer); $stmt->bindParam(':calling_public_encryption_key', $callingPublicEncryptionKey); $stmt->execute(); - return $uuid; + return $channelUUid; } catch (PDOException $e) { @@ -473,7 +473,7 @@ try { - $stmt = Database::getConnection()->prepare('SELECT * FROM encryption_channels_com WHERE channel_uuid=:channel_uuid AND recipient=:recipient ORDER BY timestamp LIMIT 100'); + $stmt = Database::getConnection()->prepare("SELECT * FROM encryption_channels_com WHERE channel_uuid=:channel_uuid AND recipient=:recipient AND status='SENT' ORDER BY timestamp LIMIT 100"); $stmt->bindParam(':channel_uuid', $channelUuid); $stmt->bindParam(':recipient', $recipient); $stmt->execute(); From 4152c064691b9dfd74855ed3aaa04e6176e458c9 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Mar 2025 14:38:20 -0500 Subject: [PATCH 388/420] Added methods to determine certain types --- .../Database/EncryptionChannelRecord.php | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/src/Socialbox/Objects/Database/EncryptionChannelRecord.php b/src/Socialbox/Objects/Database/EncryptionChannelRecord.php index c9440d2..d63d01a 100644 --- a/src/Socialbox/Objects/Database/EncryptionChannelRecord.php +++ b/src/Socialbox/Objects/Database/EncryptionChannelRecord.php @@ -6,8 +6,11 @@ use DateTime; use InvalidArgumentException; use Socialbox\Enums\Status\EncryptionChannelStatus; + use Socialbox\Enums\Types\EncryptionMessageRecipient; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\PeerAddress; + use Socialbox\Objects\Standard\EncryptionChannel; + use Socialbox\Objects\Standard\Peer; class EncryptionChannelRecord implements SerializableInterface { @@ -136,6 +139,110 @@ return $this->created; } + /** + * Checks if the given peer address is a participant in the encryption channel + * + * @param string|PeerAddress $peerAddress The peer address to check + * @return bool True if the peer address is a participant in the encryption channel + */ + public function isParticipant(string|PeerAddress $peerAddress): bool + { + if($peerAddress instanceof PeerAddress) + { + $peerAddress = $peerAddress->getAddress(); + } + + return $this->callingPeerAddress->getAddress() === $peerAddress || $this->receivingPeerAddress->getAddress() === $peerAddress; + } + + /** + * Returns the external peer address of the encryption channel + * + * @return PeerAddress|null The external peer address of the encryption channel, or null if there is none + */ + public function getExternalPeer(): ?PeerAddress + { + if($this->callingPeerAddress->isExternal()) + { + return $this->callingPeerAddress; + } + + if($this->receivingPeerAddress->isExternal()) + { + return $this->receivingPeerAddress; + } + + return null; + } + + /** + * Determines the recipient of the encryption message based on the requester + * + * @param string|PeerAddress $requester The requester of the message + * @return EncryptionMessageRecipient|null The recipient of the message, or null if the requester is not a participant + */ + public function determineRecipient(string|PeerAddress $requester): ?EncryptionMessageRecipient + { + if($requester instanceof PeerAddress) + { + $requester = $requester->getAddress(); + } + + if($this->callingPeerAddress->getAddress() === $requester) + { + return EncryptionMessageRecipient::RECEIVER; + } + elseif($this->receivingPeerAddress->getAddress() === $requester) + { + return EncryptionMessageRecipient::SENDER; + } + + return null; + } + + /** + * Determines the receiver of the encryption message based on the sender + * + * @param string|PeerAddress $sender The sender of the message + * @return PeerAddress|null The receiver of the message, or null if the sender is not a participant + */ + public function determineReceiver(string|PeerAddress $sender): ?PeerAddress + { + if($sender instanceof PeerAddress) + { + $sender = $sender->getAddress(); + } + + if($this->callingPeerAddress->getAddress() === $sender) + { + return $this->receivingPeerAddress; + } + elseif($this->receivingPeerAddress->getAddress() === $sender) + { + return $this->callingPeerAddress; + } + + return null; + } + + /** + * Returns a standard representation object representation of this internal database object + * + * @return EncryptionChannel + */ + public function toStandard(): EncryptionChannel + { + return new EncryptionChannel([ + 'uuid' => $this->uuid, + 'status' => $this->status->value, + 'calling_peer' => $this->callingPeerAddress->getAddress(), + 'calling_public_encryption_key' => $this->callingPublicEncryptionKey, + 'receiving_peer' => $this->receivingPeerAddress->getAddress(), + 'receiving_public_encryption_key' => $this->receivingPublicEncryptionKey, + 'created' => $this->created + ]); + } + /** * @inheritDoc */ From 47ebcb71ae97bdc0a588dc58ed826ea39f9260d1 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Mar 2025 14:38:32 -0500 Subject: [PATCH 389/420] Refactored EncryptionChannelSecret --- .../Client/EncryptionChannelSecret.php | 104 ++++++++---------- 1 file changed, 43 insertions(+), 61 deletions(-) diff --git a/src/Socialbox/Objects/Client/EncryptionChannelSecret.php b/src/Socialbox/Objects/Client/EncryptionChannelSecret.php index 42f10b9..7aacaad 100644 --- a/src/Socialbox/Objects/Client/EncryptionChannelSecret.php +++ b/src/Socialbox/Objects/Client/EncryptionChannelSecret.php @@ -8,12 +8,10 @@ class EncryptionChannelSecret implements SerializableInterface { private string $channelUuid; - private PeerAddress $receiver; - private string $signatureUuid; - private string $publicEncryptionKey; - private string $privateEncryptionKey; - private string $transportEncryptionAlgorithm; - private ?string $transportEncryptionKey; + private PeerAddress $recipient; + private string $localPublicEncryptionKey; + private string $localPrivateEncryptionKey; + private ?string $receivingPublicEncryptionKey; /** * Public constructor @@ -22,19 +20,17 @@ */ public function __construct(array $data) { - $this->channelUuid = $data['uuid']; - $this->receiver = PeerAddress::fromAddress($data['receiver']); - $this->signatureUuid = $data['signature_uuid']; - $this->publicEncryptionKey = $data['public_encryption_key']; - $this->privateEncryptionKey = $data['private_encryption_key']; - $this->transportEncryptionAlgorithm = $data['transport_encryption_algorithm']; - $this->transportEncryptionKey = $data['transport_encryption_key'] ?? null; + $this->channelUuid = $data['channel_uuid']; + $this->recipient = PeerAddress::fromAddress($data['recipient']); + $this->localPublicEncryptionKey = $data['local_public_encryption_key']; + $this->localPrivateEncryptionKey = $data['local_private_encryption_key']; + $this->receivingPublicEncryptionKey = $data['receiving_public_encryption_key'] ?? null; } /** - * Returns the UUID of the key pair + * Returns the channel UUID * - * @return string The UUID of the key pair + * @return string */ public function getChannelUuid(): string { @@ -42,67 +38,55 @@ } /** + * Returns the receiver + * * @return PeerAddress */ - public function getReceiver(): PeerAddress + public function getRecipient(): PeerAddress { - return $this->receiver; + return $this->recipient; } /** - * Returns the UUID of the signature + * Returns the calling public encryption key * - * @return string The UUID of the signature - */ - public function getSignatureUuid(): string - { - return $this->signatureUuid; - } - - /** - * Returns the public key of the key pair - * - * @return string The public key of the key pair - */ - public function getPublicEncryptionKey(): string - { - return $this->publicEncryptionKey; - } - - /** - * Returns the private key of the key pair - * - * @return string The private key of the key pair - */ - public function getPrivateEncryptionKey(): string - { - return $this->privateEncryptionKey; - } - - /** * @return string */ - public function getTransportEncryptionAlgorithm(): string + public function getLocalPublicEncryptionKey(): string { - return $this->transportEncryptionAlgorithm; + return $this->localPublicEncryptionKey; } /** + * Returns the calling private encryption key + * + * @return string + */ + public function getLocalPrivateEncryptionKey(): string + { + return $this->localPrivateEncryptionKey; + } + + /** + * Returns the receiving public encryption key + * * @return string|null */ - public function getTransportEncryptionKey(): ?string + public function getReceivingPublicEncryptionKey(): ?string { - return $this->transportEncryptionKey; + return $this->receivingPublicEncryptionKey; } /** - * @param string|null $transportEncryptionKey + * Sets the receiving public encryption key + * + * @param string $receivingPublicEncryptionKey The receiving public encryption key */ - public function setTransportEncryptionKey(?string $transportEncryptionKey): void + public function setReceivingPublicEncryptionKey(string $receivingPublicEncryptionKey): void { - $this->transportEncryptionKey = $transportEncryptionKey; + $this->receivingPublicEncryptionKey = $receivingPublicEncryptionKey; } - + /** * @inheritDoc */ @@ -117,13 +101,11 @@ public function toArray(): array { return [ - 'uuid' => $this->channelUuid, - 'receiver' => $this->receiver->getAddress(), - 'signature_uuid' => $this->signatureUuid, - 'public_key' => $this->publicEncryptionKey, - 'private_key' => $this->privateEncryptionKey, - 'transport_encryption_algorithm' => $this->transportEncryptionAlgorithm, - 'transport_encryption_key' => $this->transportEncryptionKey + 'channel_uuid' => $this->channelUuid, + 'recipient' => $this->recipient->getAddress(), + 'local_public_encryption_key' => $this->localPublicEncryptionKey, + 'local_private_encryption_key' => $this->localPrivateEncryptionKey, + 'receiving_public_encryption_key' => $this->receivingPublicEncryptionKey ]; } } \ No newline at end of file From 603ee575117d203297c7a5e69a2d9c1bb5ae0d85 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Mar 2025 14:38:42 -0500 Subject: [PATCH 390/420] Added nullable $parameterName for InvalidRpcArgumentException --- .../Standard/InvalidRpcArgumentException.php | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Socialbox/Exceptions/Standard/InvalidRpcArgumentException.php b/src/Socialbox/Exceptions/Standard/InvalidRpcArgumentException.php index f6274a8..9c2c30a 100644 --- a/src/Socialbox/Exceptions/Standard/InvalidRpcArgumentException.php +++ b/src/Socialbox/Exceptions/Standard/InvalidRpcArgumentException.php @@ -11,11 +11,29 @@ /** * Thrown when a required parameter is missing * - * @param string $parameterName The name of the parameter that is missing + * @param string|null $parameterName The name of the parameter that is missing * @param string|Throwable|null $reason The reason why the parameter is invalid can be a string or an exception or null */ - public function __construct(string $parameterName, null|string|Throwable $reason=null) + public function __construct(string|null $parameterName, null|string|Throwable $reason=null) { + if($parameterName === null) + { + if($reason instanceof InvalidArgumentException) + { + parent::__construct(sprintf('Invalid parameter: %s', $reason->getMessage()), StandardError::RPC_INVALID_ARGUMENTS, $reason); + return; + } + + if(is_string($reason)) + { + parent::__construct(sprintf('Invalid parameter: %s', $reason), StandardError::RPC_INVALID_ARGUMENTS); + return; + } + + parent::__construct('Invalid parameter', StandardError::RPC_INVALID_ARGUMENTS); + return; + } + if(is_null($reason)) { parent::__construct(sprintf('Invalid parameter %s', $parameterName), StandardError::RPC_INVALID_ARGUMENTS); From 9eeab95e4d1f71c8accf78967b794f6fe061f0ca Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Mar 2025 14:38:59 -0500 Subject: [PATCH 391/420] Logic Correction from "AND" to "OR" --- src/Socialbox/Objects/Database/PeerDatabaseRecord.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Socialbox/Objects/Database/PeerDatabaseRecord.php b/src/Socialbox/Objects/Database/PeerDatabaseRecord.php index 4611b59..97c61fa 100644 --- a/src/Socialbox/Objects/Database/PeerDatabaseRecord.php +++ b/src/Socialbox/Objects/Database/PeerDatabaseRecord.php @@ -192,7 +192,7 @@ */ public function isExternal(): bool { - return $this->username === 'host' && $this->server !== Configuration::getInstanceConfiguration()->getDomain(); + return $this->username === 'host' || $this->server !== Configuration::getInstanceConfiguration()->getDomain(); } /** From 526dbd8ff1d66a167d706a0ae07f51228e119a6d Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Mar 2025 14:39:20 -0500 Subject: [PATCH 392/420] Refactored parameter names and encryptionChannelSecrets --- src/Socialbox/Classes/RpcClient.php | 32 ++++++++++++++++++----------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/Socialbox/Classes/RpcClient.php b/src/Socialbox/Classes/RpcClient.php index 92d117c..f65c850 100644 --- a/src/Socialbox/Classes/RpcClient.php +++ b/src/Socialbox/Classes/RpcClient.php @@ -40,6 +40,9 @@ private string $sessionUuid; private ?string $defaultSigningKey; private array $signingKeys; + /** + * @var EncryptionChannelSecret[] + */ private array $encryptionChannelSecrets; /** @@ -822,6 +825,11 @@ return $this->encryptionChannelSecrets; } + public function getEncryptionChannelSecret(string $channelUuid): ?EncryptionChannelSecret + { + return $this->encryptionChannelSecrets[$channelUuid] ?? null; + } + /** * Adds a new encryption channel key to the current instance. * @@ -836,45 +844,45 @@ /** * Removes an encryption channel key from the current instance. * - * @param string $uuid The UUID of the encryption channel key to be removed. + * @param string $channelUuid The UUID of the encryption channel key to be removed. * @return void */ - public function removeEncryptionChannelKey(string $uuid): void + public function removeEncryptionChannelKey(string $channelUuid): void { - unset($this->encryptionChannelSecrets[$uuid]); + unset($this->encryptionChannelSecrets[$channelUuid]); } /** * Retrieves the encryption channel key associated with the specified UUID. * - * @param string $uuid The UUID of the encryption channel key to be retrieved. + * @param string $channelUuid The UUID of the encryption channel key to be retrieved. * @return EncryptionChannelSecret|null The encryption channel key associated with the UUID, or null if not found. */ - public function getEncryptionChannelKey(string $uuid): ?EncryptionChannelSecret + public function getEncryptionChannelKey(string $channelUuid): ?EncryptionChannelSecret { - return $this->encryptionChannelSecrets[$uuid] ?? null; + return $this->encryptionChannelSecrets[$channelUuid] ?? null; } /** * Checks if an encryption channel key exists with the specified UUID. * - * @param string $uuid The UUID of the encryption channel key to check. + * @param string $channelUuid The UUID of the encryption channel key to check. * @return bool True if the encryption channel key exists, false otherwise. */ - public function encryptionChannelKeyExists(string $uuid): bool + public function encryptionChannelKeyExists(string $channelUuid): bool { - return isset($this->encryptionChannelSecrets[$uuid]); + return isset($this->encryptionChannelSecrets[$channelUuid]); } /** * Deletes an encryption channel key from the current instance. * - * @param string $uuid The UUID of the encryption channel key to be deleted. + * @param string $channelUuid The UUID of the encryption channel key to be deleted. * @return void */ - public function deleteEncryptionChannelKey(string $uuid): void + public function deleteEncryptionChannelKey(string $channelUuid): void { - unset($this->encryptionChannelSecrets[$uuid]); + unset($this->encryptionChannelSecrets[$channelUuid]); } /** From 0e1201bc86a5c33ee8bb409b6979359d88ff87e6 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Mar 2025 14:40:14 -0500 Subject: [PATCH 393/420] Added EncryptionAcceptChannel --- .../EncryptionAcceptChannel.php | 209 ++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionAcceptChannel.php diff --git a/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionAcceptChannel.php b/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionAcceptChannel.php new file mode 100644 index 0000000..2fb4d83 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionAcceptChannel.php @@ -0,0 +1,209 @@ +isExternal()) + { + return self::handleExternal($request, $rpcRequest); + } + + return self::handleInternal($request, $rpcRequest); + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('An error occurred while checking the request type', StandardError::INTERNAL_SERVER_ERROR, $e); + } + } + + /** + * @param ClientRequest $request + * @param RpcRequest $rpcRequest + * @return SerializableInterface|null + * @throws StandardRpcException + */ + private static function handleInternal(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface + { + if(!$rpcRequest->containsParameter('channel_uuid')) + { + throw new MissingRpcArgumentException('channel_uuid'); + } + elseif(!Validator::validateUuid($rpcRequest->getParameter('channel_uuid'))) + { + throw new InvalidRpcArgumentException('channel_uuid', 'The given channel uuid is not a valid UUID V4'); + } + + if(!$rpcRequest->containsParameter('public_encryption_key')) + { + throw new MissingRpcArgumentException('public_encryption_key'); + } + elseif(!Cryptography::validatePublicEncryptionKey('public_encryption_key')) + { + throw new InvalidRpcArgumentException('public_encryption_key', 'The given public encryption key is invalid'); + } + + try + { + $receivingPeer = $request->getPeer(); + $encryptionChannel = EncryptionChannelManager::getChannel($rpcRequest->getParameter('channel_uuid')); + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('There was an error while trying to obtain the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if($encryptionChannel === null) + { + return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The requested encryption channel was not found'); + } + elseif($encryptionChannel->getReceivingPeerAddress()->getAddress() !== $receivingPeer->getAddress()) + { + return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The requested encryption channel is not accessible'); + } + elseif($encryptionChannel->getStatus() !== EncryptionChannelStatus::AWAITING_RECEIVER) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The encryption channel is not awaiting the receiver'); + } + + if($encryptionChannel->getCallingPeerAddress()->isExternal()) + { + try + { + $rpcClient = Socialbox::getExternalSession($encryptionChannel->getCallingPeerAddress()->getDomain()); + $rpcClient->encryptionAcceptChannel( + channelUuid: $rpcRequest->getParameter('channel_uuid'), + publicEncryptionKey: $rpcRequest->getParameter('public_encryption_key'), + identifiedAs: $receivingPeer->getAddress() + ); + } + catch(Exception $e) + { + try + { + EncryptionChannelManager::declineChannel($rpcRequest->getParameter('channel_uuid'), true); + } + catch(DatabaseOperationException $e) + { + Logger::getLogger()->error('Error declining channel as server', $e); + } + + if($e instanceof RpcException) + { + throw StandardRpcException::fromRpcException($e); + } + + throw new StandardRpcException('There was an error while trying to notify the external server of the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); + } + } + + try + { + EncryptionChannelManager::acceptChannel( + channelUuid: $rpcRequest->getParameter('channel_uuid'), + publicEncryptionKey: $rpcRequest->getParameter('public_encryption_key') + ); + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('There was an error while trying to accept the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse(true); + } + + /** + * @param ClientRequest $request + * @param RpcRequest $rpcRequest + * @return SerializableInterface|null + * @throws StandardRpcException + */ + private static function handleExternal(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface + { + if($request->getIdentifyAs() === null) + { + return $rpcRequest->produceError(StandardError::BAD_REQUEST, 'Missing required header IdentifyAs'); + } + + if(!$rpcRequest->containsParameter('channel_uuid')) + { + throw new MissingRpcArgumentException('channel_uuid'); + } + elseif(!Validator::validateUuid($rpcRequest->getParameter('channel_uuid'))) + { + throw new InvalidRpcArgumentException('channel_uuid', 'The given channel uuid is not a valid UUID V4'); + } + + if(!$rpcRequest->containsParameter('public_encryption_key')) + { + throw new MissingRpcArgumentException('public_encryption_key'); + } + elseif(!Cryptography::validatePublicEncryptionKey('public_encryption_key')) + { + throw new InvalidRpcArgumentException('public_encryption_key', 'The given public encryption key is invalid'); + } + + try + { + $receivingPeer = Socialbox::resolvePeer($request->getIdentifyAs()); + $encryptionChannel = EncryptionChannelManager::getChannel($rpcRequest->getParameter('channel_uuid')); + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('There was an error while trying to obtain the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if($encryptionChannel === null) + { + return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The requested encryption channel was not found'); + } + elseif($encryptionChannel->getReceivingPeerAddress() !== $receivingPeer->getAddress()) + { + return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The requested encryption channel is not accessible'); + } + elseif($encryptionChannel->getStatus() !== EncryptionChannelStatus::AWAITING_RECEIVER) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The encryption channel is not awaiting the receiver'); + } + + try + { + EncryptionChannelManager::acceptChannel( + channelUuid: $rpcRequest->getParameter('channel_uuid'), + publicEncryptionKey: $rpcRequest->getParameter('public_encryption_key') + ); + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('There was an error while trying to accept the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file From fe5909aeae58b62a9580f8581c090b5f69be2729 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Mar 2025 14:41:12 -0500 Subject: [PATCH 394/420] Added EncryptionChannelMessageStatus --- .../Enums/Status/EncryptionChannelMessageStatus.php | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/Socialbox/Enums/Status/EncryptionChannelMessageStatus.php diff --git a/src/Socialbox/Enums/Status/EncryptionChannelMessageStatus.php b/src/Socialbox/Enums/Status/EncryptionChannelMessageStatus.php new file mode 100644 index 0000000..1da9032 --- /dev/null +++ b/src/Socialbox/Enums/Status/EncryptionChannelMessageStatus.php @@ -0,0 +1,10 @@ + Date: Mon, 3 Mar 2025 14:58:37 -0500 Subject: [PATCH 395/420] Implement encryption channel methods and refactor related classes --- .../EncryptionChannelExists.php | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionChannelExists.php diff --git a/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionChannelExists.php b/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionChannelExists.php new file mode 100644 index 0000000..23a5f8e --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionChannelExists.php @@ -0,0 +1,37 @@ +containsParameter('channel_uuid')) + { + throw new MissingRpcArgumentException('channel_uuid'); + } + + try + { + return $rpcRequest->produceResponse(EncryptionChannelManager::channelUuidExists($rpcRequest->getParameter('channel_uuid'))); + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('An error occurred while checking if the channel exists', StandardError::INTERNAL_SERVER_ERROR, $e); + } + } + } \ No newline at end of file From b4cd8092e41b4c2b12dfadb08dc7428b3de1058d Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Mar 2025 14:59:15 -0500 Subject: [PATCH 396/420] Add encryption channel methods and related classes --- .../Managers/EncryptionChannelManager.php | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Socialbox/Managers/EncryptionChannelManager.php b/src/Socialbox/Managers/EncryptionChannelManager.php index 272edc4..847de01 100644 --- a/src/Socialbox/Managers/EncryptionChannelManager.php +++ b/src/Socialbox/Managers/EncryptionChannelManager.php @@ -373,37 +373,37 @@ * @param EncryptionMessageRecipient $recipient The recipient of the message * @param string $checksum The SHA512 checksum of the decrypted data content * @param string $data The encrypted data of the message - * @param string|null $uuid Optional. The UUID of the message, used for server-to-server replication - * @param int|null $timestamp Optional. The Timestamp of the message, used for server-to-server replication + * @param string|null $messageUuid Optional. The UUID of the message, used for server-to-server replication + * @param int|null $messageTimestamp Optional. The Timestamp of the message, used for server-to-server replication * @return string Returns the UUID of the message, if $uuid was provided then it's value is returned. * @throws DatabaseOperationException Thrown if there was a database error while inserting the record */ public static function sendMessage(string $channelUuid, EncryptionMessageRecipient $recipient, string $checksum, string $data, - ?string $uuid=null, ?int $timestamp=null): string + ?string $messageUuid=null, ?int $messageTimestamp=null): string { - if($uuid === null) + if($messageUuid === null) { - $uuid = Uuid::v4()->toRfc4122(); + $messageUuid = Uuid::v4()->toRfc4122(); } - elseif(!Validator::validateUuid($uuid)) + elseif(!Validator::validateUuid($messageUuid)) { throw new InvalidArgumentException('Invalid UUID V4 of the message'); } - if($timestamp === null) + if($messageTimestamp === null) { - $timestamp = time(); + $messageTimestamp = time(); } try { $stmt = Database::getConnection()->prepare('INSERT INTO encryption_channels_com (uuid, channel_uuid, recipient, checksum, data, timestamp) VALUES (:uuid, :channel_uuid, :recipient, :checksum, :data, :timestamp)'); - $stmt->bindParam(':uuid', $uuid); + $stmt->bindParam(':uuid', $messageUuid); $stmt->bindParam(':channel_uuid', $channelUuid); $stmt->bindParam(':recipient', $recipient); $stmt->bindParam(':checksum', $checksum); $stmt->bindParam(':data', $data); - $stmt->bindParam(':timestamp', $timestamp); + $stmt->bindParam(':timestamp', $messageTimestamp); $stmt->execute(); } @@ -412,7 +412,7 @@ throw new DatabaseOperationException('Failed to send data through the encryption channel', $e); } - return $uuid; + return $messageUuid; } /** From f4c3954b065fb01b84e71b522f27169c35c6bfe8 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Mar 2025 14:59:37 -0500 Subject: [PATCH 397/420] Add encryption channel methods and related classes --- .../Objects/Standard/EncryptionChannel.php | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 src/Socialbox/Objects/Standard/EncryptionChannel.php diff --git a/src/Socialbox/Objects/Standard/EncryptionChannel.php b/src/Socialbox/Objects/Standard/EncryptionChannel.php new file mode 100644 index 0000000..63e6028 --- /dev/null +++ b/src/Socialbox/Objects/Standard/EncryptionChannel.php @@ -0,0 +1,119 @@ +uuid = $data['uuid']; + $this->status = EncryptionChannelStatus::from($data['status']); + $this->callingPeer = PeerAddress::fromAddress($data['calling_peer']); + $this->callingPublicEncryptionKey = $data['calling_public_encryption_key']; + $this->receivingPeer = PeerAddress::fromAddress($data['receiving_peer']); + $this->receivingPublicEncryptionKey = $data['receiving_public_encryption_key'] ?? null; + + if(is_int($data['created'])) + { + $this->created = $data['created']; + } + if(is_string($data['created'])) + { + try + { + $this->created = (new DateTime($data['created']))->getTimestamp(); + } + catch (DateMalformedStringException $e) + { + throw new InvalidArgumentException($e->getMessage(), $e->getCode()); + } + } + elseif($data['created'] instanceof DateTime) + { + $this->created = $data['created']->getTimestamp(); + } + else + { + throw new InvalidArgumentException('Invalid created type, got: ' . gettype($data['created'])); + } + } + + public function getUuid(): string + { + return $this->uuid; + } + + public function getStatus(): EncryptionChannelStatus + { + return $this->status; + } + + public function getCallingPeer(): PeerAddress + { + return $this->callingPeer; + } + + public function getCallingPublicEncryptionKey(): string + { + return $this->callingPublicEncryptionKey; + } + + public function getReceivingPeer(): PeerAddress + { + return $this->receivingPeer; + } + + public function getReceivingPublicEncryptionKey(): ?string + { + return $this->receivingPublicEncryptionKey; + } + + public function getCreated(): int + { + return $this->created; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): EncryptionChannel + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'uuid' => $this->uuid, + 'status' => $this->status->value, + 'calling_peer' => $this->callingPeer->getAddress(), + 'calling_public_encryption_key' => $this->callingPublicEncryptionKey, + 'receiving_peer' => $this->receivingPeer->getAddress(), + 'receiving_public_encryption_key' => $this->receivingPublicEncryptionKey, + 'created' => $this->created + ]; + } + } \ No newline at end of file From 49c6a8ebbcdf250a070c22b82d10b2af37623e5a Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Mar 2025 15:03:01 -0500 Subject: [PATCH 398/420] Added EncryptionChannelSend --- .../EncryptionChannelSend.php | 237 ++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionChannelSend.php diff --git a/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionChannelSend.php b/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionChannelSend.php new file mode 100644 index 0000000..14f7a83 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionChannelSend.php @@ -0,0 +1,237 @@ +containsParameter('channel_uuid')) + { + throw new MissingRpcArgumentException('channel_uuid'); + } + elseif(!Validator::validateUuid($rpcRequest->getParameter('channel_uuid'))) + { + throw new InvalidRpcArgumentException('channel_uuid', 'The given channel uuid is not a valid UUID V4'); + } + + try + { + if ($request->isExternal()) + { + return self::executeExternal($request, $rpcRequest); + } + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('An error occurred while checking the request type', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return self::executeInternal($request, $rpcRequest); + } + + /** + * @param ClientRequest $request + * @param RpcRequest $rpcRequest + * @return SerializableInterface + * @throws StandardRpcException + */ + private static function executeInternal(ClientRequest $request, RpcRequest $rpcRequest): SerializableInterface + { + try + { + $channelUuid = $rpcRequest->getParameter('channel_uuid'); + $encryptionChannel = EncryptionChannelManager::getChannel($channelUuid); + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to retrieve the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if($encryptionChannel === null) + { + return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The encryption channel does not exist'); + } + + try + { + $requestingPeer = $request->getPeer(); + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to retrieve the peer', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if($requestingPeer === null) + { + return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The peer is not authorized'); + } + + if(!$encryptionChannel->isParticipant($requestingPeer->getAddress())) + { + return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The encryption channel is not accessible'); + } + elseif($encryptionChannel->getStatus() !== EncryptionChannelStatus::OPENED) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The encryption channel is not opened'); + } + + if(!$rpcRequest->containsParameter('checksum')) + { + throw new MissingRpcArgumentException('checksum'); + } + elseif(!Cryptography::validateSha512($rpcRequest->getParameter('checksum'))) + { + throw new InvalidRpcArgumentException('checksum', 'The given checksum is not a valid SHA-512 checksum'); + } + + if(!$rpcRequest->containsParameter('data')) + { + throw new MissingRpcArgumentException('data'); + } + + try + { + $messageUuid = Uuid::v4()->toRfc4122(); + $messageTimestamp = time(); + + EncryptionChannelManager::sendMessage( + channelUuid: $channelUuid, + recipient: $encryptionChannel->determineRecipient($requestingPeer->getAddress()), + checksum: $rpcRequest->getParameter('checksum'), + data: $rpcRequest->getParameter('data'), + messageUuid: $messageUuid, + messageTimestamp: $messageTimestamp + ); + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to send the message', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if($encryptionChannel->determineReceiver($requestingPeer->getAddress())->isExternal()) + { + try + { + $rpcClient = Socialbox::getExternalSession($encryptionChannel->determineReceiver($requestingPeer->getAddress())->getDomain()); + $rpcClient->encryptionChannelSend( + channelUuid: $rpcRequest->getParameter('channel_uuid'), + checksum: $rpcRequest->getParameter('checksum'), + data: $rpcRequest->getParameter('data'), + identifiedAs: $requestingPeer->getAddress(), + messageUuid: $messageUuid, + timestamp: $messageTimestamp + ); + } + catch(Exception $e) + { + if($e instanceof RpcException) + { + throw StandardRpcException::fromRpcException($e); + } + + throw new StandardRpcException('There was an error while trying to notify the external server of the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); + } + } + + return $rpcRequest->produceResponse(); + } + + /** + * @param ClientRequest $request + * @param RpcRequest $rpcRequest + * @return SerializableInterface + * @throws StandardRpcException + */ + private static function executeExternal(ClientRequest $request, RpcRequest $rpcRequest): SerializableInterface + { + if($request->getIdentifyAs() === null) + { + return $rpcRequest->produceError(StandardError::BAD_REQUEST, 'The IdentifyAs header is required'); + } + + try + { + $channelUuid = $rpcRequest->getParameter('channel_uuid'); + $encryptionChannel = EncryptionChannelManager::getChannel($channelUuid); + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to retrieve the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if($encryptionChannel === null) + { + return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The encryption channel does not exist'); + } + elseif(!$encryptionChannel->isParticipant($request->getIdentifyAs())) + { + return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The encryption channel is not accessible'); + } + + if(!$rpcRequest->containsParameter('checksum')) + { + throw new MissingRpcArgumentException('checksum'); + } + + if(!$rpcRequest->containsParameter('data')) + { + throw new MissingRpcArgumentException('data'); + } + + if(!$rpcRequest->containsParameter('message_uuid')) + { + throw new MissingRpcArgumentException('message_uuid'); + } + + if(!$rpcRequest->containsParameter('timestamp')) + { + throw new MissingRpcArgumentException('timestamp'); + } + elseif(!is_int($rpcRequest->getParameter('timestamp'))) + { + throw new InvalidRpcArgumentException('timestamp', 'The given timestamp must be type integer'); + } + + try + { + EncryptionChannelManager::sendMessage( + channelUuid: $channelUuid, + recipient: $encryptionChannel->determineRecipient($request->getIdentifyAs()), + checksum: $rpcRequest->getParameter('checksum'), + data: $rpcRequest->getParameter('data'), + messageUuid: $rpcRequest->getParameter('message_uuid'), + messageTimestamp: (int)$rpcRequest->getParameter('timestamp') + ); + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to send the message', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file From 02b3f1931c0fff0857a6c7b93ed31ca0b7a42a57 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Mar 2025 15:03:20 -0500 Subject: [PATCH 399/420] Added EncryptionCreateChannel --- .../EncryptionCreateChannel.php | 263 ++++++++++++++++++ src/Socialbox/Enums/StandardMethods.php | 24 +- src/Socialbox/SocialClient.php | 223 ++++++++++++++- 3 files changed, 494 insertions(+), 16 deletions(-) create mode 100644 src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionCreateChannel.php diff --git a/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionCreateChannel.php b/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionCreateChannel.php new file mode 100644 index 0000000..9f5b7af --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionCreateChannel.php @@ -0,0 +1,263 @@ +isExternal()) + { + return self::handleExternal($request, $rpcRequest); + } + + return self::handleInternal($request, $rpcRequest); + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('An error occurred while checking the request type', StandardError::INTERNAL_SERVER_ERROR, $e); + } + } + + /** + * @param ClientRequest $request + * @param RpcRequest $rpcRequest + * @return SerializableInterface|null + * @throws StandardRpcException + */ + private static function handleInternal(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface + { + if(!$rpcRequest->containsParameter('receiving_peer')) + { + throw new MissingRpcArgumentException('receiving_peer'); + } + elseif(!Validator::validatePeerAddress($rpcRequest->getParameter('receiving_peer'))) + { + throw new InvalidRpcArgumentException('receiving_peer', 'Invalid Receiving Peer Address'); + } + + if(!$rpcRequest->containsParameter('public_encryption_key')) + { + throw new MissingRpcArgumentException('public_encryption_key'); + } + elseif(!Cryptography::validatePublicEncryptionKey($rpcRequest->getParameter('public_encryption_key'))) + { + throw new InvalidRpcArgumentException('public_encryption_key', 'The given public encryption key is invalid'); + } + + $receivingPeerAddress = PeerAddress::fromAddress($rpcRequest->getParameter('receiving_peer')); + Socialbox::resolvePeer($receivingPeerAddress); + + try + { + $callingPeer = $request->getPeer(); + $callingPeerAddress = PeerAddress::fromAddress($callingPeer->getAddress()); + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('There was an error while trying to obtain the calling peer', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + try + { + $uuid = EncryptionChannelManager::createChannel( + callingPeer: $callingPeerAddress, + receivingPeer: $receivingPeerAddress, + callingPublicEncryptionKey: $rpcRequest->getParameter('public_encryption_ke') + ); + } + catch(InvalidArgumentException $e) + { + throw new InvalidRpcArgumentException(null, $e); + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('There was an error while trying to create a new encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + // If the receiver is in an external server, we must notify the external server as a client + if($receivingPeerAddress->isExternal()) + { + // Obtain the RPC Client, if for any reason it fails; we set the encryption channel as declined. + try + { + $rpcClient = Socialbox::getExternalSession($receivingPeerAddress->getDomain()); + $externalUuid = $rpcClient->encryptionCreateChannel( + receivingPeer: $receivingPeerAddress, + publicEncryptionKey: $rpcRequest->getParameter('public_encryption_key'), + channelUuid: $uuid, + identifiedAs: $callingPeerAddress + ); + } + catch(Exception $e) + { + try + { + EncryptionChannelManager::declineChannel($uuid, true); + } + catch(DatabaseOperationException $e) + { + Logger::getLogger()->error('Error declining channel as server', $e); + } + + if($e instanceof RpcException) + { + throw StandardRpcException::fromRpcException($e); + } + + throw new StandardRpcException('There was an error while trying to notify the external server of the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + // Check for sanity reasons + if($externalUuid !== $uuid) + { + try + { + EncryptionChannelManager::declineChannel($uuid, true); + } + catch(DatabaseOperationException $e) + { + Logger::getLogger()->error('Error declining channel as server', $e); + } + + throw new StandardRpcException('The external server did not return the correct UUID', StandardError::UUID_MISMATCH); + } + } + + return null; + } + + /** + * @param ClientRequest $request + * @param RpcRequest $rpcRequest + * @return SerializableInterface|null + * @throws StandardRpcException + */ + private static function handleExternal(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface + { + if($request->getIdentifyAs() === null) + { + return $rpcRequest->produceError(StandardError::BAD_REQUEST, 'Missing IdentifyAs request header'); + } + + $callingPeer = $request->getIdentifyAs(); + Socialbox::resolvePeer($callingPeer); + + if(!$rpcRequest->containsParameter('receiving_peer')) + { + throw new MissingRpcArgumentException('receiving_peer'); + } + elseif(!Validator::validatePeerAddress($rpcRequest->getParameter('receiving_peer'))) + { + throw new InvalidRpcArgumentException('receiving_peer', 'Invalid Receiving Peer Address'); + } + + if(!$rpcRequest->containsParameter('public_encryption_key')) + { + throw new MissingRpcArgumentException('public_encryption_key'); + } + elseif(!Cryptography::validatePublicEncryptionKey($rpcRequest->getParameter('public_encryption_key'))) + { + throw new InvalidRpcArgumentException('public_encryption_key', 'The given public encryption key is invalid'); + } + + // Check for an additional required parameter 'channel_uuid' + if(!$rpcRequest->containsParameter('channel_uuid')) + { + throw new MissingRpcArgumentException('channel_uuid'); + } + elseif(!Validator::validateUuid($rpcRequest->getParameter('channel_uuid'))) + { + throw new InvalidRpcArgumentException('channel_uuid', 'The given UUID is not a valid UUID v4 format'); + } + + // Check if the UUID already is used on this server + try + { + if(EncryptionChannelManager::channelUuidExists($rpcRequest->getParameter('channel_uuid'))) + { + return $rpcRequest->produceError(StandardError::UUID_CONFLICT, 'The given UUID already exists with another existing encryption channel on this server'); + } + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('There was an error while checking the existence of the channel UUID', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + $receivingPeerAddress = PeerAddress::fromAddress($rpcRequest->getParameter('receiving_peer')); + if($receivingPeerAddress->isExternal()) + { + return $rpcRequest->produceError(StandardError::PEER_NOT_FOUND, 'The receiving peer does not belong to this server'); + } + + try + { + $receivingPeer = RegisteredPeerManager::getPeerByAddress($rpcRequest->getParameter('receiving_peer')); + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('There was an error while trying to obtain the receiving peer', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if($receivingPeer === null) + { + return $rpcRequest->produceError(StandardError::PEER_NOT_FOUND, 'The receiving peer does not exist on this server'); + } + + try + { + $uuid = EncryptionChannelManager::createChannel( + callingPeer: $callingPeer, + receivingPeer: $receivingPeerAddress, + callingPublicEncryptionKey: $rpcRequest->getParameter('public_encryption_key'), + channelUUid: $rpcRequest->getParameter('channel_uuid') + ); + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('There was an error while trying to create the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if($uuid !== $rpcRequest->getParameter('channel_uuid')) + { + try + { + EncryptionChannelManager::declineChannel($rpcRequest->getParameter('channel_uuid'), true); + } + catch(DatabaseOperationException $e) + { + Logger::getLogger()->error('There was an error while trying to decline the encryption channel as a server', $e); + } + + return $rpcRequest->produceError(StandardError::UUID_MISMATCH, 'The created UUID in the server does not match the UUID that was received'); + } + + return $rpcRequest->produceResponse($uuid); + } + } \ No newline at end of file diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index b07adaf..66eb290 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -18,9 +18,6 @@ use Socialbox\Classes\StandardMethods\Core\ResolvePeer; use Socialbox\Classes\StandardMethods\Core\ResolveSignature; use Socialbox\Classes\StandardMethods\Core\VerifySignature; - use Socialbox\Classes\StandardMethods\Encryption\EncryptionAcceptChannel; - use Socialbox\Classes\StandardMethods\Encryption\EncryptionCloseChannel; - use Socialbox\Classes\StandardMethods\Encryption\EncryptionCreateChannel; use Socialbox\Classes\StandardMethods\ServerDocuments\AcceptCommunityGuidelines; use Socialbox\Classes\StandardMethods\ServerDocuments\AcceptPrivacyPolicy; use Socialbox\Classes\StandardMethods\ServerDocuments\AcceptTermsOfService; @@ -77,8 +74,17 @@ case GET_SESSION_STATE = 'getSessionState'; case PING = 'ping'; case RESOLVE_PEER = 'resolvePeer'; - case RESOLVE_PEER_SIGNATURE = 'resolvePeerSignature'; - case VERIFY_PEER_SIGNATURE = 'verifyPeerSignature'; + case RESOLVE_SIGNATURE = 'resolveSignature'; + case VERIFY_SIGNATURE = 'verifySignature'; + + // Encryption Channel Methods + case ENCRYPTION_ACCEPT_CHANNEL = 'encryptionAcceptChannel'; + case ENCRYPTION_CHANNEL_EXISTS = 'encryptionChannelExists'; + case ENCRYPTION_CHANNEL_SEND = 'encryptionChannelSend'; + case ENCRYPTION_CLOSE_CHANNEL = 'encryptionCloseChannel'; + case ENCRYPTION_CREATE_CHANNEL = 'encryptionCreateChannel'; + case ENCRYPTION_DECLINE_CHANNEL = 'encryptionDeclineChannel'; + case ENCRYPTION_GET_CHANNEL = 'encryptionGetChannel'; // ServerDocument Methods case ACCEPT_COMMUNITY_GUIDELINES = 'acceptCommunityGuidelines'; @@ -184,8 +190,8 @@ self::GET_SESSION_STATE => GetSessionState::execute($request, $rpcRequest), self::PING => Ping::execute($request, $rpcRequest), self::RESOLVE_PEER => ResolvePeer::execute($request, $rpcRequest), - self::RESOLVE_PEER_SIGNATURE => ResolveSignature::execute($request, $rpcRequest), - self::VERIFY_PEER_SIGNATURE => VerifySignature::execute($request, $rpcRequest), + self::RESOLVE_SIGNATURE => ResolveSignature::execute($request, $rpcRequest), + self::VERIFY_SIGNATURE => VerifySignature::execute($request, $rpcRequest), // Server Document Methods self::ACCEPT_PRIVACY_POLICY => AcceptPrivacyPolicy::execute($request, $rpcRequest), @@ -457,8 +463,8 @@ self::GET_SESSION_STATE, self::PING, self::RESOLVE_PEER, - self::RESOLVE_PEER_SIGNATURE, - self::VERIFY_PEER_SIGNATURE + self::RESOLVE_SIGNATURE, + self::VERIFY_SIGNATURE ]; } diff --git a/src/Socialbox/SocialClient.php b/src/Socialbox/SocialClient.php index 516ba87..45474ec 100644 --- a/src/Socialbox/SocialClient.php +++ b/src/Socialbox/SocialClient.php @@ -16,12 +16,14 @@ use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Exceptions\ResolutionException; use Socialbox\Exceptions\RpcException; + use Socialbox\Objects\Client\EncryptionChannelInstance; use Socialbox\Objects\Client\EncryptionChannelSecret; use Socialbox\Objects\Client\ExportedSession; use Socialbox\Objects\Client\SignatureKeyPair; use Socialbox\Objects\PeerAddress; use Socialbox\Objects\RpcRequest; use Socialbox\Objects\Standard\Contact; + use Socialbox\Objects\Standard\EncryptionChannel; use Socialbox\Objects\Standard\ImageCaptchaVerification; use Socialbox\Objects\Standard\InformationFieldState; use Socialbox\Objects\Standard\Peer; @@ -76,6 +78,113 @@ return $uuid; } + /** + * Creates a new encryption channel with the given peer, generates a new encryption key pair and sends the public + * key to the receiving peer. The private key is stored locally and is never sent to the server. + * + * @param PeerAddress|string $receivingPeer The address of the peer to create the channel with + * @return string The UUID of the encryption channel + * @throws CryptographyException Thrown if there was an error while generating the encryption key pair + */ + public function newEncryptionChannel(string|PeerAddress $receivingPeer): string + { + if($receivingPeer instanceof PeerAddress) + { + $receivingPeer = $receivingPeer->getAddress(); + } + + $encryptionKeyPair = Cryptography::generateEncryptionKeyPair(); + $encryptionChannelUuid = $this->encryptionCreateChannel($receivingPeer, $encryptionKeyPair->getPublicKey()); + $this->addEncryptionChannelSecret(new EncryptionChannelSecret([ + 'channel_uuid' => $encryptionChannelUuid, + 'receiver' => $receivingPeer, + 'local_public_encryption_key' => $encryptionKeyPair->getPublicKey(), + 'local_private_encryption_key' => $encryptionKeyPair->getPrivateKey() + ])); + + return $encryptionChannelUuid; + } + + /** + * Waits for the encryption channel to be accepted by the receiving peer, returns True if the channel was accepted + * or False if the channel was not accepted within the timeout period. + * + * @param string $channelUuid + * @param int|null $timeout + * @return bool + */ + public function waitForEncryptionChannel(string $channelUuid, ?int $timeout=30): bool + { + if($this->getEncryptionChannelSecret($channelUuid) === null) + { + throw new InvalidArgumentException('Encryption Channel was not created with newEncryptionChannel() or defined with addEncryptionChannelSecret()'); + } + + $start = time(); + while(true) + { + if($timeout !== null && time() - $start > $timeout) + { + break; + } + + $encryptionChannel = $this->encryptionGetChannel($channelUuid); + if($encryptionChannel->getReceivingPublicEncryptionKey() !== null) + { + $this->getEncryptionChannelSecret($channelUuid)->setReceivingPublicEncryptionKey($encryptionChannel->getReceivingPublicEncryptionKey()); + return true; + } + + sleep(1); + } + + return false; + } + + /** + * Accepts an encryption channel with the given UUID, generates a new encryption key pair and sends the public key + * to the calling peer. The private key is stored locally and is never sent to the server. + * + * @param string $channelUuid The UUID of the encryption channel to accept + * @return bool Returns True if the channel was accepted + * @throws CryptographyException Thrown if there was an error while generating the encryption key pair + * @throws RpcException Thrown if there was an error with the RPC request + */ + public function acceptEncryptionChannel(string $channelUuid): bool + { + $encryptionChannel = $this->encryptionGetChannel($channelUuid); + $encryptionKeyPair = Cryptography::generateEncryptionKeyPair(); + $this->encryptionAcceptChannel($channelUuid, $encryptionKeyPair->getPublicKey(), $encryptionChannel->getRecipient()); + + $this->addEncryptionChannelSecret(new EncryptionChannelSecret([ + 'channel_uuid' => $channelUuid, + 'receiver' => $encryptionChannel->getCallingPeer(), + 'local_public_encryption_key' => $encryptionKeyPair->getPublicKey(), + 'local_private_encryption_key' => $encryptionKeyPair->getPrivateKey(), + 'receiving_public_encryption_key' => $encryptionChannel->getCallingPublicEncryptionKey() + ])); + + return true; + } + + /** + * Creates a new EncryptionChannelInstance object for the given channel UUID. + * + * @param string $channelUuid The UUID of the encryption channel + * @return EncryptionChannelInstance The EncryptionChannelInstance object + * @throws InvalidArgumentException Thrown if the encryption channel secret does not exist + */ + public function createEncryptionChannelInstance(string $channelUuid): EncryptionChannelInstance + { + if($this->getEncryptionChannelSecret($channelUuid) === null) + { + throw new InvalidArgumentException('Encryption Channel was not created with newEncryptionChannel() or defined with addEncryptionChannelSecret()'); + } + + $encryptionChannelSecret = $this->getEncryptionChannelSecret($channelUuid); + return new EncryptionChannelInstance($this, $encryptionChannelSecret); + } + /** * Adds a new peer to the AddressBook, returns True upon success or False if the contact already exists in * the address book. @@ -356,7 +465,7 @@ * @return Signature|null The signature as a Signature object, or null if the signature does not exist * @throws RpcException Thrown if there was an error with the RPC request */ - public function resolvePeerSignature(PeerAddress|string $peer, string $signatureUuid): ?Signature + public function resolveSignature(PeerAddress|string $peer, string $signatureUuid): ?Signature { if($peer instanceof PeerAddress) { @@ -364,7 +473,7 @@ } $result = $this->sendRequest( - new RpcRequest(StandardMethods::RESOLVE_PEER_SIGNATURE, parameters: [ + new RpcRequest(StandardMethods::RESOLVE_SIGNATURE, parameters: [ 'peer' => $peer, 'signature_uuid' => $signatureUuid ]) @@ -381,7 +490,7 @@ /** * Verifies signature authenticity by resolving the signature UUID and comparing the given parameters with the * signature data, returns True if the signature is verified. This is a decentralized method, meaning that any - * signature UUID can be verified for as longas the $peer parameter is the address of the peer that created the + * signature UUID can be verified for as long as the $peer parameter is the address of the peer that created the * signature. * * @param PeerAddress|string $peer The address of the peer to verify the signature for @@ -393,7 +502,7 @@ * @return SignatureVerificationStatus the status of the verification * @throws RpcException Thrown if there was an error with the RPC request */ - public function verifyPeerSignature(PeerAddress|string $peer, string $signatureUuid, string $signaturePublicKey, string $signature, string $sha512, ?int $signatureTime=null): SignatureVerificationStatus + public function verifySignature(PeerAddress|string $peer, string $signatureUuid, string $signaturePublicKey, string $signature, string $sha512, ?int $signatureTime=null): SignatureVerificationStatus { if($peer instanceof PeerAddress) { @@ -401,15 +510,115 @@ } return SignatureVerificationStatus::tryFrom($this->sendRequest( - new RpcRequest(StandardMethods::VERIFY_PEER_SIGNATURE, parameters: [ + new RpcRequest(StandardMethods::VERIFY_SIGNATURE, parameters: [ 'peer' => $peer, 'signature_uuid' => $signatureUuid, - 'signature_public_key' => $signaturePublicKey, 'signature' => $signature, 'sha512' => $sha512, 'signature_time' => $signatureTime ]) - )->getResponse()->getResult()) ?? SignatureVerificationStatus::INVALID; + )->getResponse()->getResult()) ?? SignatureVerificationStatus::ERROR; + } + + public function encryptionAcceptChannel(string $channelUuid, string $publicEncryptionKey, PeerAddress|string|null $identifiedAs=null): bool + { + + if($identifiedAs instanceof PeerAddress) + { + $identifiedAs = $identifiedAs->getAddress(); + } + + return $this->sendRequest( + new RpcRequest(StandardMethods::ENCRYPTION_ACCEPT_CHANNEL, parameters: [ + 'channel_uuid' => $channelUuid, + 'public_encryption_key' => $publicEncryptionKey + ]), true, $identifiedAs + )->getResponse()->getResult(); + } + + public function encryptionChannelExists(string $channelUuid): bool + { + return $this->sendRequest( + new RpcRequest(StandardMethods::ENCRYPTION_CHANNEL_EXISTS, parameters: [ + 'channel_uuid' => $channelUuid + ]) + )->getResponse()->getResult(); + } + + public function encryptionChannelSend(string $channelUuid, string $checksum, string $data, PeerAddress|string|null $identifiedAs=null, ?string $messageUuid=null, ?int $timestamp=null): string + { + if($identifiedAs instanceof PeerAddress) + { + $identifiedAs = $identifiedAs->getAddress(); + } + + return $this->sendRequest( + new RpcRequest(StandardMethods::ENCRYPTION_CHANNEL_SEND, parameters: [ + 'channel_uuid' => $channelUuid, + 'checksum' => $checksum, + 'data' => $data, + 'uuid' => $messageUuid, + 'timestamp' => $timestamp + ]), true, $identifiedAs + )->getResponse()->getResult(); + } + + public function encryptionCloseChannel(string $channelUuid, PeerAddress|string|null $identifiedAs=null): bool + { + if($identifiedAs instanceof PeerAddress) + { + $identifiedAs = $identifiedAs->getAddress(); + } + + return $this->sendRequest( + new RpcRequest(StandardMethods::ENCRYPTION_CLOSE_CHANNEL, parameters: [ + 'channel_uuid' => $channelUuid + ]), true, $identifiedAs + )->getResponse()->getResult(); + } + + public function encryptionCreateChannel(string|PeerAddress $receivingPeer, string $publicEncryptionKey, ?string $channelUuid=null, PeerAddress|string|null $identifiedAs=null): string + { + if($receivingPeer instanceof PeerAddress) + { + $receivingPeer = $receivingPeer->getAddress(); + } + + if($identifiedAs instanceof PeerAddress) + { + $identifiedAs = $identifiedAs->getAddress(); + } + + return $this->sendRequest( + new RpcRequest(StandardMethods::ENCRYPTION_CREATE_CHANNEL, parameters: [ + 'receiving_peer' => $receivingPeer, + 'public_encryption_key' => $publicEncryptionKey, + 'channel_uuid' => $channelUuid + ]), true, $identifiedAs + )->getResponse()->getResult(); + } + + public function encryptionDeclineChannel(string $channelUuid, PeerAddress|string|null $identifiedAs=null): bool + { + if($identifiedAs instanceof PeerAddress) + { + $identifiedAs = $identifiedAs->getAddress(); + } + + return $this->sendRequest( + new RpcRequest(StandardMethods::ENCRYPTION_DECLINE_CHANNEL, parameters: [ + 'channel_uuid' => $channelUuid + ]), true, $identifiedAs + )->getResponse()->getResult(); + } + + public function encryptionGetChannel(string $channelUuid): EncryptionChannel + { + return new EncryptionChannel($this->sendRequest( + new RpcRequest(StandardMethods::ENCRYPTION_GET_CHANNEL, parameters: [ + 'channel_uuid' => $channelUuid + ]) + )->getResponse()->getResult()); } /** From 5a87d5ccd14ad1eb634b4faded8c1a299b1a6fe0 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Mar 2025 15:03:41 -0500 Subject: [PATCH 400/420] Added EncryptionDeclineChannel --- .../EncryptionDeclineChannel.php | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionDeclineChannel.php diff --git a/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionDeclineChannel.php b/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionDeclineChannel.php new file mode 100644 index 0000000..d99c483 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionDeclineChannel.php @@ -0,0 +1,187 @@ +isExternal()) + { + return self::handleExternal($request, $rpcRequest); + } + + return self::handleInternal($request, $rpcRequest); + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('An error occurred while checking the request type', StandardError::INTERNAL_SERVER_ERROR, $e); + } + } + + /** + * @param ClientRequest $request + * @param RpcRequest $rpcRequest + * @return SerializableInterface|null + * @throws StandardRpcException + */ + private static function handleInternal(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface + { + if(!$rpcRequest->containsParameter('channel_uuid')) + { + throw new MissingRpcArgumentException('channel_uuid'); + } + elseif(!Validator::validateUuid($rpcRequest->getParameter('channel_uuid'))) + { + throw new InvalidRpcArgumentException('channel_uuid', 'The given channel uuid is not a valid UUID V4'); + } + + try + { + $receivingPeer = $request->getPeer(); + $encryptionChannel = EncryptionChannelManager::getChannel($rpcRequest->getParameter('channel_uuid')); + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('There was an error while trying to obtain the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if($encryptionChannel === null) + { + return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The requested encryption channel was not found'); + } + elseif($encryptionChannel->getReceivingPeerAddress()->getAddress() !== $receivingPeer->getAddress()) + { + return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The requested encryption channel is not accessible'); + } + elseif($encryptionChannel->getStatus() !== EncryptionChannelStatus::AWAITING_RECEIVER) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The encryption channel is not awaiting the receiver'); + } + + if($encryptionChannel->getCallingPeerAddress()->isExternal()) + { + try + { + $rpcClient = Socialbox::getExternalSession($encryptionChannel->getCallingPeerAddress()->getDomain()); + $rpcClient->encryptionDeclineChannel( + channelUuid: $rpcRequest->getParameter('channel_uuid'), + identifiedAs: $receivingPeer->getAddress() + ); + } + catch(Exception $e) + { + try + { + EncryptionChannelManager::declineChannel($rpcRequest->getParameter('channel_uuid'), true); + } + catch(DatabaseOperationException $e) + { + Logger::getLogger()->error('Error declining channel as server', $e); + } + + if($e instanceof RpcException) + { + throw StandardRpcException::fromRpcException($e); + } + + throw new StandardRpcException('There was an error while trying to notify the external server of the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); + } + } + + try + { + EncryptionChannelManager::declineChannel( + channelUuid: $rpcRequest->getParameter('channel_uuid') + ); + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('There was an error while trying to decline the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse(true); + } + + /** + * @param ClientRequest $request + * @param RpcRequest $rpcRequest + * @return SerializableInterface|null + * @throws StandardRpcException + */ + private static function handleExternal(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface + { + if($request->getIdentifyAs() === null) + { + return $rpcRequest->produceError(StandardError::BAD_REQUEST, 'Missing required header IdentifyAs'); + } + + if(!$rpcRequest->containsParameter('channel_uuid')) + { + throw new MissingRpcArgumentException('channel_uuid'); + } + elseif(!Validator::validateUuid($rpcRequest->getParameter('channel_uuid'))) + { + throw new InvalidRpcArgumentException('channel_uuid', 'The given channel uuid is not a valid UUID V4'); + } + + try + { + $receivingPeer = Socialbox::resolvePeer($request->getIdentifyAs()); + $encryptionChannel = EncryptionChannelManager::getChannel($rpcRequest->getParameter('channel_uuid')); + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('There was an error while trying to obtain the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if($encryptionChannel === null) + { + return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The requested encryption channel was not found'); + } + elseif($encryptionChannel->getReceivingPeerAddress() !== $receivingPeer->getAddress()) + { + return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The requested encryption channel is not accessible'); + } + elseif($encryptionChannel->getStatus() !== EncryptionChannelStatus::AWAITING_RECEIVER) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The encryption channel is not awaiting the receiver'); + } + + try + { + EncryptionChannelManager::declineChannel( + channelUuid: $rpcRequest->getParameter('channel_uuid') + ); + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('There was an error while trying to decline the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file From 0643f3edfde45164a7994f9c890a136c47ea0314 Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Mar 2025 15:04:11 -0500 Subject: [PATCH 401/420] Added EncryptionGetChannel --- .../EncryptionGetChannel.php | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionGetChannel.php diff --git a/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionGetChannel.php b/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionGetChannel.php new file mode 100644 index 0000000..ccae0eb --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionGetChannel.php @@ -0,0 +1,55 @@ +containsParameter('channel_uuid')) + { + throw new MissingRpcArgumentException('channel_uuid'); + } + elseif(!Validator::validateUuid($rpcRequest->getParameter('channel_uuid'))) + { + throw new InvalidRpcArgumentException('channel_uuid', 'The given channel uuid is not a valid UUID V4'); + } + + try + { + $requestingPeer = $request->getPeer(); + $encryptionChannel = EncryptionChannelManager::getChannel($rpcRequest->getParameter('channel_uuid')); + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('There was an error while trying to obtain the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if($encryptionChannel === null) + { + return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The requested encryption channel was not found'); + } + elseif(!$encryptionChannel->isParticipant($requestingPeer->getAddress())) + { + return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The requested encryption channel is not accessible'); + } + + return $rpcRequest->produceResponse($encryptionChannel->toStandard()); + } + } \ No newline at end of file From 371ebfec1a3ff1af7f185527c61169b461e0b89c Mon Sep 17 00:00:00 2001 From: netkas Date: Mon, 3 Mar 2025 15:35:24 -0500 Subject: [PATCH 402/420] Correction --- .../Database/EncryptionChannelRecord.php | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Socialbox/Objects/Database/EncryptionChannelRecord.php b/src/Socialbox/Objects/Database/EncryptionChannelRecord.php index d63d01a..5406cc3 100644 --- a/src/Socialbox/Objects/Database/EncryptionChannelRecord.php +++ b/src/Socialbox/Objects/Database/EncryptionChannelRecord.php @@ -181,13 +181,27 @@ * @param string|PeerAddress $requester The requester of the message * @return EncryptionMessageRecipient|null The recipient of the message, or null if the requester is not a participant */ - public function determineRecipient(string|PeerAddress $requester): ?EncryptionMessageRecipient + public function determineRecipient(string|PeerAddress $requester, bool $reverse=false): ?EncryptionMessageRecipient { if($requester instanceof PeerAddress) { $requester = $requester->getAddress(); } + if($reverse) + { + if($this->callingPeerAddress->getAddress() === $requester) + { + return EncryptionMessageRecipient::SENDER; + } + elseif($this->receivingPeerAddress->getAddress() === $requester) + { + return EncryptionMessageRecipient::RECEIVER; + } + + return null; + } + if($this->callingPeerAddress->getAddress() === $requester) { return EncryptionMessageRecipient::RECEIVER; @@ -206,7 +220,7 @@ * @param string|PeerAddress $sender The sender of the message * @return PeerAddress|null The receiver of the message, or null if the sender is not a participant */ - public function determineReceiver(string|PeerAddress $sender): ?PeerAddress + public function determineReceiver(string|PeerAddress $sender): ?PeerAddress { if($sender instanceof PeerAddress) { From 8dde6fed133e30909d2e3d471b546311d2826f4b Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 4 Mar 2025 13:41:00 -0500 Subject: [PATCH 403/420] Added EncryptionChannelAcknowledge method for handling message acknowledgments in encryption channels --- .../EncryptionChannelAcknowledgeMessage.php | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionChannelAcknowledgeMessage.php diff --git a/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionChannelAcknowledgeMessage.php b/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionChannelAcknowledgeMessage.php new file mode 100644 index 0000000..8deb288 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionChannelAcknowledgeMessage.php @@ -0,0 +1,134 @@ +containsParameter('channel_uuid')) + { + throw new MissingRpcArgumentException('channel_uuid'); + } + elseif(!Validator::validateUuid($rpcRequest->getParameter('channel_uuid'))) + { + throw new InvalidRpcArgumentException('channel_uuid', 'The given channel uuid is not a valid UUID V4'); + } + + try + { + $channelUuid = $rpcRequest->getParameter('channel_uuid'); + $encryptionChannel = EncryptionChannelManager::getChannel($channelUuid); + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to retrieve the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if($encryptionChannel === null) + { + return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The encryption channel does not exist'); + } + + try + { + $requestingPeer = $request->getPeer(); + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to retrieve the peer', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if($requestingPeer === null) + { + return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The peer is not authorized'); + } + + if(!$encryptionChannel->isParticipant($requestingPeer->getAddress())) + { + return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The encryption channel is not accessible'); + } + elseif($encryptionChannel->getStatus() !== EncryptionChannelStatus::OPENED) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The encryption channel is not opened'); + } + + if(!$rpcRequest->containsParameter('message_uuid')) + { + throw new MissingRpcArgumentException('message_uuid'); + } + + if(is_array($rpcRequest->getParameter('message_uuid'))) + { + return self::handleMultipleMessages($rpcRequest, $encryptionChannel); + } + elseif(is_string($rpcRequest->getParameter('message_uuid'))) + { + return self::handleSingleMessage($rpcRequest, $encryptionChannel); + } + + return $rpcRequest->produceError(StandardError::BAD_REQUEST, 'The message_uuid parameter must be a string or an array of strings'); + } + + private static function handleSingleMessage(RpcRequest $rpcRequest, EncryptionChannelRecord $encryptionChannel) + { + if(!Validator::validateUuid($rpcRequest->getParameter('message_uuid'))) + { + throw new InvalidRpcArgumentException('message_uuid', 'The given message uuid is not a valid UUID V4'); + } + + try + { + EncryptionChannelManager::acknowledgeMessage($encryptionChannel->getUuid(), $rpcRequest->getParameter('message_uuid')); + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to acknowledge the message', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse(true); + } + + private static function handleMultipleMessages(RpcRequest $rpcRequest, EncryptionChannelRecord $encryptionChannel) + { + $messageUuids = $rpcRequest->getParameter('message_uuid'); + + foreach($messageUuids as $messageUuid) + { + if(!Validator::validateUuid($messageUuid)) + { + return $rpcRequest->produceError(StandardError::BAD_REQUEST, sprintf('The message uuid %s is not a valid UUID V4', $messageUuid)); + } + } + + try + { + EncryptionChannelManager::acknowledgeMessagesBatch($encryptionChannel->getUuid(), $messageUuids); + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to acknowledge the messages', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file From e142add843174f2c76790e9e783cdd3a01df380a Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 4 Mar 2025 13:41:07 -0500 Subject: [PATCH 404/420] Fix incorrect method call for retrieving peer address in EncryptionAcceptChannel --- .../EncryptionChannel/EncryptionAcceptChannel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionAcceptChannel.php b/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionAcceptChannel.php index 2fb4d83..7972aed 100644 --- a/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionAcceptChannel.php +++ b/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionAcceptChannel.php @@ -183,7 +183,7 @@ { return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The requested encryption channel was not found'); } - elseif($encryptionChannel->getReceivingPeerAddress() !== $receivingPeer->getAddress()) + elseif($encryptionChannel->getReceivingPeerAddress() !== $receivingPeer->getPeerAddress()) { return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The requested encryption channel is not accessible'); } From 23d08f45eefb8b70e87c9782b3bb9563bb1bc508 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 4 Mar 2025 13:41:12 -0500 Subject: [PATCH 405/420] Enhance EncryptionChannelAcknowledgeMessage to validate message_uuid and handle external acknowledgments --- .../EncryptionChannelAcknowledgeMessage.php | 179 +++++++++++++----- 1 file changed, 133 insertions(+), 46 deletions(-) diff --git a/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionChannelAcknowledgeMessage.php b/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionChannelAcknowledgeMessage.php index 8deb288..66c9194 100644 --- a/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionChannelAcknowledgeMessage.php +++ b/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionChannelAcknowledgeMessage.php @@ -2,11 +2,14 @@ namespace Socialbox\Classes\StandardMethods\EncryptionChannel; + use Exception; use Socialbox\Abstracts\Method; + use Socialbox\Classes\Logger; use Socialbox\Classes\Validator; use Socialbox\Enums\StandardError; use Socialbox\Enums\Status\EncryptionChannelStatus; use Socialbox\Exceptions\DatabaseOperationException; + use Socialbox\Exceptions\RpcException; use Socialbox\Exceptions\Standard\InvalidRpcArgumentException; use Socialbox\Exceptions\Standard\MissingRpcArgumentException; use Socialbox\Exceptions\Standard\StandardRpcException; @@ -15,8 +18,9 @@ use Socialbox\Objects\ClientRequest; use Socialbox\Objects\Database\EncryptionChannelRecord; use Socialbox\Objects\RpcRequest; + use Socialbox\Socialbox; - class EncryptionChannelAcknowledge extends Method + class EncryptionChannelAcknowledgeMessage extends Method { /** @@ -33,6 +37,19 @@ throw new InvalidRpcArgumentException('channel_uuid', 'The given channel uuid is not a valid UUID V4'); } + if(!$rpcRequest->containsParameter('message_uuid')) + { + throw new MissingRpcArgumentException('message_uuid'); + } + elseif(!is_string($rpcRequest->getParameter('message_uuid'))) + { + throw new InvalidRpcArgumentException('message_uuid', 'Must be type string'); + } + elseif(!Validator::validateUuid($rpcRequest->getParameter('message_uuid'))) + { + throw new InvalidRpcArgumentException('message_uuid', 'Invalid message UUID V4'); + } + try { $channelUuid = $rpcRequest->getParameter('channel_uuid'); @@ -48,6 +65,80 @@ return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The encryption channel does not exist'); } + try + { + if ($request->isExternal()) + { + return self::handleExternal($request, $rpcRequest, $encryptionChannel); + } + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to acknowledge the message', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return self::handleInternal($request, $rpcRequest, $encryptionChannel); + } + + /** + * Handles the external execution of the method. + * + * @param ClientRequest $request The client request instance. + * @param RpcRequest $rpcRequest The RPC request instance. + * @param EncryptionChannelRecord $encryptionChannel The encryption channel record. + * @return SerializableInterface|null The response to the request. + * @throws StandardRpcException If an error occurs. + */ + public static function handleExternal(ClientRequest $request, RpcRequest $rpcRequest, EncryptionChannelRecord $encryptionChannel): ?SerializableInterface + { + if($request->getIdentifyAs() === null) + { + return $rpcRequest->produceError(StandardError::BAD_REQUEST, 'The IdentifyAs header is missing'); + } + + $requestingPeerAddress = $request->getIdentifyAs(); + if(!$encryptionChannel->isParticipant($requestingPeerAddress)) + { + return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The encryption channel is not accessible'); + } + + try + { + $message = EncryptionChannelManager::getMessageRecord($rpcRequest->getParameter('channel_uuid'), $rpcRequest->getParameter('message_uuid')); + + if($message === null) + { + return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The message does not exist'); + } + + if($message->getReceiver($encryptionChannel)->getAddress() !== $requestingPeerAddress) + { + return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The message is not for the requesting peer'); + } + + EncryptionChannelManager::acknowledgeMessage( + $rpcRequest->getParameter('channel_uuid'), $rpcRequest->getParameter('message_uuid') + ); + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to acknowledge the message', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse(true); + } + + /** + * Handles the internal execution of the method. + * + * @param ClientRequest $request The client request instance. + * @param RpcRequest $rpcRequest The RPC request instance. + * @param EncryptionChannelRecord $encryptionChannel The encryption channel record. + * @return SerializableInterface|null The response to the request. + * @throws StandardRpcException If an error occurs. + */ + public static function handleInternal(ClientRequest $request, RpcRequest $rpcRequest, EncryptionChannelRecord $encryptionChannel): ?SerializableInterface + { try { $requestingPeer = $request->getPeer(); @@ -71,64 +162,60 @@ return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The encryption channel is not opened'); } - if(!$rpcRequest->containsParameter('message_uuid')) - { - throw new MissingRpcArgumentException('message_uuid'); - } - - if(is_array($rpcRequest->getParameter('message_uuid'))) - { - return self::handleMultipleMessages($rpcRequest, $encryptionChannel); - } - elseif(is_string($rpcRequest->getParameter('message_uuid'))) - { - return self::handleSingleMessage($rpcRequest, $encryptionChannel); - } - - return $rpcRequest->produceError(StandardError::BAD_REQUEST, 'The message_uuid parameter must be a string or an array of strings'); - } - - private static function handleSingleMessage(RpcRequest $rpcRequest, EncryptionChannelRecord $encryptionChannel) - { - if(!Validator::validateUuid($rpcRequest->getParameter('message_uuid'))) - { - throw new InvalidRpcArgumentException('message_uuid', 'The given message uuid is not a valid UUID V4'); - } - try { - EncryptionChannelManager::acknowledgeMessage($encryptionChannel->getUuid(), $rpcRequest->getParameter('message_uuid')); + $message = EncryptionChannelManager::getMessageRecord($rpcRequest->getParameter('channel_uuid'), $rpcRequest->getParameter('message_uuid')); + + if($message === null) + { + return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The message does not exist'); + } + + if($message->getReceiver($encryptionChannel)->getAddress() !== $requestingPeer->getAddress()) + { + return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The message is not for the requesting peer'); + } + + EncryptionChannelManager::acknowledgeMessage( + $rpcRequest->getParameter('channel_uuid'), $rpcRequest->getParameter('message_uuid') + ); } catch(DatabaseOperationException $e) { throw new StandardRpcException('Failed to acknowledge the message', StandardError::INTERNAL_SERVER_ERROR, $e); } - return $rpcRequest->produceResponse(true); - } - - private static function handleMultipleMessages(RpcRequest $rpcRequest, EncryptionChannelRecord $encryptionChannel) - { - $messageUuids = $rpcRequest->getParameter('message_uuid'); - - foreach($messageUuids as $messageUuid) + if($message->getOwner($encryptionChannel)->isExternal()) { - if(!Validator::validateUuid($messageUuid)) + try { - return $rpcRequest->produceError(StandardError::BAD_REQUEST, sprintf('The message uuid %s is not a valid UUID V4', $messageUuid)); + $rpcClient = Socialbox::getExternalSession($message->getOwner($encryptionChannel)->getDomain()); + $rpcClient->encryptionChannelAcknowledgeMessage( + channelUuid: $rpcRequest->getParameter('channel_uuid'), + messageUuid: $rpcRequest->getParameter('message_uuid'), + identifiedAs: $requestingPeer->getAddress() + ); + } + catch(Exception $e) + { + try + { + EncryptionChannelManager::rejectMessage($rpcRequest->getParameter('channel_uuid'), $rpcRequest->getParameter('message_uuid'), true); + } + catch (DatabaseOperationException $e) + { + Logger::getLogger()->error('Error rejecting message as server', $e); + } + + if($e instanceof RpcException) + { + throw StandardRpcException::fromRpcException($e); + } + + throw new StandardRpcException('Failed to acknowledge the message with the external server', StandardError::INTERNAL_SERVER_ERROR, $e); } } - try - { - EncryptionChannelManager::acknowledgeMessagesBatch($encryptionChannel->getUuid(), $messageUuids); - } - catch(DatabaseOperationException $e) - { - throw new StandardRpcException('Failed to acknowledge the messages', StandardError::INTERNAL_SERVER_ERROR, $e); - } - - return $rpcRequest->produceResponse(true); } } \ No newline at end of file From 3a5bf57ec1e22919ad5338601aac6cf18fe81076 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 4 Mar 2025 13:41:17 -0500 Subject: [PATCH 406/420] Add EncryptionChannelReceive class for handling incoming messages in encryption channels --- .../EncryptionChannelReceive.php | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionChannelReceive.php diff --git a/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionChannelReceive.php b/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionChannelReceive.php new file mode 100644 index 0000000..fa0db4e --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionChannelReceive.php @@ -0,0 +1,98 @@ +containsParameter('channel_uuid')) + { + throw new MissingRpcArgumentException('channel_uuid'); + } + elseif(!Validator::validateUuid($rpcRequest->getParameter('channel_uuid'))) + { + throw new InvalidRpcArgumentException('channel_uuid', 'The given channel uuid is not a valid UUID V4'); + } + + try + { + $channelUuid = $rpcRequest->getParameter('channel_uuid'); + $encryptionChannel = EncryptionChannelManager::getChannel($channelUuid); + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to retrieve the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if($encryptionChannel === null) + { + return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The encryption channel does not exist'); + } + + try + { + $requestingPeer = $request->getPeer(); + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to retrieve the peer', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if($requestingPeer === null) + { + return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The peer is not authorized'); + } + + if(!$encryptionChannel->isParticipant($requestingPeer->getAddress())) + { + return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The encryption channel is not accessible'); + } + elseif($encryptionChannel->getStatus() !== EncryptionChannelStatus::OPENED) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The encryption channel is not opened'); + } + + $acknowledge = false; + if($rpcRequest->containsParameter('acknowledge') && is_bool($rpcRequest->getParameter('acknowledge')) && $rpcRequest->getParameter('acknowledge')) + { + $acknowledge = true; + } + + try + { + + $messages = EncryptionChannelManager::receiveData( + $rpcRequest->getParameter('channel_uuid'), $encryptionChannel->determineRecipient($requestingPeer->getAddress(), true) + ); + + if($acknowledge) + { + EncryptionChannelManager::acknowledgeMessagesBatch($rpcRequest->getParameter('channel_uuid'), array_map(fn($message) => $message->getUuid(), $messages)); + } + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to retrieve the messages', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse(array_map(fn($message) => $message->toStandard(), $messages)); + } + } \ No newline at end of file From 90324072b1c04e5052d3cbaaa268ea1022438366 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 4 Mar 2025 13:41:22 -0500 Subject: [PATCH 407/420] Fix incorrect method call for retrieving peer address in EncryptionDeclineChannel --- .../EncryptionChannel/EncryptionDeclineChannel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionDeclineChannel.php b/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionDeclineChannel.php index d99c483..a93b3fa 100644 --- a/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionDeclineChannel.php +++ b/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionDeclineChannel.php @@ -162,7 +162,7 @@ { return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The requested encryption channel was not found'); } - elseif($encryptionChannel->getReceivingPeerAddress() !== $receivingPeer->getAddress()) + elseif($encryptionChannel->getReceivingPeerAddress() !== $receivingPeer->getPeerAddress()) { return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The requested encryption channel is not accessible'); } From a0dcec9733ed96e2686de9783149e9053c0c320d Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 4 Mar 2025 13:41:26 -0500 Subject: [PATCH 408/420] Add ENCRYPTION_CHANNEL_ACKNOWLEDGE_MESSAGE case to StandardMethods enum --- src/Socialbox/Enums/StandardMethods.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Socialbox/Enums/StandardMethods.php b/src/Socialbox/Enums/StandardMethods.php index 66eb290..2bdc72f 100644 --- a/src/Socialbox/Enums/StandardMethods.php +++ b/src/Socialbox/Enums/StandardMethods.php @@ -79,6 +79,7 @@ // Encryption Channel Methods case ENCRYPTION_ACCEPT_CHANNEL = 'encryptionAcceptChannel'; + case ENCRYPTION_CHANNEL_ACKNOWLEDGE_MESSAGE = 'encryptionChannelAcknowledgeMessage'; case ENCRYPTION_CHANNEL_EXISTS = 'encryptionChannelExists'; case ENCRYPTION_CHANNEL_SEND = 'encryptionChannelSend'; case ENCRYPTION_CLOSE_CHANNEL = 'encryptionCloseChannel'; From 8663de6e6146bb67c48240a15f5d01f7c668db95 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 4 Mar 2025 13:41:32 -0500 Subject: [PATCH 409/420] Add method to acknowledge a batch of messages in EncryptionChannelManager --- .../Managers/EncryptionChannelManager.php | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/Socialbox/Managers/EncryptionChannelManager.php b/src/Socialbox/Managers/EncryptionChannelManager.php index 847de01..9eabc86 100644 --- a/src/Socialbox/Managers/EncryptionChannelManager.php +++ b/src/Socialbox/Managers/EncryptionChannelManager.php @@ -525,6 +525,46 @@ } } + /** + * Acknowledges a batch of messages as received + * + * @param string $channelUuid The Unique Universal Identifier of the channel + * @param array $messageUuids An array of message UUIDs to acknowledge + * @return void + * @throws DatabaseOperationException Thrown if there was an error with the database operation + */ + public static function acknowledgeMessagesBatch(string $channelUuid, array $messageUuids): void + { + if(!Validator::validateUuid($channelUuid)) + { + throw new InvalidArgumentException('The given Channel UUID is not a valid V4 UUID'); + } + + if(empty($messageUuids)) + { + throw new InvalidArgumentException('The given Message UUIDs array is empty'); + } + + $placeholders = implode(',', array_fill(0, count($messageUuids), '?')); + $query = "UPDATE encryption_channels_com SET status='RECEIVED' WHERE channel_uuid=:channel_uuid AND uuid IN ($placeholders)"; + + try + { + $stmt = Database::getConnection()->prepare($query); + $stmt->bindParam(':channel_uuid', $channelUuid); + foreach($messageUuids as $index => $messageUuid) + { + $stmt->bindValue($index + 1, $messageUuid); + } + + $stmt->execute(); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('There was an error while acknowledging the message records', $e); + } + } + /** * Rejects the requested message * From 8a843eb60b6d7ed2896f0d859a44bad98253ba9f Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 4 Mar 2025 13:41:36 -0500 Subject: [PATCH 410/420] Refactor RegisteredPeerManager to use getPeerAddress() for domain and username retrieval --- src/Socialbox/Managers/RegisteredPeerManager.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Socialbox/Managers/RegisteredPeerManager.php b/src/Socialbox/Managers/RegisteredPeerManager.php index 91622fa..c7cc831 100644 --- a/src/Socialbox/Managers/RegisteredPeerManager.php +++ b/src/Socialbox/Managers/RegisteredPeerManager.php @@ -202,17 +202,17 @@ */ public static function synchronizeExternalPeer(Peer $peer): void { - if($peer->getAddress()->getDomain() === Configuration::getInstanceConfiguration()->getDomain()) + if($peer->getPeerAddress()->getDomain() === Configuration::getInstanceConfiguration()->getDomain()) { throw new InvalidArgumentException('Given peer is not an external peer'); } - if($peer->getAddress()->getUsername() === ReservedUsernames::HOST->value) + if($peer->getPeerAddress()->getUsername() === ReservedUsernames::HOST->value) { throw new InvalidArgumentException('Cannot synchronize an external host peer'); } - $existingPeer = self::getPeerByAddress($peer->getAddress()); + $existingPeer = self::getPeerByAddress($peer->getPeerAddress()); if($existingPeer !== null) { // getUpdated is DateTime() if it's older than 1 hour, update it @@ -263,9 +263,9 @@ { $statement = Database::getConnection()->prepare('INSERT INTO peers (uuid, username, server, enabled) VALUES (:uuid, :username, :server, 1)'); $statement->bindParam(':uuid', $uuid); - $username = $peer->getAddress()->getUsername(); + $username = $peer->getPeerAddress()->getUsername(); $statement->bindParam(':username', $username); - $server = $peer->getAddress()->getDomain(); + $server = $peer->getPeerAddress()->getDomain(); $statement->bindParam(':server', $server); $statement->execute(); From 4a37844920b49c7755412af56314426b57021059 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 4 Mar 2025 13:41:39 -0500 Subject: [PATCH 411/420] Add EncryptionChannelInstance class for managing encryption channel messages --- .../Client/EncryptionChannelInstance.php | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/Socialbox/Objects/Client/EncryptionChannelInstance.php diff --git a/src/Socialbox/Objects/Client/EncryptionChannelInstance.php b/src/Socialbox/Objects/Client/EncryptionChannelInstance.php new file mode 100644 index 0000000..7457dc1 --- /dev/null +++ b/src/Socialbox/Objects/Client/EncryptionChannelInstance.php @@ -0,0 +1,26 @@ +client = $client; + } + + public function sendMessage(string $message): void + { + $this->client->sendMessage($message); + } + } \ No newline at end of file From 172398307227680161293629dcc13b79e6c775a1 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 4 Mar 2025 13:41:44 -0500 Subject: [PATCH 412/420] Add getSharedSecret method for Diffie-Hellman key exchange in EncryptionChannelSecret --- .../Objects/Client/EncryptionChannelSecret.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Socialbox/Objects/Client/EncryptionChannelSecret.php b/src/Socialbox/Objects/Client/EncryptionChannelSecret.php index 7aacaad..d631910 100644 --- a/src/Socialbox/Objects/Client/EncryptionChannelSecret.php +++ b/src/Socialbox/Objects/Client/EncryptionChannelSecret.php @@ -2,6 +2,8 @@ namespace Socialbox\Objects\Client; + use Socialbox\Classes\Cryptography; + use Socialbox\Exceptions\CryptographyException; use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\PeerAddress; @@ -87,6 +89,22 @@ $this->receivingPublicEncryptionKey = $receivingPublicEncryptionKey; } + /** + * Preform a Diffie-Hellman Exchange to get the shared secret between the two peers + * + * @return string The shared secret + * @throws CryptographyException If the receiving public encryption key is not set + */ + public function getSharedSecret(): string + { + if($this->receivingPublicEncryptionKey === null) + { + throw new CryptographyException('The receiving public encryption key is not set'); + } + + return Cryptography::performDHE($this->receivingPublicEncryptionKey, $this->localPrivateEncryptionKey); + } + /** * @inheritDoc */ From 912de28871aab1616f55049aac3b615b4d3b65ff Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 4 Mar 2025 13:41:48 -0500 Subject: [PATCH 413/420] Add methods to retrieve message owner and receiver, and convert record to standard message --- .../EncryptionChannelMessageRecord.php | 70 ++++++++++++++----- 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/src/Socialbox/Objects/Database/EncryptionChannelMessageRecord.php b/src/Socialbox/Objects/Database/EncryptionChannelMessageRecord.php index c5e04fb..0949ec0 100644 --- a/src/Socialbox/Objects/Database/EncryptionChannelMessageRecord.php +++ b/src/Socialbox/Objects/Database/EncryptionChannelMessageRecord.php @@ -8,6 +8,8 @@ use Socialbox\Enums\Status\EncryptionChannelMessageStatus; use Socialbox\Enums\Types\EncryptionMessageRecipient; use Socialbox\Interfaces\SerializableInterface; + use Socialbox\Objects\PeerAddress; + use Socialbox\Objects\Standard\EncryptionChannelMessage; class EncryptionChannelMessageRecord implements SerializableInterface { @@ -98,24 +100,7 @@ return $this->status; } - /**try { - switch($fieldName) { - case InformationFieldName::DISPLAY_NAME: - SessionManager::updateFlow($request->getSession(), [SessionFlags::SET_DISPLAY_NAME]); - break; - // Other cases... - } -} catch (Exception $e) { - try { - PeerInformationManager::deleteProperty($peer, $fieldName); - } catch (DatabaseOperationException $e) { - throw new StandardException('Failed to rollback the information field', StandardError::INTERNAL_SERVER_ERROR, $e); - } - if($e instanceof StandardException) { - throw $e; - } - throw new StandardException('Failed to update the session flow', StandardError::INTERNAL_SERVER_ERROR, $e); -} + /** * Returns the SHA512 hash of the decrypted content * * @return string The SHA512 hash of the decrypted content @@ -145,6 +130,55 @@ return $this->timestamp; } + /** + * Returns the owner of the message + * + * @param EncryptionChannelRecord $channelRecord The channel record to use + * @return PeerAddress The owner of the message + */ + public function getOwner(EncryptionChannelRecord $channelRecord): PeerAddress + { + if($this->recipient === EncryptionMessageRecipient::SENDER) + { + return $channelRecord->getCallingPeerAddress(); + } + + return $channelRecord->getReceivingPeerAddress(); + } + + /** + * Returns the receiver of the message + * + * @param EncryptionChannelRecord $channelRecord The channel record to use + * @return PeerAddress The receiver of the message + */ + public function getReceiver(EncryptionChannelRecord $channelRecord): PeerAddress + { + if($this->recipient === EncryptionMessageRecipient::SENDER) + { + return $channelRecord->getReceivingPeerAddress(); + } + + return $channelRecord->getCallingPeerAddress(); + } + + /** + * Converts the record to a standard message + * + * @return EncryptionChannelMessage The standard message + */ + public function toStandard(): EncryptionChannelMessage + { + return new EncryptionChannelMessage([ + 'message_uuid' => $this->uuid, + 'channel_uuid' => $this->channelUuid, + 'status' => $this->status->value, + 'hash' => $this->hash, + 'data' => $this->data, + 'timestamp' => $this->timestamp->getTimestamp() + ]); + } + /** * @inheritDoc */ From d9955f93931c685c33f7bebfdf1e293535e3c5f7 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 4 Mar 2025 13:41:52 -0500 Subject: [PATCH 414/420] Add EncryptionChannelMessage class for managing encryption channel message data --- .../Standard/EncryptionChannelMessage.php | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 src/Socialbox/Objects/Standard/EncryptionChannelMessage.php diff --git a/src/Socialbox/Objects/Standard/EncryptionChannelMessage.php b/src/Socialbox/Objects/Standard/EncryptionChannelMessage.php new file mode 100644 index 0000000..031d78e --- /dev/null +++ b/src/Socialbox/Objects/Standard/EncryptionChannelMessage.php @@ -0,0 +1,79 @@ +messageUuid = $data['message_uuid']; + $this->channelUuid = $data['channel_uuid']; + $this->status = EncryptionChannelStatus::from($data['status']); + $this->hash = $data['hash']; + $this->data = $data['data']; + $this->timestamp = $data['timestamp']; + } + + public function getMessageUuid(): string + { + return $this->messageUuid; + } + + public function getChannelUuid(): string + { + return $this->channelUuid; + } + + public function getStatus(): EncryptionChannelStatus + { + return $this->status; + } + + public function getHash(): string + { + return $this->hash; + } + + public function getData(): string + { + return $this->data; + } + + public function getTimestamp(): int + { + return $this->timestamp; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $data): EncryptionChannelMessage + { + return new self($data); + } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return [ + 'message_uuid' => $this->messageUuid, + 'channel_uuid' => $this->channelUuid, + 'status' => $this->status->value, + 'hash' => $this->hash, + 'data' => $this->data, + 'timestamp' => $this->timestamp + ]; + } + } \ No newline at end of file From e3ca7869e216f946c0386a48dcb8258602bb1fa8 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 4 Mar 2025 13:41:57 -0500 Subject: [PATCH 415/420] Rename getAddress method to getPeerAddress for clarity --- src/Socialbox/Objects/Standard/Peer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Socialbox/Objects/Standard/Peer.php b/src/Socialbox/Objects/Standard/Peer.php index 1d2ae72..7c1f32a 100644 --- a/src/Socialbox/Objects/Standard/Peer.php +++ b/src/Socialbox/Objects/Standard/Peer.php @@ -104,7 +104,7 @@ * * @return PeerAddress The address associated with the instance. */ - public function getAddress(): PeerAddress + public function getPeerAddress(): PeerAddress { return $this->address; } From fa78fdbac2593fe5528f8501ac48aa09dd8c6555 Mon Sep 17 00:00:00 2001 From: netkas Date: Tue, 4 Mar 2025 13:50:37 -0500 Subject: [PATCH 416/420] Add methods for managing encrypted channels, including acceptance, acknowledgment, creation, and retrieval --- src/Socialbox/SocialClient.php | 112 +++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/src/Socialbox/SocialClient.php b/src/Socialbox/SocialClient.php index 45474ec..c491c44 100644 --- a/src/Socialbox/SocialClient.php +++ b/src/Socialbox/SocialClient.php @@ -520,6 +520,15 @@ )->getResponse()->getResult()) ?? SignatureVerificationStatus::ERROR; } + /** + * Accepts an encrypted channel using the provided public encryption key. + * + * @param string $channelUuid The UUID of the channel to accept + * @param string $publicEncryptionKey The public encryption key to use for the channel + * @param PeerAddress|string|null $identifiedAs Optional identifier for the peer (PeerAddress object, string, or null) + * + * @return bool Returns true if the channel was successfully accepted, false otherwise + */ public function encryptionAcceptChannel(string $channelUuid, string $publicEncryptionKey, PeerAddress|string|null $identifiedAs=null): bool { @@ -536,6 +545,13 @@ )->getResponse()->getResult(); } + /** + * Checks if an encryption channel exists for the given channel UUID. + * + * @param string $channelUuid The UUID of the channel to check. + * + * @return bool True if an encryption channel exists, false otherwise. + */ public function encryptionChannelExists(string $channelUuid): bool { return $this->sendRequest( @@ -545,6 +561,69 @@ )->getResponse()->getResult(); } + /** + * Acknowledges an encrypted message in a specific channel. + * + * This method is used to confirm the successful delivery and decryption of a message + * within an encrypted communication channel. It allows the client to signal that a + * particular message, identified by its UUID, has been successfully processed. + * + * @param string $channelUuid The UUID of the channel in which the message was sent. + * @param string $messageUuid The UUID of the message being acknowledged. + * @param PeerAddress|string|null $identifiedAs Optional. The peer address or identifier of the recipient acknowledging the message. + * It can be a PeerAddress object, a string representation of the address, or null if not applicable. + * + * @return bool True if the acknowledgement was successful, false otherwise. + */ + public function encryptionChannelAcknowledgeMessage(string $channelUuid, string $messageUuid, PeerAddress|string|null $identifiedAs=null): bool + { + if($identifiedAs instanceof PeerAddress) + { + $identifiedAs = $identifiedAs->getAddress(); + } + + return $this->sendRequest( + new RpcRequest(StandardMethods::ENCRYPTION_CHANNEL_ACKNOWLEDGE_MESSAGE, parameters: [ + 'channel_uuid' => $channelUuid, + 'message_uuid' => $messageUuid + ]), true, $identifiedAs + )->getResponse()->getResult(); + } + + /** + * Acknowledges messages in an encryption channel. + * + * @param string $channelUuid The UUID of the encryption channel. + * @param array $messageUuids An array of message UUIDs to acknowledge. + * @param PeerAddress|string|null $identifiedAs Optional peer address or identifier. + * + * @return array An array containing the results of the acknowledgement operation. + */ + public function encryptionChannelAcknowledgeMessages(string $channelUuid, array $messageUuids, PeerAddress|string|null $identifiedAs=null): array + { + if($identifiedAs instanceof PeerAddress) + { + $identifiedAs = $identifiedAs->getAddress(); + } + + return $this->sendRequests(array_map(fn($messageUuid) => new RpcRequest(StandardMethods::ENCRYPTION_CHANNEL_ACKNOWLEDGE_MESSAGE, parameters: [ + 'channel_uuid' => $channelUuid, + 'message_uuid' => $messageUuid + ]), $messageUuids), $identifiedAs); + } + + /** + * Sends an encrypted message to a specific channel. + * + * @param string $channelUuid The UUID of the channel to send the message to. + * @param string $checksum The checksum of the encrypted data for integrity verification. + * @param string $data The encrypted data to be sent. + * @param PeerAddress|string|null $identifiedAs The peer address or identifier of the sender (optional). + * @param string|null $messageUuid The UUID of the message (optional). If null, a UUID will be generated. + * @param int|null $timestamp The timestamp of the message (optional). If null, the current timestamp will be used. + * + * @return string The UUID of the sent message. + */ public function encryptionChannelSend(string $channelUuid, string $checksum, string $data, PeerAddress|string|null $identifiedAs=null, ?string $messageUuid=null, ?int $timestamp=null): string { if($identifiedAs instanceof PeerAddress) @@ -563,6 +642,14 @@ )->getResponse()->getResult(); } + /** + * Closes an encrypted communication channel. + * + * @param string $channelUuid The UUID of the channel to close. + * @param PeerAddress|string|null $identifiedAs Optional identifier for the peer, either a PeerAddress object, a string representation, or null if not applicable. + * + * @return bool True if the channel was successfully closed, false otherwise. + */ public function encryptionCloseChannel(string $channelUuid, PeerAddress|string|null $identifiedAs=null): bool { if($identifiedAs instanceof PeerAddress) @@ -577,6 +664,16 @@ )->getResponse()->getResult(); } + /** + * Creates an encrypted channel with the specified peer. + * + * @param string|PeerAddress $receivingPeer The peer to create the channel with. Can be a PeerAddress object or a string representation of the peer's address. + * @param string $publicEncryptionKey The public encryption key to use for the channel. + * @param string|null $channelUuid Optional UUID for the channel. If null, a UUID will be generated. + * @param PeerAddress|string|null $identifiedAs Optional peer address or string to identify the channel as. + * + * @return string The UUID of the created channel. + */ public function encryptionCreateChannel(string|PeerAddress $receivingPeer, string $publicEncryptionKey, ?string $channelUuid=null, PeerAddress|string|null $identifiedAs=null): string { if($receivingPeer instanceof PeerAddress) @@ -598,6 +695,14 @@ )->getResponse()->getResult(); } + /** + * Declines an encryption channel request. + * + * @param string $channelUuid The UUID of the channel to decline. + * @param PeerAddress|string|null $identifiedAs The peer address or identifier of the peer that initiated the channel request. + * + * @return bool True if the channel decline was successful, false otherwise. + */ public function encryptionDeclineChannel(string $channelUuid, PeerAddress|string|null $identifiedAs=null): bool { if($identifiedAs instanceof PeerAddress) @@ -612,6 +717,13 @@ )->getResponse()->getResult(); } + /** + * Retrieves an encryption channel by its UUID. + * + * @param string $channelUuid The UUID of the encryption channel to retrieve. + * + * @return EncryptionChannel The EncryptionChannel object associated with the given UUID. + */ public function encryptionGetChannel(string $channelUuid): EncryptionChannel { return new EncryptionChannel($this->sendRequest( From 1fedaa27c4be1ab084fdaf791a544d7d8d4b8feb Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 6 Mar 2025 15:14:23 -0500 Subject: [PATCH 417/420] Refactor channel retrieval methods to support pagination and enhance peer address validation --- .../Managers/EncryptionChannelManager.php | 155 ++++++++++++++++-- 1 file changed, 143 insertions(+), 12 deletions(-) diff --git a/src/Socialbox/Managers/EncryptionChannelManager.php b/src/Socialbox/Managers/EncryptionChannelManager.php index 9eabc86..31a3444 100644 --- a/src/Socialbox/Managers/EncryptionChannelManager.php +++ b/src/Socialbox/Managers/EncryptionChannelManager.php @@ -5,7 +5,9 @@ use InvalidArgumentException; + use PDO; use PDOException; + use Socialbox\Classes\Configuration; use Socialbox\Classes\Cryptography; use Socialbox\Classes\Database; use Socialbox\Classes\Validator; @@ -262,21 +264,102 @@ /** * Returns an array of channels that are outgoing from the specified peer address * - * @param string $peerAddress The Peer Address of the caller + * @param string|PeerAddress $peerAddress The Peer Address of the caller + * @param int $page The page number + * @param int $limit The limit of records to return * @return EncryptionChannelRecord[] An array of channel records * @throws DatabaseOperationException Thrown if there was a database error while retrieving the records */ - public static function getIncomingChannels(string $peerAddress): array + public static function getChannels(string|PeerAddress $peerAddress, int $page=1, int $limit=100): array { - if(!Validator::validatePeerAddress($peerAddress)) + if($peerAddress instanceof PeerAddress) + { + $peerAddress = $peerAddress->getAddress(); + } + elseif(!Validator::validatePeerAddress($peerAddress)) { throw new InvalidArgumentException('Invalid Peer Address'); } + if($page < 1) + { + throw new InvalidArgumentException('The page number cannot be less than 1'); + } + + if($limit < 1) + { + throw new InvalidArgumentException('The limit cannot be less than 1'); + } + elseif($limit > Configuration::getPoliciesConfiguration()->getEncryptionChannelsLimit()) + { + throw new InvalidArgumentException(sprintf('The limit cannot exceed a value of %d', Configuration::getPoliciesConfiguration()->getEncryptionChannelsLimit())); + } + try { - $stmt = Database::getConnection()->prepare('SELECT * FROM encryption_channels WHERE receiving_peer_address=:peer_address'); + $offset = ($page - 1) * $limit; + $stmt = Database::getConnection()->prepare('SELECT * FROM encryption_channels WHERE calling_peer_address=:peer_address OR receiving_peer_address=:peer_address LIMIT :limit OFFSET :offset'); $stmt->bindParam(':peer_address', $peerAddress); + $stmt->bindParam(':limit', $limit, PDO::PARAM_INT); + $stmt->bindParam(':offset', $offset, PDO::PARAM_INT); + $stmt->execute(); + + $results = $stmt->fetchAll(); + + if(!$results) + { + return []; + } + + return array_map(fn($result) => EncryptionChannelRecord::fromArray($result), $results); + } + catch(PDOException $e) + { + throw new DatabaseOperationException('Failed to retrieve encryption channels', $e); + } + } + + /** + * Returns an array of channels that are outgoing from the specified peer address + * + * @param string|PeerAddress $peerAddress The Peer Address of the caller + * @param int $page The page number + * @param int $limit The limit of records to return + * @return EncryptionChannelRecord[] An array of channel records + * @throws DatabaseOperationException Thrown if there was a database error while retrieving the records + */ + public static function getIncomingChannels(string|PeerAddress $peerAddress, int $page=1, int $limit=100): array + { + if($peerAddress instanceof PeerAddress) + { + $peerAddress = $peerAddress->getAddress(); + } + elseif(!Validator::validatePeerAddress($peerAddress)) + { + throw new InvalidArgumentException('Invalid Peer Address'); + } + + if($page < 1) + { + throw new InvalidArgumentException('The page number cannot be less than 1'); + } + + if($limit < 1) + { + throw new InvalidArgumentException('The limit cannot be less than 1'); + } + elseif($limit > Configuration::getPoliciesConfiguration()->getEncryptionChannelIncomingLimit()) + { + throw new InvalidArgumentException(sprintf('The limit cannot exceed a value of %d', Configuration::getPoliciesConfiguration()->getEncryptionChannelIncomingLimit())); + } + + try + { + $offset = ($page - 1) * $limit; + $stmt = Database::getConnection()->prepare('SELECT * FROM encryption_channels WHERE receiving_peer_address=:peer_address LIMIT :limit OFFSET :offset'); + $stmt->bindParam(':peer_address', $peerAddress); + $stmt->bindParam(':limit', $limit); + $stmt->bindParam(':offset', $offset); $stmt->execute(); $results = $stmt->fetchAll(); @@ -297,21 +380,44 @@ /** * Returns an array of outgoing channels for the given peer address * - * @param string $peerAddress The Peer Address of the caller + * @param string|PeerAddress $peerAddress The Peer Address of the caller + * @param int $page The page number + * @param int $limit The limit of records to return * @return EncryptionChannelRecord[] An array of channel records * @throws DatabaseOperationException Thrown if there was a database error while retrieving the records */ - public static function getOutgoingChannels(string $peerAddress): array + public static function getOutgoingChannels(string|PeerAddress $peerAddress, int $page=1, int $limit=100): array { - if(!Validator::validatePeerAddress($peerAddress)) + if($peerAddress instanceof PeerAddress) + { + $peerAddress = $peerAddress->getAddress(); + } + elseif(!Validator::validatePeerAddress($peerAddress)) { throw new InvalidArgumentException('Invalid Peer Address'); } + if($page < 1) + { + throw new InvalidArgumentException('The page number cannot be less than 1'); + } + + if($limit < 1) + { + throw new InvalidArgumentException('The limit cannot be less than 1'); + } + elseif($limit > Configuration::getPoliciesConfiguration()->getEncryptionChannelOutgoingLimit()) + { + throw new InvalidArgumentException(sprintf('The limit cannot exceed a value of %d', Configuration::getPoliciesConfiguration()->getEncryptionChannelOutgoingLimit())); + } + try { - $stmt = Database::getConnection()->prepare('SELECT * FROM encryption_channels WHERE calling_peer_address=:peer_address'); + $offset = ($page -1) * $limit; + $stmt = Database::getConnection()->prepare('SELECT * FROM encryption_channels WHERE calling_peer_address=:peer_address LIMIT :limit OFFSET :offset'); $stmt->bindParam(':peer_address', $peerAddress); + $stmt->bindParam(':limit', $limit); + $stmt->bindParam(':offset', $offset); $stmt->execute(); $results = $stmt->fetchAll(); @@ -332,23 +438,48 @@ /** * Returns an array of channels that are awaiting the receiver to accept the channel * - * @param string $peerAddress The Peer Address of the receiver + * @param string|PeerAddress $peerAddress The Peer Address of the receiver + * @param int $page The page number + * @param int $limit The limit of records to return * @return EncryptionChannelRecord[] An array of channel records * @throws DatabaseOperationException Thrown if there was a database error while retrieving the records */ - public static function getChannelRequests(string $peerAddress): array + public static function getChannelRequests(string|PeerAddress $peerAddress, int $page=1, int $limit=100): array { - if(!Validator::validatePeerAddress($peerAddress)) + if($peerAddress instanceof PeerAddress) + { + $peerAddress = $peerAddress->getAddress(); + } + elseif(!Validator::validatePeerAddress($peerAddress)) { throw new InvalidArgumentException('Invalid Peer Address'); } + + if($page < 1) + { + throw new InvalidArgumentException('The page number cannot be less than 1'); + } + + if($limit < 1) + { + throw new InvalidArgumentException('The limit cannot be less than 1'); + } + elseif($limit > Configuration::getPoliciesConfiguration()->getEncryptionChannelRequestsLimit()) + { + throw new InvalidArgumentException(sprintf('The limit cannot exceed a value of %d', Configuration::getPoliciesConfiguration()->getEncryptionChannelRequestsLimit())); + } + + try { - $stmt = Database::getConnection()->prepare('SELECT * FROM encryption_channels WHERE receiving_peer_address=:peer_address AND status=:status'); + $offset = ($page -1) * $limit; + $stmt = Database::getConnection()->prepare('SELECT * FROM encryption_channels WHERE receiving_peer_address=:peer_address AND status=:status LIMIT :limit OFFSET :offset'); $stmt->bindParam(':peer_address', $peerAddress); $status = EncryptionChannelStatus::AWAITING_RECEIVER->value; $stmt->bindParam(':status', $status); + $stmt->bindParam(':limit', $limit); + $stmt->bindParam(':offset', $offset); $stmt->execute(); $results = $stmt->fetchAll(); From 91153a353292957d8506934549c49074f51059d5 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 6 Mar 2025 15:14:27 -0500 Subject: [PATCH 418/420] Add factory method to create InvalidRpcArgumentException from InvalidArgumentException --- .../Standard/InvalidRpcArgumentException.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Socialbox/Exceptions/Standard/InvalidRpcArgumentException.php b/src/Socialbox/Exceptions/Standard/InvalidRpcArgumentException.php index 9c2c30a..5a1f101 100644 --- a/src/Socialbox/Exceptions/Standard/InvalidRpcArgumentException.php +++ b/src/Socialbox/Exceptions/Standard/InvalidRpcArgumentException.php @@ -48,4 +48,15 @@ parent::__construct(sprintf('Invalid parameter %s: %s', $parameterName, $reason), StandardError::RPC_INVALID_ARGUMENTS); } + + /** + * Creates an instance of InvalidRpcArgumentException from an InvalidArgumentException + * + * @param InvalidArgumentException $e The exception to create the instance from + * @return InvalidRpcArgumentException The instance created from the exception + */ + public static function fromInvalidArgumentException(InvalidArgumentException $e): InvalidRpcArgumentException + { + return new InvalidRpcArgumentException(null, $e); + } } \ No newline at end of file From ec44e85a5e94271429f9fcc4e6a6c2eadbf3ba36 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 6 Mar 2025 15:14:33 -0500 Subject: [PATCH 419/420] Improve documentation for encryption channel methods and add rejection functionality --- src/Socialbox/SocialClient.php | 61 ++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/src/Socialbox/SocialClient.php b/src/Socialbox/SocialClient.php index c491c44..a530347 100644 --- a/src/Socialbox/SocialClient.php +++ b/src/Socialbox/SocialClient.php @@ -50,7 +50,7 @@ } /** - * Create a new signing keypair, sends it to the server and saves it locally with the session so that the client + * Create a new signing keypair, sends it to the server, and saves it locally with the session so that the client * can use the SigningKey pair for signing and verifying signatures in the future. * * This is not a server-side operation, the server only stores the public key and associates it with the peer's @@ -79,7 +79,7 @@ } /** - * Creates a new encryption channel with the given peer, generates a new encryption key pair and sends the public + * Creates a new encryption channel with the given peer, generates a new encryption key pair, and sends the public * key to the receiving peer. The private key is stored locally and is never sent to the server. * * @param PeerAddress|string $receivingPeer The address of the peer to create the channel with @@ -109,9 +109,10 @@ * Waits for the encryption channel to be accepted by the receiving peer, returns True if the channel was accepted * or False if the channel was not accepted within the timeout period. * - * @param string $channelUuid - * @param int|null $timeout - * @return bool + * @param string $channelUuid The UUID of the encryption channel to wait for + * @param int|null $timeout Optional. The timeout period in seconds + * @return bool Returns True if the channel was accepted, False if the channel was not accepted within the timeout period + * @throws RpcException Thrown if there was an error with the RPC request */ public function waitForEncryptionChannel(string $channelUuid, ?int $timeout=30): bool { @@ -142,8 +143,8 @@ } /** - * Accepts an encryption channel with the given UUID, generates a new encryption key pair and sends the public key - * to the calling peer. The private key is stored locally and is never sent to the server. + * Accepts an encryption channel with the given unique universal identifier, generates a new encryption key pair, + * and sends the public key to the calling peer. The private key is stored locally and is never sent to the server. * * @param string $channelUuid The UUID of the encryption channel to accept * @return bool Returns True if the channel was accepted @@ -154,7 +155,7 @@ { $encryptionChannel = $this->encryptionGetChannel($channelUuid); $encryptionKeyPair = Cryptography::generateEncryptionKeyPair(); - $this->encryptionAcceptChannel($channelUuid, $encryptionKeyPair->getPublicKey(), $encryptionChannel->getRecipient()); + $this->encryptionAcceptChannel($channelUuid, $encryptionKeyPair->getPublicKey()); $this->addEncryptionChannelSecret(new EncryptionChannelSecret([ 'channel_uuid' => $channelUuid, @@ -237,7 +238,7 @@ } /** - * Deletes a contact from the address book, returns True if the contact was deleted or False if the contact + * Deletes a contact from the address book, returns True if the contact was deleted, or False if the contact * does not exist. * * @param PeerAddress|string $peer The address of the peer to delete @@ -526,12 +527,12 @@ * @param string $channelUuid The UUID of the channel to accept * @param string $publicEncryptionKey The public encryption key to use for the channel * @param PeerAddress|string|null $identifiedAs Optional identifier for the peer (PeerAddress object, string, or null) - * + * * @return bool Returns true if the channel was successfully accepted, false otherwise + * @throws RpcException Thrown if there was an error with the RPC request */ public function encryptionAcceptChannel(string $channelUuid, string $publicEncryptionKey, PeerAddress|string|null $identifiedAs=null): bool { - if($identifiedAs instanceof PeerAddress) { $identifiedAs = $identifiedAs->getAddress(); @@ -551,6 +552,7 @@ * @param string $channelUuid The UUID of the channel to check. * * @return bool True if an encryption channel exists, false otherwise. + * @throws RpcException Thrown if there was an error with the RPC request. */ public function encryptionChannelExists(string $channelUuid): bool { @@ -574,6 +576,7 @@ * It can be a PeerAddress object, a string representation of the address, or null if not applicable. * * @return bool True if the acknowledgement was successful, false otherwise. + * @throws RpcException Thrown if there was an error with the RPC request. */ public function encryptionChannelAcknowledgeMessage(string $channelUuid, string $messageUuid, PeerAddress|string|null $identifiedAs=null): bool { @@ -596,8 +599,8 @@ * @param string $channelUuid The UUID of the encryption channel. * @param array $messageUuids An array of message UUIDs to acknowledge. * @param PeerAddress|string|null $identifiedAs Optional peer address or identifier. - * * @return array An array containing the results of the acknowledgement operation. + * @throws RpcException Thrown if there was an error with the RPC request. */ public function encryptionChannelAcknowledgeMessages(string $channelUuid, array $messageUuids, PeerAddress|string|null $identifiedAs=null): array { @@ -612,6 +615,30 @@ ]), $messageUuids), $identifiedAs); } + /** + * Rejects a message from the encryption channel + * + * @param string $channelUuid The channel UUID used for communication + * @param string $messageUuid The Message UUID to reject + * @param PeerAddress|string|null $identifiedAs Optional. The requesting peer that's rejecting this + * @return bool Returns True on success, False if not applicable + * @throws RpcException Thrown when there is an error + */ + public function encryptionChannelRejectMessage(string $channelUuid, string $messageUuid, PeerAddress|string|null $identifiedAs): bool + { + if($identifiedAs instanceof PeerAddress) + { + $identifiedAs = $identifiedAs->getAddress(); + } + + return $this->sendRequest( + new RpcRequest(StandardMethods::ENCRYPTION_CHANNEL_REJECT_MESSAGE, parameters: [ + 'channel_uuid' => $channelUuid, + 'message_uuid' => $messageUuid, + ]), true, $identifiedAs + )->getResponse()->getResult(); + } + /** * Sends an encrypted message to a specific channel. * @@ -621,8 +648,8 @@ * @param PeerAddress|string|null $identifiedAs The peer address or identifier of the sender (optional). * @param string|null $messageUuid The UUID of the message (optional). If null, a UUID will be generated. * @param int|null $timestamp The timestamp of the message (optional). If null, the current timestamp will be used. - * * @return string The UUID of the sent message. + * @throws RpcException Thrown if there was an error with the RPC request */ public function encryptionChannelSend(string $channelUuid, string $checksum, string $data, PeerAddress|string|null $identifiedAs=null, ?string $messageUuid=null, ?int $timestamp=null): string { @@ -647,8 +674,8 @@ * * @param string $channelUuid The UUID of the channel to close. * @param PeerAddress|string|null $identifiedAs Optional identifier for the peer, either a PeerAddress object, a string representation, or null if not applicable. - * * @return bool True if the channel was successfully closed, false otherwise. + * @throws RpcException Thrown if there was an error with the RPC request */ public function encryptionCloseChannel(string $channelUuid, PeerAddress|string|null $identifiedAs=null): bool { @@ -671,8 +698,8 @@ * @param string $publicEncryptionKey The public encryption key to use for the channel. * @param string|null $channelUuid Optional UUID for the channel. If null, a UUID will be generated. * @param PeerAddress|string|null $identifiedAs Optional peer address or string to identify the channel as. - * * @return string The UUID of the created channel. + * @throws RpcException Thrown if there was an error with the RPC requests */ public function encryptionCreateChannel(string|PeerAddress $receivingPeer, string $publicEncryptionKey, ?string $channelUuid=null, PeerAddress|string|null $identifiedAs=null): string { @@ -700,8 +727,8 @@ * * @param string $channelUuid The UUID of the channel to decline. * @param PeerAddress|string|null $identifiedAs The peer address or identifier of the peer that initiated the channel request. - * * @return bool True if the channel decline was successful, false otherwise. + * @throws RpcException Thrown if there was an error with the RPC request */ public function encryptionDeclineChannel(string $channelUuid, PeerAddress|string|null $identifiedAs=null): bool { @@ -721,8 +748,8 @@ * Retrieves an encryption channel by its UUID. * * @param string $channelUuid The UUID of the encryption channel to retrieve. - * * @return EncryptionChannel The EncryptionChannel object associated with the given UUID. + * @throws RpcException Thrown if there was an error with the RPC request */ public function encryptionGetChannel(string $channelUuid): EncryptionChannel { From f9185b59697c4bf703a0e9261725663f42ce065e Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 6 Mar 2025 15:16:43 -0500 Subject: [PATCH 420/420] Add EncryptionChannelRejectMessage class to handle message rejection in encryption channels --- .../EncryptionChannelRejectMessage.php | 221 ++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionChannelRejectMessage.php diff --git a/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionChannelRejectMessage.php b/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionChannelRejectMessage.php new file mode 100644 index 0000000..e929654 --- /dev/null +++ b/src/Socialbox/Classes/StandardMethods/EncryptionChannel/EncryptionChannelRejectMessage.php @@ -0,0 +1,221 @@ +containsParameter('channel_uuid')) + { + throw new MissingRpcArgumentException('channel_uuid'); + } + elseif(!Validator::validateUuid($rpcRequest->getParameter('channel_uuid'))) + { + throw new InvalidRpcArgumentException('channel_uuid', 'The given channel uuid is not a valid UUID V4'); + } + + if(!$rpcRequest->containsParameter('message_uuid')) + { + throw new MissingRpcArgumentException('message_uuid'); + } + elseif(!is_string($rpcRequest->getParameter('message_uuid'))) + { + throw new InvalidRpcArgumentException('message_uuid', 'Must be type string'); + } + elseif(!Validator::validateUuid($rpcRequest->getParameter('message_uuid'))) + { + throw new InvalidRpcArgumentException('message_uuid', 'Invalid message UUID V4'); + } + + try + { + $channelUuid = $rpcRequest->getParameter('channel_uuid'); + $encryptionChannel = EncryptionChannelManager::getChannel($channelUuid); + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to retrieve the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if($encryptionChannel === null) + { + return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The encryption channel does not exist'); + } + + try + { + if ($request->isExternal()) + { + return self::handleExternal($request, $rpcRequest, $encryptionChannel); + } + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to reject the message', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return self::handleInternal($request, $rpcRequest, $encryptionChannel); + } + + /** + * Handles the external execution of the method. + * + * @param ClientRequest $request The client request instance. + * @param RpcRequest $rpcRequest The RPC request instance. + * @param EncryptionChannelRecord $encryptionChannel The encryption channel record. + * @return SerializableInterface|null The response to the request. + * @throws StandardRpcException If an error occurs. + */ + public static function handleExternal(ClientRequest $request, RpcRequest $rpcRequest, EncryptionChannelRecord $encryptionChannel): ?SerializableInterface + { + if($request->getIdentifyAs() === null) + { + return $rpcRequest->produceError(StandardError::BAD_REQUEST, 'The IdentifyAs header is missing'); + } + + $requestingPeerAddress = $request->getIdentifyAs(); + if(!$encryptionChannel->isParticipant($requestingPeerAddress)) + { + return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The encryption channel is not accessible'); + } + + try + { + $message = EncryptionChannelManager::getMessageRecord($rpcRequest->getParameter('channel_uuid'), $rpcRequest->getParameter('message_uuid')); + + if($message === null) + { + return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The message does not exist'); + } + + if($message->getReceiver($encryptionChannel)->getAddress() !== $requestingPeerAddress) + { + return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The message is not for the requesting peer'); + } + + EncryptionChannelManager::rejectMessage( + $rpcRequest->getParameter('channel_uuid'), $rpcRequest->getParameter('message_uuid') + ); + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to reject the message', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + return $rpcRequest->produceResponse(true); + } + + /** + * Handles the internal execution of the method. + * + * @param ClientRequest $request The client request instance. + * @param RpcRequest $rpcRequest The RPC request instance. + * @param EncryptionChannelRecord $encryptionChannel The encryption channel record. + * @return SerializableInterface|null The response to the request. + * @throws StandardRpcException If an error occurs. + */ + public static function handleInternal(ClientRequest $request, RpcRequest $rpcRequest, EncryptionChannelRecord $encryptionChannel): ?SerializableInterface + { + try + { + $requestingPeer = $request->getPeer(); + } + catch (DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to retrieve the peer', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if($requestingPeer === null) + { + return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The peer is not authorized'); + } + + if(!$encryptionChannel->isParticipant($requestingPeer->getAddress())) + { + return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The encryption channel is not accessible'); + } + elseif($encryptionChannel->getStatus() !== EncryptionChannelStatus::OPENED) + { + return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The encryption channel is not opened'); + } + + try + { + $message = EncryptionChannelManager::getMessageRecord($rpcRequest->getParameter('channel_uuid'), $rpcRequest->getParameter('message_uuid')); + + if($message === null) + { + return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The message does not exist'); + } + + if($message->getReceiver($encryptionChannel)->getAddress() !== $requestingPeer->getAddress()) + { + return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The message is not for the requesting peer'); + } + + EncryptionChannelManager::acknowledgeMessage( + $rpcRequest->getParameter('channel_uuid'), $rpcRequest->getParameter('message_uuid') + ); + } + catch(DatabaseOperationException $e) + { + throw new StandardRpcException('Failed to acknowledge the message', StandardError::INTERNAL_SERVER_ERROR, $e); + } + + if($message->getOwner($encryptionChannel)->isExternal()) + { + try + { + $rpcClient = Socialbox::getExternalSession($message->getOwner($encryptionChannel)->getDomain()); + $rpcClient->encryptionChannelRejectMessage( + channelUuid: $rpcRequest->getParameter('channel_uuid'), + messageUuid: $rpcRequest->getParameter('message_uuid'), + identifiedAs: $requestingPeer->getAddress() + ); + } + catch(Exception $e) + { + try + { + EncryptionChannelManager::rejectMessage($rpcRequest->getParameter('channel_uuid'), $rpcRequest->getParameter('message_uuid'), true); + } + catch (DatabaseOperationException $e) + { + Logger::getLogger()->error('Error rejecting message as server', $e); + } + + if($e instanceof RpcException) + { + throw StandardRpcException::fromRpcException($e); + } + + throw new StandardRpcException('Failed to acknowledge the message with the external server', StandardError::INTERNAL_SERVER_ERROR, $e); + } + } + + return $rpcRequest->produceResponse(true); + } + } \ No newline at end of file