Implemented peer syncing (sort-of)

This commit is contained in:
Netkas 2023-06-23 02:05:48 -04:00
parent 2e1a7a612c
commit c93568b8f1
No known key found for this signature in database
GPG key ID: 5DAF58535614062B
10 changed files with 365 additions and 67 deletions

View file

@ -67,51 +67,6 @@
return $name; return $name;
} }
/**
* Parses a federated address into an array of parts.
* Example: entity:uid
*
* @param string $address
* @return array
*/
public static function parseFederatedAddress(string $address): array
{
if (preg_match($address, '/^(?P<entity>[a-zA-Z0-9_-]+):(?P<uid>[a-zA-Z0-9_-]+)$/', $matches, PREG_UNMATCHED_AS_NULL))
{
return [
'entity' => $matches['entity'],
'uid' => $matches['uid']
];
}
throw new InvalidArgumentException(sprintf('Invalid address provided: %s', $address));
}
/**
* Recursively converts a Throwable into an array representation.
*
* @param Throwable $throwable
* @return array
*/
public static function throwableToArray(Throwable $throwable): array
{
$results = [
'message' => $throwable->getMessage(),
'code' => $throwable->getCode(),
'file' => $throwable->getFile(),
'line' => $throwable->getLine(),
'trace' => $throwable->getTrace(),
'previous' => $throwable->getPrevious()
];
if($results['previous'] instanceof Throwable)
{
$results['previous'] = self::throwableToArray($results['previous']);
}
return $results;
}
/** /**
* Uses the z-score method to detect anomalies in an array of numbers. * Uses the z-score method to detect anomalies in an array of numbers.
* The key of the returned array is the index of the number in the original array. * The key of the returned array is the index of the number in the original array.
@ -170,7 +125,7 @@
} }
// Generate a random number between 0 and 1 // Generate a random number between 0 and 1
$rand = mt_rand() / getrandmax(); $rand = mt_rand() / mt_getrandmax();
// Select an item // Select an item
$cumulativeWeight = 0.0; $cumulativeWeight = 0.0;

View file

@ -53,6 +53,10 @@
public const INVALID_PEER_ASSOCIATION_TYPE = 2003; public const INVALID_PEER_ASSOCIATION_TYPE = 2003;
public const INVALID_DATA = 2004;
public const BAD_REQUEST = 2005;
public const ALL = [ public const ALL = [
self::INTERNAL_SERVER_ERROR, self::INTERNAL_SERVER_ERROR,
@ -67,6 +71,10 @@
self::INVALID_PEER_METADATA, self::INVALID_PEER_METADATA,
self::PEER_METADATA_NOT_FOUND, self::PEER_METADATA_NOT_FOUND,
self::INVALID_FEDERATED_ADDRESS, self::INVALID_FEDERATED_ADDRESS,
self::INVALID_PEER_ASSOCIATION_TYPE self::INVALID_PEER_ASSOCIATION_TYPE,
self::INVALID_DATA,
self::BAD_REQUEST
]; ];
// TOOD: Re-organize all the error codes to make sense, this is not final.
} }

View file

@ -6,26 +6,22 @@
{ {
public const PING = 'ping'; public const PING = 'ping';
public const WHOAMI = 'whoami'; public const WHOAMI = 'whoami';
public const CREATE_CLIENT = 'create_client'; public const CREATE_CLIENT = 'create_client';
public const GET_CLIENT = 'get_client'; public const GET_CLIENT = 'get_client';
public const UPDATE_CLIENT_NAME = 'update_client_name'; public const UPDATE_CLIENT_NAME = 'update_client_name';
public const UPDATE_CLIENT_DESCRIPTION = 'update_client_description'; public const UPDATE_CLIENT_DESCRIPTION = 'update_client_description';
public const UPDATE_CLIENT_PERMISSION_ROLE = 'update_client_permission_role'; public const UPDATE_CLIENT_PERMISSION_ROLE = 'update_client_permission_role';
public const SYNC_PEER = 'sync_peer'; public const SYNC_PEER = 'sync_peer';
public const ALL = [ public const ALL = [
self::PING, self::PING,
self::WHOAMI, self::WHOAMI,
self::CREATE_CLIENT, self::CREATE_CLIENT,
self::GET_CLIENT, self::GET_CLIENT,
self::UPDATE_CLIENT_NAME, self::UPDATE_CLIENT_NAME,
self::UPDATE_CLIENT_DESCRIPTION, self::UPDATE_CLIENT_DESCRIPTION,
self::UPDATE_CLIENT_PERMISSION_ROLE, self::UPDATE_CLIENT_PERMISSION_ROLE,
self::SYNC_PEER, self::SYNC_PEER,
]; ];
} }

View file

@ -0,0 +1,19 @@
<?php
namespace FederationLib\Exceptions\Standard;
use Exception;
use FederationLib\Enums\Standard\ErrorCodes;
use Throwable;
class BadRequestException extends Exception
{
/**
* @param string $message
* @param Throwable|null $previous
*/
public function __construct(string $message = "", ?Throwable $previous = null)
{
parent::__construct($message, ErrorCodes::BAD_REQUEST, $previous);
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace FederationLib\Exceptions\Standard;
use Exception;
use FederationLib\Enums\Standard\ErrorCodes;
use Throwable;
class InvalidDataException extends Exception
{
/**
* @param string $message
* @param Throwable|null $previous
*/
public function __construct(string $message = "", ?Throwable $previous = null)
{
parent::__construct($message, ErrorCodes::INVALID_DATA, $previous);
}
}

View file

@ -12,11 +12,17 @@
use FederationLib\Exceptions\Standard\InternalServerException; use FederationLib\Exceptions\Standard\InternalServerException;
use FederationLib\Exceptions\Standard\InvalidClientDescriptionException; use FederationLib\Exceptions\Standard\InvalidClientDescriptionException;
use FederationLib\Exceptions\Standard\InvalidClientNameException; use FederationLib\Exceptions\Standard\InvalidClientNameException;
use FederationLib\Exceptions\Standard\InvalidFederatedAddressException;
use FederationLib\Exceptions\Standard\InvalidPeerAssociationTypeException;
use FederationLib\Exceptions\Standard\InvalidPeerMetadataException;
use FederationLib\Exceptions\Standard\UnsupportedPeerType;
use FederationLib\Managers\AssociationManager;
use FederationLib\Managers\ClientManager; use FederationLib\Managers\ClientManager;
use FederationLib\Managers\PeerManager; use FederationLib\Managers\PeerManager;
use FederationLib\Objects\ClientRecord; use FederationLib\Objects\ClientRecord;
use FederationLib\Objects\ResolvedIdentity; use FederationLib\Objects\ResolvedIdentity;
use FederationLib\Objects\Standard\ClientIdentity; use FederationLib\Objects\Standard\ClientIdentity;
use FederationLib\Objects\Standard\PeerUpdate;
use TamerLib\Enums\TamerMode; use TamerLib\Enums\TamerMode;
use TamerLib\tm; use TamerLib\tm;
use Throwable; use Throwable;
@ -33,6 +39,11 @@
*/ */
private PeerManager $peer_manager; private PeerManager $peer_manager;
/**
* @var AssociationManager
*/
private AssociationManager $association_manager;
/** /**
* FederationLib constructor. * FederationLib constructor.
*/ */
@ -40,6 +51,7 @@
{ {
$this->client_manager = new ClientManager($this); $this->client_manager = new ClientManager($this);
$this->peer_manager = new PeerManager($this); $this->peer_manager = new PeerManager($this);
$this->association_manager = new AssociationManager($this);
} }
/** /**
@ -346,19 +358,25 @@
} }
/** /**
* Syncs the peer to the database, this requires the peer's metadata to be set so that the peer can be
* registered or updated if it already exists, additionally more information about the peer can be set
* such as the peer's association.
*
* Returns True if successful
*
* @param ClientIdentity|null $identity * @param ClientIdentity|null $identity
* @param string $federated_address * @param PeerUpdate $peer_update
* @param array $metadata
* @return bool * @return bool
* @throws AccessDeniedException * @throws AccessDeniedException
* @throws ClientNotFoundException * @throws ClientNotFoundException
* @throws DatabaseException * @throws DatabaseException
* @throws Exceptions\Standard\InvalidFederatedAddressException * @throws InvalidFederatedAddressException
* @throws Exceptions\Standard\InvalidPeerMetadataException * @throws InvalidPeerAssociationTypeException
* @throws Exceptions\Standard\UnsupportedPeerType * @throws InvalidPeerMetadataException
* @throws UnsupportedPeerType
* @throws InternalServerException * @throws InternalServerException
*/ */
public function syncPeer(?ClientIdentity $identity, string $federated_address, array $metadata): bool public function syncPeer(?ClientIdentity $identity, PeerUpdate $peer_update): bool
{ {
if(!$this->checkPermission(Methods::SYNC_PEER, $this->resolveIdentity($identity))) if(!$this->checkPermission(Methods::SYNC_PEER, $this->resolveIdentity($identity)))
{ {
@ -370,20 +388,42 @@
throw new Exceptions\Standard\AccessDeniedException('You must be authenticated to sync a peer'); throw new Exceptions\Standard\AccessDeniedException('You must be authenticated to sync a peer');
} }
try if($peer_update->getMetadata() === null)
{ {
$this->peer_manager->syncPeer($identity->getClientUuid(), $federated_address, $metadata); throw new Exceptions\Standard\InvalidPeerMetadataException('You must provide metadata to sync a peer');
} }
catch(Exception $e)
{
if(in_array($e->getCode(), ErrorCodes::ALL, true))
{
throw $e;
}
throw new Exceptions\Standard\InternalServerException('There was an error while syncing the peer', $e); // TODO: Make this run in parallel
// Update the metadata if there's any
if($peer_update->getMetadata() !== null)
{
try
{
$this->peer_manager->syncPeer($identity->getClientUuid(), $peer_update->getAddress(), $peer_update->getMetadata());
}
catch(Exception $e)
{
if(in_array($e->getCode(), ErrorCodes::ALL, true))
{
throw $e;
}
throw new Exceptions\Standard\InternalServerException('There was an error while syncing the peer', $e);
}
}
// Update the association if there's one
if($peer_update->getAssociation() !== null)
{
$association = $peer_update->getAssociation();
/** @noinspection NullPointerExceptionInspection */
$this->association_manager->associate($identity->getClientUuid(),
$peer_update->getAddress(), $association->getParent(), $association->getType()
);
} }
return true; return true;
} }
} }

View file

@ -0,0 +1,13 @@
<?php
namespace FederationLib\Interfaces;
interface ValidateInterface
{
/**
* @return void
*/
public function validate(): void;
// TODO: There could be a better way to do this
}

View file

@ -46,7 +46,7 @@
* @throws DatabaseException * @throws DatabaseException
* @throws InvalidPeerAssociationTypeException * @throws InvalidPeerAssociationTypeException
*/ */
public function associate(ClientRecord|string $client_uuid, ParsedFederatedAddress|string $parent, ParsedFederatedAddress|string $child, string $type): void public function associate(ClientRecord|string $client_uuid, ParsedFederatedAddress|string $child, ParsedFederatedAddress|string $parent, string $type): void
{ {
if(!Validate::peerAssociationType($type)) if(!Validate::peerAssociationType($type))
{ {

View file

@ -0,0 +1,140 @@
<?php
namespace FederationLib\Objects\Standard;
use FederationLib\Classes\Security;
use FederationLib\Classes\Validate;
use FederationLib\Exceptions\Standard\InvalidDataException;
use FederationLib\Interfaces\SerializableObjectInterface;
use FederationLib\Interfaces\ValidateInterface;
use FederationLib\Objects\ParsedFederatedAddress;
use FederationLib\Objects\Standard\PeerUpdate\Association;
use InvalidArgumentException;
class PeerUpdate implements SerializableObjectInterface, ValidateInterface
{
/**
* @var string
*/
private $address;
/**
* @var array|null
*/
private $metadata;
/**
* @var Association|null
*/
private $association;
/**
* Validates the given object data, throws an exception if invalid.
*
* @return void
* @throws InvalidDataException
*/
public function validate(): void
{
if(!is_string($this->address))
{
throw new InvalidDataException(sprintf('Peer\'s address must be a string, %s given.', Security::gettype($this->address)));
}
try
{
$parsed_address = new ParsedFederatedAddress($this->address);
}
catch(InvalidArgumentException $e)
{
throw new InvalidDataException(sprintf('Peer\'s address must be a valid federated address, %s given.', $this->address), $e);
}
if(!Validate::validateEntityType($parsed_address->getPeerType()))
{
throw new InvalidDataException(sprintf('Peer\'s address must be a valid peer type, %s given.', $parsed_address->getPeerType()));
}
if(!is_null($this->metadata) && !is_array($this->metadata))
{
throw new InvalidDataException(sprintf('Peer\'s metadata must be an array or null, %s given.', Security::gettype($this->metadata)));
}
if(!is_null($this->association))
{
$this->association->validate();
}
}
/**
* Returns the federated address of the peer.
*
* @return string
*/
public function getAddress(): string
{
return $this->address;
}
/**
* Optional. Returns the metadata of the peer, or null if none.
*
* @return array|null
*/
public function getMetadata(): ?array
{
return $this->metadata;
}
/**
* Optional. Returns the association of the peer, or null if none.
*
* @return Association|null
*/
public function getAssociation(): ?Association
{
return $this->association;
}
/**
* Returns an array representation of the string
*
* @return array
*/
public function toArray(): array
{
$association = null;
if($association instanceof Association)
{
$association = $this->association->toArray();
}
return [
'address' => $this->address,
'metadata' => $this->metadata,
'association' => $association
];
}
/**
* Constructs the object from an array representation
*
* @param array $array
* @return PeerUpdate
*/
public static function fromArray(array $array): PeerUpdate
{
$object = new self();
$object->address = $array['address'];
$object->metadata = $array['metadata'];
if(isset($array['association']))
{
$object->association = Association::fromArray($array['association']);
}
return $object;
}
}

View file

@ -0,0 +1,108 @@
<?php
/** @noinspection PhpMissingFieldTypeInspection */
namespace FederationLib\Objects\Standard\PeerUpdate;
use FederationLib\Classes\Security;
use FederationLib\Classes\Validate;
use FederationLib\Exceptions\Standard\InvalidDataException;
use FederationLib\Interfaces\SerializableObjectInterface;
use FederationLib\Interfaces\ValidateInterface;
use FederationLib\Objects\ParsedFederatedAddress;
use InvalidArgumentException;
class Association implements SerializableObjectInterface, ValidateInterface
{
/**
* @var string
*/
private $parent;
/**
* @var string
*/
private $type;
/**
* Validates the given object data, throws an exception if invalid.
*
* @return void
* @throws InvalidDataException
*/
public function validate(): void
{
if(!is_string($this->parent))
{
throw new InvalidDataException(sprintf('Peer\'s assocation parent must be a string, %s given.', Security::gettype($this->parent)));
}
if(!is_string($this->type))
{
throw new InvalidDataException(sprintf('Peer\'s assocation type must be a string, %s given.', Security::gettype($this->type)));
}
try
{
$parsed_address = new ParsedFederatedAddress($this->parent);
}
catch(InvalidArgumentException $e)
{
throw new InvalidDataException(sprintf('Peer\'s association parent must be a valid federated address, %s given.', $this->parent), $e);
}
if(!Validate::validateEntityType($parsed_address->getPeerType()))
{
throw new InvalidDataException(sprintf('Peer\'s association parent must be a valid peer type, %s given.', $parsed_address->getPeerType()));
}
}
/**
* Returns the parent of the association.
*
* @return string
*/
public function getParent(): string
{
return $this->parent;
}
/**
* Returns the type of association.
*
* @return string
*/
public function getType(): string
{
return $this->type;
}
/**
* Returns an array representation of the object.
*
* @return array
*/
public function toArray(): array
{
return [
'parent' => $this->parent,
'type' => $this->type,
];
}
/**
* Constructs the object from an array representation.
*
* @param array $array
* @return Association
*/
public static function fromArray(array $array): Association
{
$object = new self();
$object->parent = $array['parent'];
$object->type = $array['type'];
return $object;
}
}