Add new classes and methods for session management
This commit is contained in:
parent
b9c84aeb27
commit
764ec51fa4
12 changed files with 1026 additions and 28 deletions
118
src/Socialbox/Classes/CacheLayer/RedisCacheLayer.php
Normal file
118
src/Socialbox/Classes/CacheLayer/RedisCacheLayer.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
196
src/Socialbox/Classes/RpcHandler.php
Normal file
196
src/Socialbox/Classes/RpcHandler.php
Normal 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);
|
||||
}
|
||||
}
|
19
src/Socialbox/Classes/StandardMethods/Ping.php
Normal file
19
src/Socialbox/Classes/StandardMethods/Ping.php
Normal 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);
|
||||
}
|
||||
}
|
108
src/Socialbox/Classes/Utilities.php
Normal file
108
src/Socialbox/Classes/Utilities.php
Normal 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;
|
||||
}
|
||||
}
|
19
src/Socialbox/Classes/Validator.php
Normal file
19
src/Socialbox/Classes/Validator.php
Normal 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;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue