From ce64643d7371375e0e3c2db827792fd80cd1b42f Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 25 Oct 2024 14:23:43 -0400 Subject: [PATCH] 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()); } }