From ef3b10b2866c9d885453b09035f05b5c05be6e58 Mon Sep 17 00:00:00 2001 From: netkas Date: Thu, 19 Dec 2024 15:09:22 -0500 Subject: [PATCH] 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() {