2024-09-13 13:47:45 -04:00
|
|
|
<?php
|
|
|
|
|
2024-12-12 04:33:10 -05:00
|
|
|
namespace Socialbox\Objects;
|
|
|
|
|
|
|
|
use InvalidArgumentException;
|
|
|
|
use Socialbox\Classes\Cryptography;
|
|
|
|
use Socialbox\Classes\Utilities;
|
|
|
|
use Socialbox\Enums\SessionState;
|
|
|
|
use Socialbox\Enums\StandardHeaders;
|
|
|
|
use Socialbox\Enums\Types\RequestType;
|
|
|
|
use Socialbox\Exceptions\CryptographyException;
|
|
|
|
use Socialbox\Exceptions\RequestException;
|
2024-12-14 00:43:19 -05:00
|
|
|
use Socialbox\Managers\RegisteredPeerManager;
|
2024-12-12 04:33:10 -05:00
|
|
|
use Socialbox\Managers\SessionManager;
|
2024-12-14 00:43:19 -05:00
|
|
|
use Socialbox\Objects\Database\RegisteredPeerRecord;
|
2024-12-12 04:33:10 -05:00
|
|
|
use Socialbox\Objects\Database\SessionRecord;
|
|
|
|
|
|
|
|
class ClientRequest
|
2024-09-13 13:47:45 -04:00
|
|
|
{
|
2024-12-12 04:33:10 -05:00
|
|
|
private array $headers;
|
|
|
|
private RequestType $requestType;
|
|
|
|
private ?string $requestBody;
|
2024-09-13 13:47:45 -04:00
|
|
|
|
2024-12-12 05:12:28 -05:00
|
|
|
private ?string $clientName;
|
|
|
|
private ?string $clientVersion;
|
2024-12-12 04:33:10 -05:00
|
|
|
private ?string $identifyAs;
|
|
|
|
private ?string $sessionUuid;
|
|
|
|
private ?string $signature;
|
2024-09-13 13:47:45 -04:00
|
|
|
|
2024-12-12 04:33:10 -05:00
|
|
|
public function __construct(array $headers, ?string $requestBody)
|
|
|
|
{
|
|
|
|
$this->headers = $headers;
|
|
|
|
$this->requestBody = $requestBody;
|
|
|
|
|
2024-12-12 05:12:28 -05:00
|
|
|
$this->clientName = $headers[StandardHeaders::CLIENT_NAME->value] ?? null;
|
|
|
|
$this->clientVersion = $headers[StandardHeaders::CLIENT_VERSION->value] ?? null;
|
2024-12-12 04:33:10 -05:00
|
|
|
$this->requestType = RequestType::from($headers[StandardHeaders::REQUEST_TYPE->value]);
|
|
|
|
$this->identifyAs = $headers[StandardHeaders::IDENTIFY_AS->value] ?? null;
|
|
|
|
$this->sessionUuid = $headers[StandardHeaders::SESSION_UUID->value] ?? null;
|
|
|
|
$this->signature = $headers[StandardHeaders::SIGNATURE->value] ?? null;
|
|
|
|
}
|
2024-09-13 13:47:45 -04:00
|
|
|
|
2024-12-12 04:33:10 -05:00
|
|
|
public function getHeaders(): array
|
|
|
|
{
|
|
|
|
return $this->headers;
|
|
|
|
}
|
2024-09-13 13:47:45 -04:00
|
|
|
|
2024-12-12 04:33:10 -05:00
|
|
|
public function headerExists(StandardHeaders|string $header): bool
|
|
|
|
{
|
|
|
|
if(is_string($header))
|
|
|
|
{
|
|
|
|
return isset($this->headers[$header]);
|
|
|
|
}
|
2024-09-13 13:47:45 -04:00
|
|
|
|
2024-12-12 04:33:10 -05:00
|
|
|
return isset($this->headers[$header->value]);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getHeader(StandardHeaders|string $header): ?string
|
2024-09-13 13:47:45 -04:00
|
|
|
{
|
2024-12-12 04:33:10 -05:00
|
|
|
if(!$this->headerExists($header))
|
|
|
|
{
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(is_string($header))
|
|
|
|
{
|
|
|
|
return $this->headers[$header];
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->headers[$header->value];
|
2024-09-13 13:47:45 -04:00
|
|
|
}
|
|
|
|
|
2024-12-12 04:33:10 -05:00
|
|
|
public function getRequestBody(): ?string
|
|
|
|
{
|
|
|
|
return $this->requestBody;
|
|
|
|
}
|
2024-09-13 13:47:45 -04:00
|
|
|
|
2024-12-12 12:58:00 -05:00
|
|
|
public function getClientName(): ?string
|
2024-09-13 13:47:45 -04:00
|
|
|
{
|
2024-12-12 04:33:10 -05:00
|
|
|
return $this->clientName;
|
2024-09-13 13:47:45 -04:00
|
|
|
}
|
|
|
|
|
2024-12-12 12:58:00 -05:00
|
|
|
public function getClientVersion(): ?string
|
2024-12-12 04:33:10 -05:00
|
|
|
{
|
|
|
|
return $this->clientVersion;
|
|
|
|
}
|
2024-09-13 13:47:45 -04:00
|
|
|
|
2024-12-12 04:33:10 -05:00
|
|
|
public function getRequestType(): RequestType
|
2024-09-13 13:47:45 -04:00
|
|
|
{
|
2024-12-12 04:33:10 -05:00
|
|
|
return $this->requestType;
|
2024-09-13 13:47:45 -04:00
|
|
|
}
|
|
|
|
|
2024-12-12 04:33:10 -05:00
|
|
|
public function getIdentifyAs(): ?PeerAddress
|
|
|
|
{
|
|
|
|
if($this->identifyAs === null)
|
|
|
|
{
|
|
|
|
return null;
|
|
|
|
}
|
2024-09-13 13:47:45 -04:00
|
|
|
|
2024-12-12 04:33:10 -05:00
|
|
|
return PeerAddress::fromAddress($this->identifyAs);
|
|
|
|
}
|
2024-09-13 13:47:45 -04:00
|
|
|
|
2024-12-12 04:33:10 -05:00
|
|
|
public function getSessionUuid(): ?string
|
2024-09-13 13:47:45 -04:00
|
|
|
{
|
2024-12-12 04:33:10 -05:00
|
|
|
return $this->sessionUuid;
|
2024-09-13 13:47:45 -04:00
|
|
|
}
|
|
|
|
|
2024-12-12 04:33:10 -05:00
|
|
|
public function getSession(): ?SessionRecord
|
2024-09-30 03:00:02 -04:00
|
|
|
{
|
2024-12-12 04:33:10 -05:00
|
|
|
if($this->sessionUuid === null)
|
|
|
|
{
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return SessionManager::getSession($this->sessionUuid);
|
2024-09-30 03:00:02 -04:00
|
|
|
}
|
2024-12-12 04:33:10 -05:00
|
|
|
|
2024-12-14 00:43:19 -05:00
|
|
|
public function getPeer(): ?RegisteredPeerRecord
|
|
|
|
{
|
|
|
|
$session = $this->getSession();
|
|
|
|
|
|
|
|
if($session === null)
|
|
|
|
{
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return RegisteredPeerManager::getPeer($session->getPeerUuid());
|
|
|
|
}
|
|
|
|
|
2024-12-12 04:33:10 -05:00
|
|
|
public function getSignature(): ?string
|
|
|
|
{
|
|
|
|
return $this->signature;
|
|
|
|
}
|
|
|
|
|
|
|
|
private function verifySignature(string $decryptedContent): bool
|
2024-09-30 03:00:02 -04:00
|
|
|
{
|
2024-12-12 04:33:10 -05:00
|
|
|
if($this->getSignature() == null || $this->getSessionUuid() == null)
|
2024-09-30 03:00:02 -04:00
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2024-12-12 04:33:10 -05:00
|
|
|
try
|
|
|
|
{
|
|
|
|
return Cryptography::verifyContent(hash('sha1', $decryptedContent), $this->getSignature(), $this->getSession()->getPublicKey());
|
|
|
|
}
|
|
|
|
catch(CryptographyException)
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
2024-09-30 03:00:02 -04:00
|
|
|
}
|
2024-09-13 13:47:45 -04:00
|
|
|
|
2024-12-12 04:33:10 -05:00
|
|
|
/**
|
|
|
|
* 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 RequestException Thrown if the request is invalid
|
|
|
|
*/
|
|
|
|
public function getRpcRequests(): array
|
2024-09-30 03:00:02 -04:00
|
|
|
{
|
2024-12-12 04:33:10 -05:00
|
|
|
if($this->getSessionUuid() === null)
|
|
|
|
{
|
|
|
|
throw new RequestException("Session UUID required", 400);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the existing session
|
|
|
|
$session = $this->getSession();
|
|
|
|
|
|
|
|
// If we're awaiting a DHE, encryption is not possible at this point
|
|
|
|
if($session->getState() === SessionState::AWAITING_DHE)
|
|
|
|
{
|
|
|
|
throw new RequestException("DHE exchange required", 400);
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the session is not active, we can't serve these requests
|
|
|
|
if($session->getState() !== SessionState::ACTIVE)
|
|
|
|
{
|
|
|
|
throw new RequestException("Session is not active", 400);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Attempt to decrypt the content and verify the signature of the request
|
|
|
|
try
|
|
|
|
{
|
|
|
|
$decrypted = Cryptography::decryptTransport($this->requestBody, $session->getEncryptionKey());
|
|
|
|
|
|
|
|
if(!$this->verifySignature($decrypted))
|
|
|
|
{
|
|
|
|
throw new RequestException("Invalid request signature", 401);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
catch (CryptographyException $e)
|
|
|
|
{
|
|
|
|
throw new RequestException("Failed to decrypt request body", 400, $e);
|
|
|
|
}
|
|
|
|
|
|
|
|
// At this stage, all checks has passed; we can try parsing the RPC request
|
|
|
|
try
|
|
|
|
{
|
|
|
|
// Decode the request body
|
|
|
|
$body = Utilities::jsonDecode($decrypted);
|
|
|
|
}
|
|
|
|
catch(InvalidArgumentException $e)
|
|
|
|
{
|
|
|
|
throw new RequestException("Invalid JSON in request body: " . $e->getMessage(), 400, $e);
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the body only contains a method, we assume it's a single request
|
|
|
|
if(isset($body['method']))
|
|
|
|
{
|
|
|
|
return [$this->parseRequest($body)];
|
|
|
|
}
|
|
|
|
|
|
|
|
// Otherwise, we assume it's an array of requests
|
|
|
|
return array_map(fn($request) => $this->parseRequest($request), $body);
|
2024-09-30 03:00:02 -04:00
|
|
|
}
|
2024-12-12 04:33:10 -05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Parses the raw request data into an RpcRequest object
|
|
|
|
*
|
|
|
|
* @param array $data The raw request data
|
|
|
|
* @return RpcRequest The parsed RpcRequest object
|
|
|
|
* @throws RequestException If the request is invalid
|
|
|
|
*/
|
|
|
|
private function parseRequest(array $data): RpcRequest
|
2024-09-30 03:00:02 -04:00
|
|
|
{
|
2024-12-12 04:33:10 -05:00
|
|
|
if(!isset($data['method']))
|
|
|
|
{
|
|
|
|
throw new RequestException("Missing 'method' key in request", 400);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(isset($data['id']))
|
|
|
|
{
|
|
|
|
if(!is_string($data['id']))
|
|
|
|
{
|
|
|
|
throw new RequestException("Invalid 'id' key in request: Expected string", 400);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(strlen($data['id']) === 0)
|
|
|
|
{
|
|
|
|
throw new RequestException("Invalid 'id' key in request: Expected non-empty string", 400);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(strlen($data['id']) > 8)
|
|
|
|
{
|
|
|
|
throw new RequestException("Invalid 'id' key in request: Expected string of length <= 8", 400);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(isset($data['parameters']))
|
|
|
|
{
|
|
|
|
if(!is_array($data['parameters']))
|
|
|
|
{
|
|
|
|
throw new RequestException("Invalid 'parameters' key in request: Expected array", 400);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return new RpcRequest($data['method'], $data['id'] ?? null, $data['parameters'] ?? null);
|
2024-09-30 03:00:02 -04:00
|
|
|
}
|
2024-12-12 04:33:10 -05:00
|
|
|
}
|