From fde3ccfc68b1821d5a34114bc0f898c11fdcc2f5 Mon Sep 17 00:00:00 2001 From: netkas Date: Fri, 10 Jan 2025 14:58:24 -0500 Subject: [PATCH] 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.