Add new classes and methods for session management

This commit is contained in:
netkas 2024-09-13 13:52:38 -04:00
parent b9c84aeb27
commit 764ec51fa4
12 changed files with 1026 additions and 28 deletions

View file

@ -0,0 +1,118 @@
<?php
namespace Socialbox\Classes\CacheLayer;
use Redis;
use RedisException;
use RuntimeException;
use Socialbox\Abstracts\CacheLayer;
class RedisCacheLayer extends CacheLayer
{
private Redis $redis;
/**
* Redis cache layer constructor.
*
* @param string $host The Redis server host.
* @param int $port The Redis server port.
* @param string|null $password Optional. The Redis server password.
*/
public function __construct(string $host, int $port, ?string $password=null)
{
if (!extension_loaded('redis'))
{
throw new RuntimeException('The Redis extension is not loaded in your PHP environment.');
}
$this->redis = new Redis();
try
{
$this->redis->connect($host, $port);
if ($password !== null)
{
$this->redis->auth($password);
}
}
catch (RedisException $e)
{
throw new RuntimeException('Failed to connect to the Redis server.', 0, $e);
}
}
/**
* @inheritDoc
*/
public function set(string $key, mixed $value, int $ttl = 0): bool
{
try
{
return $this->redis->set($key, $value, $ttl);
}
catch (RedisException $e)
{
throw new RuntimeException('Failed to set the value in the Redis cache.', 0, $e);
}
}
/**
* @inheritDoc
*/
public function get(string $key): mixed
{
try
{
return $this->redis->get($key);
}
catch (RedisException $e)
{
throw new RuntimeException('Failed to get the value from the Redis cache.', 0, $e);
}
}
/**
* @inheritDoc
*/
public function delete(string $key): bool
{
try
{
return $this->redis->del($key) > 0;
}
catch (RedisException $e)
{
throw new RuntimeException('Failed to delete the value from the Redis cache.', 0, $e);
}
}
/**
* @inheritDoc
*/
public function exists(string $key): bool
{
try
{
return $this->redis->exists($key);
}
catch (RedisException $e)
{
throw new RuntimeException('Failed to check if the key exists in the Redis cache.', 0, $e);
}
}
/**
* @inheritDoc
*/
public function clear(): bool
{
try
{
return $this->redis->flushAll();
}
catch (RedisException $e)
{
throw new RuntimeException('Failed to clear the Redis cache.', 0, $e);
}
}
}

View file

@ -0,0 +1,196 @@
<?php
namespace Socialbox\Classes;
use InvalidArgumentException;
use RuntimeException;
use Socialbox\Enums\StandardHeaders;
use Socialbox\Exceptions\CryptographyException;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\RpcException;
use Socialbox\Managers\SessionManager;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\RpcRequest;
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
* @throws RpcException Thrown if the request is invalid
*/
public static function getClientRequest(): ClientRequest
{
if($_SERVER['REQUEST_METHOD'] !== 'POST')
{
throw new RpcException('Invalid Request Method, expected POST', 400);
}
try
{
$headers = Utilities::getRequestHeaders();
foreach(StandardHeaders::getRequiredHeaders() as $header)
{
if(!isset($headers[$header]))
{
throw new RpcException("Missing required header: $header", 400);
}
// Validate the headers
switch(StandardHeaders::tryFrom($header))
{
case StandardHeaders::CLIENT_VERSION:
if($headers[$header] !== '1.0')
{
throw new RpcException(sprintf("Unsupported Client Version: %s", $headers[$header]));
}
break;
case StandardHeaders::CONTENT_TYPE:
if($headers[$header] !== 'application/json')
{
throw new RpcException("Invalid Content-Type header: Expected application/json", 400);
}
break;
case StandardHeaders::FROM_PEER:
if(!Validator::validatePeerAddress($headers[$header]))
{
throw new RpcException("Invalid From-Peer header: " . $headers[$header], 400);
}
break;
default:
break;
}
}
}
catch(RuntimeException $e)
{
throw new RpcException("Failed to parse request: " . $e->getMessage(), 400, $e);
}
$clientRequest = new ClientRequest($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', StandardHeaders::SIGNATURE->value), 401);
}
try
{
if(!SessionManager::sessionExists($clientRequest->getSessionUuid()))
{
throw new RpcException('Session UUID not found', 404);
}
$session = SessionManager::getSession($clientRequest->getSessionUuid());
// Verify the signature of the request
if(!Cryptography::verifyContent($clientRequest->getHash(), $clientRequest->getSignature(), $session->getPublicKey()))
{
throw new RpcException('Request signature check failed', 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);
}
}
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);
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Socialbox\Classes\StandardMethods;
use Socialbox\Abstracts\Method;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\RpcRequest;
class Ping extends Method
{
/**
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
return $rpcRequest->produceResponse(true);
}
}

View file

@ -0,0 +1,108 @@
<?php
namespace Socialbox\Classes;
use InvalidArgumentException;
use RuntimeException;
class Utilities
{
/**
* Decodes a JSON string into an associative array, throws an exception if the JSON is invalid
*
* @param string $json The JSON string to decode
* @return array The decoded associative array
* @throws InvalidArgumentException If the JSON is invalid
*/
public static function jsonDecode(string $json): array
{
$decoded = json_decode($json, true);
if (json_last_error() !== JSON_ERROR_NONE)
{
throw match (json_last_error())
{
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;
}
/**
* 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;
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Socialbox\Classes;
class Validator
{
private const PEER_ADDRESS_PATTERN = "/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/";
/**
* 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 preg_match(self::PEER_ADDRESS_PATTERN, $address) === 1;
}
}

View file

@ -0,0 +1,182 @@
<?php
namespace Socialbox\Managers;
use DateMalformedStringException;
use DateTime;
use InvalidArgumentException;
use PDO;
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\Objects\SessionRecord;
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.
*
* @return string The UUID of the newly created session.
*
* @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
{
if($publicKey === '')
{
throw new InvalidArgumentException('The public key cannot be empty', StandardError::RPC_INVALID_ARGUMENTS);
}
if(!Cryptography::validatePublicKey($publicKey))
{
throw new InvalidArgumentException('The given public key is invalid', StandardError::INVALID_PUBLIC_KEY);
}
$publicKey = Utilities::base64decode($publicKey);
$uuid = Uuid::v4()->toRfc4122();
try
{
$statement = Database::getConnection()->prepare("INSERT INTO sessions (uuid, public_key) VALUES (?, ?)");
$statement->bindParam(1, $uuid);
$statement->bindParam(2, $publicKey);
$statement->execute();
}
catch(PDOException $e)
{
throw new DatabaseOperationException('Failed to create a session on the database', $e);
}
return $uuid;
}
/**
* Checks if a session with the given UUID exists in the database.
*
* @param string $uuid The UUID of the session to check.
* @return bool True if the session exists, false otherwise.
* @throws DatabaseOperationException If there is an error executing the database query.
*/
public static function sessionExists(string $uuid): bool
{
try
{
$statement = Database::getConnection()->prepare("SELECT COUNT(*) FROM sessions WHERE uuid=?");
$statement->bindParam(1, $uuid);
$statement->execute();
$result = $statement->fetchColumn();
return $result > 0;
}
catch(PDOException $e)
{
throw new DatabaseOperationException('Failed to check if the session exists', $e);
}
}
/**
* 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.
* @throws DatabaseOperationException If the session record cannot be found or if there is an error during retrieval.
*/
public static function getSession(string $uuid): SessionRecord
{
try
{
$statement = Database::getConnection()->prepare("SELECT * FROM sessions WHERE uuid=?");
$statement->bindParam(1, $uuid);
$statement->execute();
$data = $statement->fetch(PDO::FETCH_ASSOC);
if ($data === false)
{
throw new DatabaseOperationException(sprintf('Session record %s not found', $uuid));
}
// Convert the timestamp fields to DateTime objects
$data['created'] = new DateTime($data['created']);
$data['last_request'] = new DateTime($data['last_request']);
return SessionRecord::fromArray($data);
}
catch (PDOException | DateMalformedStringException $e)
{
throw new DatabaseOperationException(sprintf('Failed to retrieve session record %s', $uuid), $e);
}
}
/**
* Update the authenticated peer associated with the given session UUID.
*
* @param string $uuid The UUID of the session to update.
* @return void
*/
public static function updateAuthenticatedPeer(string $uuid): void
{
try
{
$statement = Database::getConnection()->prepare("UPDATE sessions SET authenticated_peer_uuid=? WHERE uuid=?");
$statement->bindParam(1, $uuid);
$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.
*
* @param string $uuid The UUID of the session to be updated.
* @return void
*/
public static function updateLastRequest(string $uuid): void
{
try
{
$formattedTime = (new DateTime('@' . time()))->format('Y-m-d H:i:s');
$statement = Database::getConnection()->prepare("UPDATE sessions SET last_request=? WHERE uuid=?");
$statement->bindValue(1, $formattedTime, PDO::PARAM_STR);
$statement->bindParam(2, $uuid);
$statement->execute();
}
catch (PDOException | DateMalformedStringException $e)
{
throw new DatabaseOperationException('Failed to update last request', $e);
}
}
/**
* Updates the state of a session given its UUID.
*
* @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.
*/
public static function updateState(string $uuid, SessionState $state): void
{
try
{
$state_value = $state->value;
$statement = Database::getConnection()->prepare('UPDATE sessions SET state=? WHERE uuid=?');
$statement->bindParam(1, $state_value);
$statement->bindParam(2, $uuid);
}
catch(PDOException $e)
{
throw new DatabaseOperationException('Failed to update session state', $e);
}
}
}

View file

@ -0,0 +1,96 @@
<?php
namespace Socialbox\Objects;
use InvalidArgumentException;
use Socialbox\Classes\Validator;
use Socialbox\Enums\ReservedUsernames;
class PeerAddress
{
private string $username;
private string $domain;
/**
* 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)
{
$this->username = $username;
$this->domain = $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))
{
throw new InvalidArgumentException("Invalid peer address: $address");
}
$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);
}
}

View file

@ -2,26 +2,27 @@
namespace Socialbox\Objects; namespace Socialbox\Objects;
use Socialbox\Enums\StandardError;
use Socialbox\Interfaces\SerializableInterface; use Socialbox\Interfaces\SerializableInterface;
class RpcError implements SerializableInterface class RpcError implements SerializableInterface
{ {
private string $id; private string $id;
private string $error; private string $message;
private int $code; private StandardError $code;
/** /**
* Constructs the RPC error object. * Constructs the RPC error object.
* *
* @param string $id The ID of the RPC request * @param string $id The ID of the RPC request
* @param string $error The error message * @param StandardError $error The error code
* @param int $code The error code * @param string $message The error message
*/ */
public function __construct(string $id, string $error, int $code) public function __construct(string $id, StandardError $error, string $message)
{ {
$this->id = $id; $this->id = $id;
$this->error = $error; $this->code = $error;
$this->code = $code; $this->message = $message;
} }
/** /**
@ -39,17 +40,17 @@ class RpcError implements SerializableInterface
* *
* @return string The error message. * @return string The error message.
*/ */
public function getError(): string public function getMessage(): string
{ {
return $this->error; return $this->message;
} }
/** /**
* Returns the error code. * Returns the error code.
* *
* @return int The error code. * @return StandardError The error code.
*/ */
public function getCode(): int public function getCode(): StandardError
{ {
return $this->code; return $this->code;
} }
@ -63,8 +64,8 @@ class RpcError implements SerializableInterface
{ {
return [ return [
'id' => $this->id, 'id' => $this->id,
'error' => $this->error, 'error' => $this->message,
'code' => $this->code 'code' => $this->code->value
]; ];
} }
@ -76,6 +77,13 @@ class RpcError implements SerializableInterface
*/ */
public static function fromArray(array $data): RpcError public static function fromArray(array $data): RpcError
{ {
return new RpcError($data['id'], $data['error'], $data['code']); $errorCode = StandardError::tryFrom($data['code']);
if($errorCode == null)
{
$errorCode = StandardError::UNKNOWN;
}
return new RpcError($data['id'], $data['error'], $errorCode);
} }
} }

View file

@ -2,6 +2,11 @@
namespace Socialbox\Objects; namespace Socialbox\Objects;
use InvalidArgumentException;
use ncc\ThirdParty\nikic\PhpParser\Node\Expr\BinaryOp\BooleanOr;
use Socialbox\Enums\StandardError;
use Socialbox\Exceptions\RpcException;
use Socialbox\Exceptions\StandardException;
use Socialbox\Interfaces\SerializableInterface; use Socialbox\Interfaces\SerializableInterface;
class RpcRequest implements SerializableInterface class RpcRequest implements SerializableInterface
@ -13,13 +18,15 @@ class RpcRequest implements SerializableInterface
/** /**
* Constructs the object from an array of data. * Constructs the object from an array of data.
* *
* @param array $data The data to construct the object from. * @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(array $data) public function __construct(string $method, ?string $id, ?array $parameters)
{ {
$this->id = $data['id'] ?? null; $this->method = $method;
$this->method = $data['method']; $this->parameters = $parameters;
$this->parameters = $data['parameters'] ?? null; $this->id = $id;
} }
/** /**
@ -45,13 +52,106 @@ class RpcRequest implements SerializableInterface
/** /**
* Returns the parameters of the request. * Returns the parameters of the request.
* *
* @return array|null The parameters of the request. * @return array|null The parameters of the request, null if the request is a notification.
*/ */
public function getParameters(): ?array public function getParameters(): ?array
{ {
return $this->parameters; 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))
{
return null;
}
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)
{
return null;
}
$valid = false;
if(is_array($result))
{
$valid = true;
}
elseif($result instanceof SerializableInterface)
{
$valid = true;
}
elseif(is_null($result))
{
$valid = true;
}
if(!$valid)
{
throw new InvalidArgumentException('The \'$result\' property must either be array, null or SerializableInterface');
}
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. * Returns an array representation of the object.
* *
@ -74,6 +174,6 @@ class RpcRequest implements SerializableInterface
*/ */
public static function fromArray(array $data): RpcRequest public static function fromArray(array $data): RpcRequest
{ {
return static($data); return new RpcRequest($data['method'], $data['id'] ?? null, $data['parameters'] ?? null);
} }
} }

View file

@ -7,15 +7,15 @@ use Socialbox\Interfaces\SerializableInterface;
class RpcResponse implements SerializableInterface class RpcResponse implements SerializableInterface
{ {
private string $id; private string $id;
private ?object $result; private mixed $result;
/** /**
* Constructs the response object. * Constructs the response object.
* *
* @param string $id The ID of the response. * @param string $id The ID of the response.
* @param object|null $result The result of the response. * @param mixed|null $result The result of the response.
*/ */
public function __construct(string $id, ?object $result) public function __construct(string $id, mixed $result)
{ {
$this->id = $id; $this->id = $id;
$this->result = $result; $this->result = $result;
@ -34,13 +34,37 @@ class RpcResponse implements SerializableInterface
/** /**
* Returns the result of the response. * Returns the result of the response.
* *
* @return object|null The result of the response. * @return mixed|null The result of the response.
*/ */
public function getResult(): ?object public function getResult(): mixed
{ {
return $this->result; 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)
{
return $data->toArray();
}
// If the data is an array, recursively apply this method to each element
if (is_array($data))
{
return array_map([$this, 'convertToArray'], $data);
}
// Otherwise, return the data as-is (e.g., for scalar values)
return $data;
}
/** /**
* Returns an array representation of the object. * Returns an array representation of the object.
* *
@ -50,7 +74,7 @@ class RpcResponse implements SerializableInterface
{ {
return [ return [
'id' => $this->id, 'id' => $this->id,
'result' => $this->result->toArray() 'result' => $this->convertToArray($this->result)
]; ];
} }

View file

@ -0,0 +1,82 @@
<?php
namespace Socialbox\Objects;
use DateTime;
use Socialbox\Enums\SessionState;
use Socialbox\Interfaces\SerializableInterface;
class SessionRecord implements SerializableInterface
{
private string $uuid;
private ?string $authenticatedPeerUuid;
private string $publicKey;
private SessionState $state;
private DateTime $created;
private DateTime $lastRequest;
public function __construct(array $data)
{
$this->uuid = $data['uuid'];
$this->authenticatedPeerUuid = $data['authenticated_peer_uuid'] ?? null;
$this->publicKey = $data['public_key'];
$this->created = $data['created'];
$this->lastRequest = $data['last_request'];
if(SessionState::tryFrom($data['state']) == null)
{
$this->state = SessionState::CLOSED;
}
else
{
$this->state = SessionState::from($data['state']);
}
}
public function getUuid(): string
{
return $this->uuid;
}
public function getAuthenticatedPeerUuid(): ?string
{
return $this->authenticatedPeerUuid;
}
public function getPublicKey(): string
{
return $this->publicKey;
}
public function getState(): SessionState
{
return $this->state;
}
public function getCreated(): int
{
return $this->created;
}
public function getLastRequest(): int
{
return $this->lastRequest;
}
public static function fromArray(array $data): object
{
return new self($data);
}
public function toArray(): array
{
return [
'uuid' => $this->uuid,
'authenticated_peer_uuid' => $this->authenticatedPeerUuid,
'public_key' => $this->publicKey,
'state' => $this->state->value,
'created' => $this->created,
'last_request' => $this->lastRequest,
];
}
}

View file

@ -1,8 +1,54 @@
<?php <?php
namespace socialbox; namespace Socialbox;
use Socialbox\Classes\RpcHandler;
use Socialbox\Enums\StandardError;
use Socialbox\Enums\StandardMethods;
use Socialbox\Exceptions\RpcException;
class Socialbox class Socialbox
{ {
public static function handleRpc(): void
{
try
{
$clientRequest = RpcHandler::getClientRequest();
}
catch(RpcException $e)
{
http_response_code($e->getCode());
print($e->getMessage());
return;
}
$results = [];
foreach($clientRequest->getRequests() as $rpcRequest)
{
$method = StandardMethods::tryFrom($rpcRequest->getMethod());
if($method === false)
{
$response = $rpcRequest->produceError(StandardError::RPC_METHOD_NOT_FOUND, 'The requested method does not exist');;
if($response !== null)
{
$results[] = $response;
}
}
$response = $method->execute($clientRequest, $rpcRequest);
if($response !== null)
{
$results[] = $response;
}
}
if(count($results) > 0)
{
print(json_encode($results));
return;
}
http_response_code(204);
}
} }