From 6e599b2c0c1fb9faaf3fb84fc3cbef9637dcd01f Mon Sep 17 00:00:00 2001 From: Netkas Date: Sun, 4 Jun 2023 14:23:51 -0400 Subject: [PATCH] Initial Commit --- .gitignore | 3 +- .idea/.gitignore | 0 .idea/.name | 1 + .../{federationlib.iml => FederationLib.iml} | 2 + .idea/dataSources.xml | 21 + .idea/inspectionProfiles/Project_Default.xml | 0 .idea/modules.xml | 2 +- .idea/php.xml | 114 + .idea/runConfigurations/Build.xml | 0 .idea/runConfigurations/Clean.xml | 0 .idea/runConfigurations/Install.xml | 0 .idea/runConfigurations/base32_test_php.xml | 7 + .../cache_update_test_php.xml | 7 + .../runConfigurations/create_clients_php.xml | 7 + .../create_secured_client_php.xml | 7 + .idea/runConfigurations/list_clients_php.xml | 7 + .idea/runConfigurations/totp_test_php.xml | 7 + .idea/vcs.xml | 0 .idea/webResources.xml | 14 + Makefile | 0 README.md | 4 + STANDARD.md | 242 + assets/federated_email_address.png | Bin 0 -> 29550 bytes assets/federated_ipv4.png | Bin 0 -> 21380 bytes assets/federated_telegram_user.png | Bin 0 -> 24643 bytes assets/recon_example.png | Bin 0 -> 45933 bytes database/anomaly_tracking.sql | 23 + database/associations.sql | 29 + database/clients.sql | 34 + database/events.sql | 35 + database/peers.sql | 29 + database/peers_telegram_chat.sql | 23 + database/peers_telegram_user.sql | 23 + database/query_documents.sql | 47 + database/reports.sql | 45 + database/restrictions.sql | 28 + main | 6 + project.json | 54 +- src/FederationCLI/InteractiveMode.php | 149 + .../InteractiveMode/ClientManager.php | 190 + .../InteractiveMode/ConfigurationManager.php | 113 + src/FederationCLI/Program.php | 41 + src/FederationCLI/Utilities.php | 101 + src/FederationLib/Classes/Base32.php | 84 + src/FederationLib/Classes/CacheSystem.php | 7 + src/FederationLib/Classes/Configuration.php | 226 + .../CacheServerConfiguration.php | 162 + .../CacheSystemConfiguration.php | 144 + src/FederationLib/Classes/Data/wordlist.txt | 6082 +++++++++++++++++ src/FederationLib/Classes/Database.php | 88 + src/FederationLib/Classes/Memcached.php | 18 + src/FederationLib/Classes/Redis.php | 91 + src/FederationLib/Classes/Security.php | 102 + src/FederationLib/Classes/Utilities.php | 183 + src/FederationLib/Classes/Validate.php | 60 + src/FederationLib/Enums/CacheDriver.php | 9 + src/FederationLib/Enums/DatabaseTables.php | 30 + src/FederationLib/Enums/EventPriority.php | 11 + src/FederationLib/Enums/FilterOrder.php | 9 + .../Enums/Filters/ListClientsFilter.php | 18 + src/FederationLib/Enums/Misc.php | 8 + .../Enums/SerializationMethod.php | 9 + .../Enums/Standard/AttachmentDataType.php | 12 + .../Enums/Standard/ContentType.php | 15 + .../Enums/Standard/DocumentSubjectType.php | 12 + .../Enums/Standard/ErrorCodes.php | 55 + .../Enums/Standard/EventCode.php | 116 + .../Enums/Standard/EventType.php | 16 + .../Enums/Standard/InternetPeerType.php | 15 + .../Enums/Standard/PeerAssociationType.php | 48 + src/FederationLib/Enums/Standard/PeerType.php | 18 + .../Enums/Standard/ReportType.php | 76 + src/FederationLib/Enums/Standard/ScanMode.php | 12 + .../Enums/Standard/UserPeerType.php | 12 + .../Enums/UserEntityRelationType.php | 8 + .../Exceptions/DatabaseException.php | 26 + .../Exceptions/RedisException.php | 20 + .../Standard/ClientNotFoundException.php | 19 + .../Standard/InvalidClientNameException.php | 19 + .../Standard/PeerNotFoundException.php | 20 + .../Standard/UnsupportedPeerType.php | 20 + .../Exceptions/UnsupportedEntityType.php | 19 + src/FederationLib/FederationLib.php | 38 + .../Interfaces/CacheDriverInterface.php | 47 + .../Interfaces/EntityObjectInterface.php | 20 + .../SerializableObjectInterface.php | 21 + src/FederationLib/Managers/ClientManager.php | 503 ++ src/FederationLib/Managers/PeerManager.php | 172 + src/FederationLib/Objects/Client.php | 380 + .../Objects/ParsedFederatedAddress.php | 78 + src/FederationLib/Objects/Peer.php | 138 + src/FederationLib/Objects/QueryDocument.php | 17 + src/README.md | 14 +- tests/base32_test.php | 18 + tests/client_manager/cache_update_test.php | 25 + tests/client_manager/create_client.php | 14 + .../client_manager/create_secured_client.php | 18 + tests/client_manager/list_clients.php | 8 + tests/totp_test.php | 15 + 99 files changed, 10836 insertions(+), 4 deletions(-) mode change 100644 => 100755 .gitignore mode change 100644 => 100755 .idea/.gitignore create mode 100644 .idea/.name rename .idea/{federationlib.iml => FederationLib.iml} (76%) create mode 100644 .idea/dataSources.xml mode change 100644 => 100755 .idea/inspectionProfiles/Project_Default.xml mode change 100644 => 100755 .idea/runConfigurations/Build.xml mode change 100644 => 100755 .idea/runConfigurations/Clean.xml mode change 100644 => 100755 .idea/runConfigurations/Install.xml create mode 100644 .idea/runConfigurations/base32_test_php.xml create mode 100644 .idea/runConfigurations/cache_update_test_php.xml create mode 100644 .idea/runConfigurations/create_clients_php.xml create mode 100644 .idea/runConfigurations/create_secured_client_php.xml create mode 100644 .idea/runConfigurations/list_clients_php.xml create mode 100644 .idea/runConfigurations/totp_test_php.xml mode change 100644 => 100755 .idea/vcs.xml create mode 100644 .idea/webResources.xml mode change 100644 => 100755 Makefile create mode 100644 README.md create mode 100755 STANDARD.md create mode 100644 assets/federated_email_address.png create mode 100644 assets/federated_ipv4.png create mode 100644 assets/federated_telegram_user.png create mode 100644 assets/recon_example.png create mode 100644 database/anomaly_tracking.sql create mode 100644 database/associations.sql create mode 100644 database/clients.sql create mode 100644 database/events.sql create mode 100644 database/peers.sql create mode 100644 database/peers_telegram_chat.sql create mode 100644 database/peers_telegram_user.sql create mode 100644 database/query_documents.sql create mode 100644 database/reports.sql create mode 100644 database/restrictions.sql create mode 100644 main mode change 100644 => 100755 project.json create mode 100644 src/FederationCLI/InteractiveMode.php create mode 100644 src/FederationCLI/InteractiveMode/ClientManager.php create mode 100644 src/FederationCLI/InteractiveMode/ConfigurationManager.php create mode 100644 src/FederationCLI/Program.php create mode 100644 src/FederationCLI/Utilities.php create mode 100644 src/FederationLib/Classes/Base32.php create mode 100644 src/FederationLib/Classes/CacheSystem.php create mode 100644 src/FederationLib/Classes/Configuration.php create mode 100644 src/FederationLib/Classes/Configuration/CacheServerConfiguration.php create mode 100644 src/FederationLib/Classes/Configuration/CacheSystemConfiguration.php create mode 100644 src/FederationLib/Classes/Data/wordlist.txt create mode 100644 src/FederationLib/Classes/Database.php create mode 100644 src/FederationLib/Classes/Memcached.php create mode 100644 src/FederationLib/Classes/Redis.php create mode 100644 src/FederationLib/Classes/Security.php create mode 100644 src/FederationLib/Classes/Utilities.php create mode 100644 src/FederationLib/Classes/Validate.php create mode 100644 src/FederationLib/Enums/CacheDriver.php create mode 100644 src/FederationLib/Enums/DatabaseTables.php create mode 100644 src/FederationLib/Enums/EventPriority.php create mode 100644 src/FederationLib/Enums/FilterOrder.php create mode 100644 src/FederationLib/Enums/Filters/ListClientsFilter.php create mode 100644 src/FederationLib/Enums/Misc.php create mode 100644 src/FederationLib/Enums/SerializationMethod.php create mode 100644 src/FederationLib/Enums/Standard/AttachmentDataType.php create mode 100644 src/FederationLib/Enums/Standard/ContentType.php create mode 100644 src/FederationLib/Enums/Standard/DocumentSubjectType.php create mode 100644 src/FederationLib/Enums/Standard/ErrorCodes.php create mode 100644 src/FederationLib/Enums/Standard/EventCode.php create mode 100644 src/FederationLib/Enums/Standard/EventType.php create mode 100644 src/FederationLib/Enums/Standard/InternetPeerType.php create mode 100644 src/FederationLib/Enums/Standard/PeerAssociationType.php create mode 100644 src/FederationLib/Enums/Standard/PeerType.php create mode 100644 src/FederationLib/Enums/Standard/ReportType.php create mode 100644 src/FederationLib/Enums/Standard/ScanMode.php create mode 100644 src/FederationLib/Enums/Standard/UserPeerType.php create mode 100644 src/FederationLib/Enums/UserEntityRelationType.php create mode 100644 src/FederationLib/Exceptions/DatabaseException.php create mode 100644 src/FederationLib/Exceptions/RedisException.php create mode 100644 src/FederationLib/Exceptions/Standard/ClientNotFoundException.php create mode 100644 src/FederationLib/Exceptions/Standard/InvalidClientNameException.php create mode 100644 src/FederationLib/Exceptions/Standard/PeerNotFoundException.php create mode 100644 src/FederationLib/Exceptions/Standard/UnsupportedPeerType.php create mode 100644 src/FederationLib/Exceptions/UnsupportedEntityType.php mode change 100644 => 100755 src/FederationLib/FederationLib.php create mode 100644 src/FederationLib/Interfaces/CacheDriverInterface.php create mode 100644 src/FederationLib/Interfaces/EntityObjectInterface.php create mode 100644 src/FederationLib/Interfaces/SerializableObjectInterface.php create mode 100644 src/FederationLib/Managers/ClientManager.php create mode 100644 src/FederationLib/Managers/PeerManager.php create mode 100755 src/FederationLib/Objects/Client.php create mode 100644 src/FederationLib/Objects/ParsedFederatedAddress.php create mode 100644 src/FederationLib/Objects/Peer.php create mode 100644 src/FederationLib/Objects/QueryDocument.php mode change 100644 => 100755 src/README.md create mode 100644 tests/base32_test.php create mode 100644 tests/client_manager/cache_update_test.php create mode 100644 tests/client_manager/create_client.php create mode 100644 tests/client_manager/create_secured_client.php create mode 100644 tests/client_manager/list_clients.php create mode 100644 tests/totp_test.php diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index d163863..afe8786 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -build/ \ No newline at end of file +build/ +/scratch/ diff --git a/.idea/.gitignore b/.idea/.gitignore old mode 100644 new mode 100755 diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..2d67cd2 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +FederationLib \ No newline at end of file diff --git a/.idea/federationlib.iml b/.idea/FederationLib.iml similarity index 76% rename from .idea/federationlib.iml rename to .idea/FederationLib.iml index 4126d6f..e9afffb 100644 --- a/.idea/federationlib.iml +++ b/.idea/FederationLib.iml @@ -3,7 +3,9 @@ + + diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..8a4726b --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,21 @@ + + + + + mariadb + true + A local test database + org.mariadb.jdbc.Driver + jdbc:mariadb://localhost:3306/federation + $ProjectFileDir$ + + + redis + true + A local test Redis Server + jdbc.RedisDriver + jdbc:redis://127.0.0.1:6379/0 + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml old mode 100644 new mode 100755 diff --git a/.idea/modules.xml b/.idea/modules.xml index 0d5a0df..c592fc9 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml index 2c7445e..432a2aa 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -9,6 +9,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.idea/runConfigurations/Build.xml b/.idea/runConfigurations/Build.xml old mode 100644 new mode 100755 diff --git a/.idea/runConfigurations/Clean.xml b/.idea/runConfigurations/Clean.xml old mode 100644 new mode 100755 diff --git a/.idea/runConfigurations/Install.xml b/.idea/runConfigurations/Install.xml old mode 100644 new mode 100755 diff --git a/.idea/runConfigurations/base32_test_php.xml b/.idea/runConfigurations/base32_test_php.xml new file mode 100644 index 0000000..ab7305d --- /dev/null +++ b/.idea/runConfigurations/base32_test_php.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/cache_update_test_php.xml b/.idea/runConfigurations/cache_update_test_php.xml new file mode 100644 index 0000000..184fb1f --- /dev/null +++ b/.idea/runConfigurations/cache_update_test_php.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/create_clients_php.xml b/.idea/runConfigurations/create_clients_php.xml new file mode 100644 index 0000000..dbe6ab1 --- /dev/null +++ b/.idea/runConfigurations/create_clients_php.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/create_secured_client_php.xml b/.idea/runConfigurations/create_secured_client_php.xml new file mode 100644 index 0000000..ecf9400 --- /dev/null +++ b/.idea/runConfigurations/create_secured_client_php.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/list_clients_php.xml b/.idea/runConfigurations/list_clients_php.xml new file mode 100644 index 0000000..625a189 --- /dev/null +++ b/.idea/runConfigurations/list_clients_php.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/totp_test_php.xml b/.idea/runConfigurations/totp_test_php.xml new file mode 100644 index 0000000..73caab9 --- /dev/null +++ b/.idea/runConfigurations/totp_test_php.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml old mode 100644 new mode 100755 diff --git a/.idea/webResources.xml b/.idea/webResources.xml new file mode 100644 index 0000000..cbf6834 --- /dev/null +++ b/.idea/webResources.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Makefile b/Makefile old mode 100644 new mode 100755 diff --git a/README.md b/README.md new file mode 100644 index 0000000..ff3cc90 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# FederationLib + +Please note that this project is currently in development and is not ready for use. +See the [STANDARD.md](STANDARD.md) file for more information. \ No newline at end of file diff --git a/STANDARD.md b/STANDARD.md new file mode 100755 index 0000000..6dfa733 --- /dev/null +++ b/STANDARD.md @@ -0,0 +1,242 @@ +# Internet Federation Database Standard (IFDS) Version 1 + +IFDS (Internet Federation Database Standard) is a standard database system to which allows internet clients both +authorized and public users to query the database to check if the given content or entities involved in the +communication could be classified as spam or malicious, allowing a client to take place protections against +spam and malicious content on various communication channels such as email, social media, messaging apps, and more. + +# Introduction + +# Table of Contents + + +* [Internet Federation Database Standard (IFDS) Version 1](#internet-federation-database-standard-ifds-version-1) +* [Introduction](#introduction) +* [Table of Contents](#table-of-contents) +* [Definitions](#definitions) +* [Clients](#clients) + * [Client TOTP Signature](#client-totp-signature) + * [Client Object](#client-object) +* [Federation Standard](#federation-standard) + * [Standard Federated Addresses](#standard-federated-addresses) +* [Query Document](#query-document) + * [QueryDocument Versioning](#querydocument-versioning) + * [QueryDocument Subject Types](#querydocument-subject-types) + * [RECON](#recon) + * [ANALYZE](#analyze) + * [REPORT](#report) + * [QueryDocument Event Types](#querydocument-event-types) + * [QueryDocument Object](#querydocument-object) + * [QueryDocument.Peer Object](#querydocumentpeer-object) + * [QueryDocument.Content Object](#querydocumentcontent-object) + * [QueryDocument.Attachment Object](#querydocumentattachment-object) + * [QueryDocument.Report Object](#querydocumentreport-object) +* [Platforms](#platforms) + * [Telegram](#telegram) + + +# Definitions + +Some definitions used in this document, these definitions are not official and are only used to provide a better +understanding of the document and the purposes behind features and fields. + + - **Peer** - Everything from users, bots, channels, groups, and more is considered a peer, a peer is represented by + a standard federated address, see [Standard Federated Addresses](#standard-federated-addresses) for more information, + a peer could be a parent or child of another peer, for example, a user could be a child of a group or channel with + an association type of "member", a peer could also be a parent of another peer, for example, a group or channel + could be a parent of a user with an association type of "owner" or "admin", these associations are only made known + by trusted clients if specified in the QueryDocument, see [QueryDocument](#querydocument) for more information. + + - **Discovery** - The process of using the provided information in a QueryDocument to discover & collect information + about the content or entities involved in the communication, allowing the database to keep a record of or to + have a better understanding of peer associations, content, and more. Discovery is not required for a QueryDocument + to be valid, however, it is recommended to provide as much information as possible to allow the database to + have a better understanding of the content or entities involved in the communication. + + +# Clients + +TODO: Write this section + + +## Client TOTP Signature + +TODO: Write this section + + +## Client Object + +TODO: Write this section + + +# Federation Standard + +The federation standard is a standard which defines how clients should communicate with servers, the standard + +## Standard Federated Addresses + +A standard federated address is a universally unique identifier which can represent multiple types of peers from +different platforms, federated address formats are standardized so only one format is required to be used by clients +and allows servers to reject requests to platforms that are not supported by the server. + +![Federated Telegram User](assets/federated_telegram_user.png) +![Federated Email Address](assets/federated_email_address.png) +![Federated IPV4](assets/federated_ipv4.png) + +The format of a standard federated address is as follows: + +`source`.`type`:`id` + + - `source` - The platform/source where the peer is from, must be a supported platform, see [Platform](#platform) + - `type` - The type of peer, peer types are unique to the platform, see the platform's documentation for more information + - `id` - The unique identifier of the peer, the identifier must be unique to the platform, see the platform's documentation for more information + +Even though the value types are defined by the platform's specifications for the federated address, the end result +will always be a parsable address that can be used by the server to identify the peer. + +When coming up with a federated address standard for a platform, it is important to keep in mind that `id` must be +unique to the peer on the platform, for example, if a platform has a username system it is best to avoid using something +that can be changed by the user such as a username, instead, use something that is unique to the peer such as an ID or +UUID provided by the platform, this will ensure that the federated address will always be valid and will always point +to the correct peer even if the peer changes their username or other information that isn't unique to the peer. + +For a regex pattern to match a standard federated address, see the following: + +```regex +(?[a-z0-9]+)\.(?[a-z0-9]+):(?.+) +``` + +# Query Document + +QueryDocument is a query document constructed by the client which presents the contents of the message or event to +either be put into discovery, analyzed for spam or malicious content, or report an event where a report has been +produced either by a client's automated system or by manually by a user. + +## QueryDocument Versioning + +The version of the QueryDocument is represented by the `version` field, the version must always be "1" for now, future +versions of the QueryDocument structure may be introduced in the future, to ensure backwards compatibility, the +version field must be checked to ensure the server can parse the document correctly. If the server does not support +the version of the document, the server must reject the document and return an error to the client. + +## QueryDocument Subject Types + +The subject type of the QueryDocument is represented by the `subject_type` field, the subject type must be one of the +following values: + +TODO: Write this section + + +### RECON + +![RECON Example](assets/recon_example.png) + +TODO: Write this section + + +### ANALYZE + +TODO: Write this section + + +### REPORT + +TODO: Write this section + + +## QueryDocument Event Types + +The event type of the QueryDocument is represented by the `event_type` field, the event type must be one of the +following values: + + - **GENERAL** - The event is a general event that does not fit into any other event type, the `GENERAL` event type is + used for events such as a general update to a peer's activity or status that could be used to keep the database + up to date with the peer's activity or status. + - **INCOMING** - The event is an incoming message/request from a peer, the `INCOMING` event type is used for events such + as a message/request from a peer that was captured by the client. + - **OUTGOING** - The event is an outgoing message/request to a peer, the `OUTGOING` event type is used for events such + as a message/request to a peer that was captured by the client. + - **PEER_JOIN** - The event is a peer join event, the `PEER_JOIN` event type is used for events such when a peer joins + or connects to the `channel_peer` + - **PEER_LEAVE** - The event is a peer leave event, the `PEER_LEAVE` event type is used for events such when a peer + leaves or disconnects from the `channel_peer` + - **PEER_BAN** - The event is a peer ban event, the `PEER_BAN` event type is used for events such when a peer is banned + from the `channel_peer`, in such cases the `from_peer` is the peer that banned the `to_peer` and the `to_peer` + is the peer that was banned. + - **PEER_UNBAN** - The event is a peer unban event, the `PEER_UNBAN` event type is used for events such when a peer is + unbanned from the `channel_peer`, in such cases the `from_peer` is the peer that unbanned the `to_peer` and the + `to_peer` is the peer that was unbanned. + - **PEER_KICK** - The event is a peer kick event, the `PEER_KICK` event type is used for events such when a peer is + kicked from the `channel_peer`, in such cases the `from_peer` is the peer that kicked the `to_peer` and the + `to_peer` is the peer that was kicked. + - **PEER_RESTRICT** - The event is a peer restrict event, the `PEER_RESTRICT` event type is used for events such when a + peer is restricted from the `channel_peer`, in such cases the `from_peer` is the peer that restricted the `to_peer` + and the `to_peer` is the peer that was restricted. + - **ANNOUNCEMENT** - The event is an announcement event, the `ANNOUNCEMENT` event type is used for events such as an + announcement from the `channel_peer` that was captured by the client, optionally, the `from_peer` field can be + used to specify the peer that made the announcement, if the `from_peer` field is not specified, the announcement + is assumed to be from the `channel_peer + +Events are used to give the database context about what's happening in the channel, for example, a server with +anomaly detection may use the events to determine if a peer is sending too many messages in a short period of time +or a channel is being raided by a group of peers. + +In all other cases `GENERAL` may be used if the client is simply doing RECON requests to keep the database up to date +with the peer's activity or status. + +## QueryDocument Object + +The QueryDocument is a JSON object which contains the following fields: + +| Name | Type | Required | Example Value(s) | Description | +|-------------------------|--------------------------------------------|----------|-----------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `version` | `string` | Yes | `1` | The version of the QueryDocument, must always be "1" | +| `subject_type` | `string` | Yes | `RECON`, `ANALYZE` or `REPORT` | The subject type, must be "**RECON**", "**ANALYZE**" or "**REPORT**" | +| `client_id` | `string` | Yes | `00000000-0000-0000-0000-000000000000` | The client's unique UUIDv4 identifier to identify who the document is from | +| `client_totp_signature` | `string` | No | `a94a8fe5ccb19ba61c4c0873d391e987982fbbd3` | The client's TOTP signature to verify the client's identity with the document's reported timestamp, if the client does not use authentication, this field is not required, see [Client TOTP Signature](#client-totp-signature) | +| `timestamp` | `int` | No | `1614556800` | The timestamp of the document, if the client does not use authentication, this field is not required instead the server will use the server's timestamp. | +| `platform` | `string` | Yes | `telegram.org`, `email`, `discord.com`, etc. | The platform of the message or event, must be a supported service, see [Platform](#platform) | +| `event_type` | `string` | Yes | `incoming_message`, `peer_join`, `peer_leave`, etc. | The event type of the message or event that is represented by the document, this provides the server with context to detect anomalies in the document, see [Event Type](#event-type) | +| `channel_peer` | `string` | No | `telegram.chat:-1001301191379` | The channel peer represented as a Standard Federated Address in which the content was sent in, this could be a communication channel or chat room where one or more peers may use to broadcast messages on, see [Standard Federated Addresses](#standard-federated-addresses) | +| `resent_from_peer` | `string` | No | `telegram.user:123456789` | The resent from peer represented as a Standard Federated Address in which the content was resent/forwarded from, this could be a peer who has resent the content from another peer, see [Standard Federated Addresses](#standard-federated-addresses) | +| `from_peer` | `string` | No | `telegram.user:123456789` | The from peer represented as a Standard Federated Address in which the content was sent from, this could be a peer who has sent the content, see [Standard Federated Addresses](#standard-federated-addresses) | +| `to_peer` | `string` | No | `telegram.user:123456789` | The to peer represented as a Standard Federated Address in which the content was sent to, this could be the intended recipient of the content, see [Standard Federated Addresses](#standard-federated-addresses) | +| `proxy_peer` | `string` | No | `telegram.user:123456789` | The proxy peer represented as a Standard Federated Address in which the content was sent through a proxy, if identified as a proxy that could be mean `from_peer` is not the original sender of the content but instead the proxy peer is, see [Standard Federated Addresses](#standard-federated-addresses) | +| `peers` | [`Peer[]`](#querydocumentpeer) | No | N/A | The peer definitions of the document, see [Peer](#querydocumentpeer) | +| `content` | [`Content`](#querydocumentcontent) | No | N/A | The content of the document if applicable to the event type, see [Content](#querydocumentcontent) | +| `attachments` | [`Attachment[]`](#querydocumentattachment) | No | N/A | Optional attachments if the content contains any, see [Attachment](#querydocumentattachment) | +| `reports` | [`Report[]`](#querydocumentreport) | No | N/A | Optional reports if the content contains any, see [Report](#querydocumentreport) | + + > **Note**: The `timestamp` field is not required if the client uses authentication, instead the server will use the server's timestamp to verify the document's timestamp, if the client does not use authentication, the `timestamp` field is required to verify the document's timestamp. + + > **Note**: `peers` uses the Standard Federated Address as the key + + > **Note**: `attachments` uses the File Name as the key + +### QueryDocument.Peer Object + +TODO: Write this section + + +### QueryDocument.Content Object + +TODO: Write this section + + +### QueryDocument.Attachment Object + +TODO: Write this section + + +### QueryDocument.Report Object + +TODO: Write this section + + +# Platforms + +TODO: Write this section + +## Telegram + +TODO: Write this section \ No newline at end of file diff --git a/assets/federated_email_address.png b/assets/federated_email_address.png new file mode 100644 index 0000000000000000000000000000000000000000..75be0e71246da3656c1d1c7c732c94127d7f0477 GIT binary patch literal 29550 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 RR?`F#6_gUF;?wc_{{Y|4X|VtR literal 0 HcmV?d00001 diff --git a/assets/federated_ipv4.png b/assets/federated_ipv4.png new file mode 100644 index 0000000000000000000000000000000000000000..dbe52408b2c211a6ec13eaaaa10fcef1ce5f1372 GIT binary patch literal 21380 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

)?Zu(bTSa~AS#Sr$R z@h#*&DAGNVM+^B~{%9wV$+Ar$=+?b%0%K4Wyf{cOC(DUpP}MB3i#eU3-pn3Pg+p0m8W*deMDF>p^?jG6>X}{fK^}c#E4) zqBz-m%lT-?1A0~f+9{dv_wG)&w{dRQt>(h}Q_U<|H=-Jpz=$lK7c`s~e1o(-x z$8bX#D=_}Tk!hbIS|b^zt9Rt)$%W_GK!c?%0Jotc!sTtA2gz`3V`4)**@D`n**SoM|RsYa{ zaKY6#YBmi8EqaU2ml1Y`3fd6#K4Z2^U$My4l=doSD__A?Ruohv_Tho3L;iMMIf^GZ zeeCA$sGqtAy#j(XH{gP`&JK1tw|n4I>fMdU~8`1laJqR%8EkoUZPa#L8JN5zi60oLyszMfy<%<^V!Xp=*EDZjDnX`=+`5 zBT5wrtA^Alpt?T_)}!o2@FUe;dQf`&Q*kgNEO5Y*5KZy55n+nzbWz-Bi3|T|D#L|x zjV=~YG1U(KV5wChQ;-wbz^~S+cZK5N936}w$H%3bTa`gie9n4;W>XOx3Ms< zJy%$DyvqpRT}h~`&f}t|l=DOCwzPJQVsta)kn

wzN3;j?WXyX|3HYEyMSp_2EKT zJF`*pv`~pu+hwa6lD?(^+vEu`&-P%DX=1<$&Mvou?a0Lzb&4~n4*sJD() z5YEp*muW~^QREFz2k)uUCGrzM$7z2=$gIDg>3oXh6Ypxj&gh`Q^!o?28hy?Tg;*BE zKR55pced#I41R1TuKT`P}DMQzI(tv(GaCCBGJHT!(dB zqHaRHiPu`CzlX0uiR*gZYxLLbUcjde^WV0EE^gQ8N)G)t{>HyKr##{4C1dawkCXgL zs#8Y*Zk`A%QueODpxjQEyV0XYVDt4u)FCpufRJsbCja*^r7dkB6~f8JmrHEKk5awpbk0HK=0B3H}lG4pYpW0we%>v9Lo5!{X(BBl z)Us`~Q>mVcUR25ICL9~zu2F=mb`F;nq}j%G0rSc>+`9Nuq4AOHPTYjv*O+_< zki7iK+6Er5n#jzpAGCndR-Dx#qPLM#TAS6KA!4m(P!dNJAterm)pWOM3r*}>-8}kU zh(SMYZfeU*ClEebLGojXbA9%+&BO^peL9-4@<}c4t94hgy2&B899z%$Dr+NweD({+=d)iQ9*Vx7!g!B~lUb9mRqe@Iz1C_6-GR5Lb+TqT$*yxl zwk&x%6&uB`Q`Y3egUHRaMy7}?bx2X4BO=M=*S)%gB-pqhLW@E{aY-%3+KH~4+Dq<5 z$C#*OrpkiVcUL;c$Gl^I&>LF@Tacug=Yq}#Busch?OEh;Ad0nJ1=HDj^$l{XJiHd>?fu%sFX4;R%K?B_s>kJZ%AAvQxIiqZSE}Tkd2?|{6K7zc z?0dV*;<9}R8O1O9`Cf|XK3o==y+{$bR_j6en}z?=$yt9z)wW-pl7>N%k_M4dT0*)T zK_sMx8mXZ~8irOtx&{S@4r!2se>rYhCM{wf1N4 zeK&d8ok)w@g{a1U_=h&u04i>}6QQbrYy?W*!WlCwXgp<0mBwpm{mJ^o5AB9D^>pMb zgBDQ_ZUH)J3iUb_u)w>wXL4KhXkU%1?JS?lgV`PG%A7<78rc|q+O;4~ z=vF-3HRwhEA6OLc3arEM-4nh9q#tN80N3x{kL+v;Ka!{Dm+9e;kGmH;qVi<-73OE2 znFFC7U4VodW5xbQgcSdd$RoT+?3zT8?)xyhUDb>jf|xl^ItW*ALq72{tk|D77o!wO zYz@dkL{|gU+cbhPg$ylpdXGC39{)E&*dB)w5qGaXh2wKS%sw?a5!ph1Gkg2U1!};6D^tn9DA==M)i)%YuVLq89?LYH?A5Rhk z=>wUTx9&H@U%srftF&lkTA{6VSwNc`8m6(r?&xhqGqe+vlWp9Z%ytLffsLGrBW}-j zX4kd9iE!W0-(D?<(oj>2j!#UWJ3D1U2zFE`@XCvebyJ|wqI(xpNPSz{3)A%qlR8I| z&s`m`f|bwJHhmT{7`wn01X>fyRQ-EC9Gsq>9!E+}9VupddP#;00@y|V6BaAguTs9p z?^?2(nywb|^Pm32m>ROZzW4D*t9|jmE_P-Q+OiMmMZw0L>38%FK9=CQS_d+)x4kTb zzzWgHVyi!{-|1%JxfuHN7x)upAvP}ld>4VL@xeMg!{}Z+jJ@`qk&#$sS(z4{PWa^H z!d8)!TZl2Flm?0a<# z>Z_^}@Q)13u3TAJS?!CZ_ts0eXqezNs+f1Qv^<0irf}P30}hFc{{~8{zJDgA`%oH$ zX(VfCXsiX52Aw&0`}#J$2?)56>o84fg!uG4xCX%PGOQ8pm_0gbVe-6Ceqh_e?5sDr z%wb=5cTA|PUeNOIxdKh7-63(a!(`#rApxgZ?gyoBa@nj8gZmsH5XfX`C{FBXmTW_C zpJ0qrDJ4^p$Hs_s3nn4kb1BF>jYX6?hd?NGnz}45E?$!}hzl?uTlPu#9_Lf$-)D7o zMvz>{>E73=hvwzk5Z*X1wFO>%d+8+F!K*GVAAGU3a&X@ScRKm5QBV^Q5J26*TQ-Nf zOOq1U-A6Wz(%)sn4@Za?o+~OUGHPgQR>xBsd~RuxGQTWcr+6G9x2hzktzAwFTauEJ ziu(;xayQh#vGJVqob#KxbKa8DG)9T&C-Ix!kXw=d}ViM z{7R-}ze41go$=c0MTUSA9a4?qlCFLpA zV*TLoF!`5D^(p@|2J(#C|g- zX#CJDh|)ZaGKF=Vur6(urAv#cg+-EYS>JfVoGSuS5y68 zK7v(e9Iq*Y6CBLe3jH21!c}C`>)oz$-i3Xv(4(LBGI;*fUA@Qv*Sme8`-i9*o3ilI zJ5&DgWEzZK+9v!N4s9KJ zsIhpvSYqWU!sXWqaM4`68)LpXrdD2P9bM)ELum0k5JEBlTNk1HFAR`u7Pq2uo*XQ;@2z2<<7Q6+PvTP+YEi}Ildzc0Y4d2n*i zpsjqpKZqD!d?0G{=eIf(iTGu7+H90}orz}n<5cpbzj%;6GKdtL$FZJk3UDn~vn0EY zV0Qlufy3u5wYsMTp(pckvAGdgEKI=joxXCILDI{Z^*x!bRZvZ`cbfnpwrg&spIPvR zlQ-2b7$vkRLe0YZ!Y^IgH4iH~VuwBFTNhvv(jisM*T2yMn8}(7datFVZ0GFH4+wIy zfq&kezn&|)V1iLz*L`2?-1MLHA~%d(9%cBV##vq$RtRJkkrCJ^`7^xUf5KzFmZSa5 zUI+5Xc|Yqr&UUzHnlq zr{=*WEZFw~(Uk(|0oEMbzgDlP31fN6mOW*ohbETH(k)%4hY50PQI@jSg5MpbTEIfo z9D^#`ro^LEX1<27WrdaP&NcPIwi@?-jNXFwjI8~oZyV`D*??6oq7`C_!{qH2-V1qG z=U>R5v{81&7RJ|bY5K&*BZ#KEw;1Vzf{s8A*J=Ni_ z8yacZxS$5OX!4XK+-2fbW$+@(szXM&Lq+|qL^BjJR^z@B6zUI8HObMHHgAV2`_bEW zB$3G!_IkGZ@jhgMV4+$7hrzSE;G65n=8kJj?nq?l|eH_FPo-IO9U8 z^K1?v4!7{n^NcPIfzuOZ(}Bu(hwbyDjfFx;u*%2$8>t9W;CX{^(Mp5Lv)~^mEjQ#Q zv=MM-iT#w7UyXN6ulgwmo<$GIV`<0rDy2t713%fyUixWuMTzwb*`(TSNpgHDeV)tF zvh;@>SsETR*615rr`iE#)gBgKlaSu4$a-`5rcJ-9&kc(zV08l-vPA-Ak`l;YXlg3d z88`7|ZSCcKnO4fm3V5O#s;_=`Cpsb?7+ zNEUk%XcdVSkxHSB5t7f?`RH^+l+uy(?H7n-!J6p(w#!_&aP`LfoxNw+_=DHv{F2Fq z0@7kq9yQtNO*MQD9}5FBx&l$8-{=o9e9d+aDdb`U01}tJ`N2hR z+i}&Uu=*%iN#ViF!;*+{^>I0`>I-ryyr51;TD8UU)?3ca3WzV`_hJULi0#hPz5jUXcTi+Mj~=Vx0QRbd(UIO)db2&>Nz9F6>d zl&No-V(%5dxxez7@Y8NfmDu;BMh16ja3?(=F@v`=pRk5YcB`FIt+AQ$iCS3Ea}W|G zk{RzI2SG%siYUDYzTw!2=R%^IG?KmUi?XgqB@ENqA2*X` zu8b=ux|=%tlY7g?#4&fAy?3fm5%t?H1k9QQMsQYo3%_yUan@1a-3ctjEv;~i=#C7h zz#+7kvFze~?#?8^Kms2nd043Yq*?X;=(-hy$s%eZ4#u}YVEF3JSqEMipoW5?$ zO-CAg z5jjt{108ukSS$Y%5h>WT&qQ#lmYvI!p_<*_K*IT)kBS{1-Wwaet6km}%k`WyFnYdZ z0a{P2HogkoLRTW>>b8j=>33)}&E8?-PIvb+dnemUat?Tr-iO00VRa=|9@~L=kFhgA zKd}!~Cy;Nll4R|iaL;^+;?%|D2G5G!-o5wcf`mKy0GdEoPriR@qZ2OkHj{LomJ-TK zK(Ac2#*L2?<$3VDOu2S#S)&xq`vbwF8#&gN0lh=GqU&-@h$jHAF6-JIqyZZ(Tcq z3$JYC)mkJm?we{-Xm7xQ*y@}vJj1no-drb6Aiw1@({EgS^JA!uRUhb}VhJgl+b(p8 zXHO0K{AMJU=itBxRMk6OR_(FD*sxO`n3Z|2wdc4jUzAWp+{%f^$w%rrb+QJ05mlMA ztRW}&cZ*P6hudoP@^w^rmPWdj$iQ*#o!<7-NygE7XVDz%h7={7AKV*z^%Wk?S)Mgt zB}Xm;l8a_dUM+tbYo0+kKu1 zBFLm+USuC$ElN>EFAZ2-^AfE!`7tky*f3C8PX6xyUHL^K#IhPISvBeO`v*Rhgy-XR ze!Pr0Y;P+jFMx`FXVLO=m3Ye>Qpsg;|JHkMR;EPVua6!YNIb9{EmLE*?J@H69oXYf z^+Z*~)Hw`WIXO|O=+faJQ-W#X=bWfjxOz5fyGFwujf=4s?uoaq7V@GyFQjJ(X%iuG; zyM`L$yH4-?d20M!RI8Btn!6BRWY0ua@?`|1?)!kbVY?QnVunHhoBd&J6zF%XcNrFl z^=|k<$c;xHh0&)IlNYyY&(pAqC2zccAh2~K9HmUXfE2)V)t5_IH?UUVZwm>QIdWYc zzZS=8DjvsdmNTqBa%Yr|M0iiAaCRGJ&%#z=v5>$QZvBbHW$w*PMhc5e3@eM_?fZyg zFh>k4M(EfZ|HUj;M;x{HAVS@_xPC8Z$xgtq#b(1#L!^_%*C`$yTTuuesy8(}8eEW7|H)4Oh7Zn|mU7h`-t1Gjz z&N&q!FDnKMg$V@!001i?F02Ru00jGM>q3A5006FJpg95n=tD^e3n;q-Ugy~&Q4`W!Uv|21t;%UR(mYkFoTbhJf<1dkRfM~(x;by#i7J?RdgI%(s%ziRXCvcg&He=^2foQk` z-LMvl!;0V^=s+$VT>Vt9aZ3^l!=axB2Lca(1%M0$fPM=gp6urYZ-I|*b0N?7q%NAh ze|#yQ;3=`a#BpJ_9Swg2NXxrWsSPQYs-Bvzfl=C9sZM0)n=~?0SLq=o7D8xCD=50T zzo=^=j}n(y0Hq>a6`-i(FP^I~bPxuD03;xxQ3!ar4?w`z?^puKVcEr?)_ubP5f8-g zW48@^<3@i7OjGf2EmjG_=f}kk&VWKck5Px8S;$H&XeDT-{n16TWCxgW8Q(o;ZXaY% z03TW?m5ZsnMJD^+Ko>xm0xw+cH}{}tWsMyStS5)8CeV|&wYIK;gb!Z^0c5?k6Yyum z1j=td6yk8FXJCk^cC*|K@zeTT0or>=x1!ezN(#oi6%CMQb@oj7p)R{AheQ;>jRw}K zh-EU3XGS#!0|VnUB0xZ!UqsK@n4M_D(G3NOaoKV4n|_A|yteX6#M^zs8JsZ9Zb;#2 z|1r)REzp=5wf8h~DDwK)QjK_^iPo!Ci(p$%9CvRT3a_|$#GU9d%{7<^^eo7#b4B3 z6Fj3wLOj=^WbiL9TMXc}YUvoYw8b3sShLV0Vm*02JY0lyAQtqnCkS|Gn$&Woz48@W ztU?0&TjQAzB~UYB`0(%$-Z_AfE&>3TC;JE>>ZUAy7xe+mcY*Gt**gFrfGG~<8B@pp z02k_I#->25sdPJm4k>55UNP`MhZYbZ>RW#ul<%F@XUmENKuxp5E`u z0o#89Z^Zx{Oe&goiXKn|Wl{ND7mUU6W|N&dcRj`~?9&fMj8%+mt67;LhMw|xm|VT6 zH;@L;sAmoQkcpIB&ahQ0+IHKzRizgcZ@r67*-HjJ{5|(AXuPxhN|2p%slj@_uI4_EgZ;e!;37r+YZI=z=k9O1a1pB@f#fPA$`Arm)8JN&-5!*ALmQ06z zUX;~pEcg@-a4?0spo5#QCa@&7vm?uxW=^hsj+>bBp209v2M_3fQ&vjrEa|o-F=a0v zwj6KYAP&Rw1@J8+{kHv3A7X}Pt*U*chO`TLN&Q%ght0r#QbzS)cX}_px5^b?rQ3y& zWK(JV6?JSG%|O)W-Dy3yg1j$OaBO=LPR|}=R|wx@1_YGZW4DGAXL?F6gf!EnR=Sq~ z@lwXpt!2n#ba(*fht^?f^pQu%NmPJMfFa}IEuM@83ix011+>S+noG%vEb3+wX0?6f zU= zwQLI)Fw{r@UeZQ%Bt5l`*rF|^5=qymD9==Rkqs*)Ki`k3mFUu#`Ft5@IK(dzbNtS? z`tqfho~303X&ifor#q-NKD~I-tYg9`RGPK01=h=rUO`+yY47b#CGGB88lFxZTNsMW zM>NnXscrbT2f66xokPzHLIBykg+TDq2DljeB;}pgddksOBxW$wP?W5raqPD`r2AA} zM;sU)?#E$Oc>-xXgV0tIrJ}8;`@ig{@gdpU zpPUcf0H%xaiB(eOc7yfQ6IYuwo#>>;Cr(qcdXNwGc}o0-?hqrN|3ssDc{T@)PgVC9 zJc2)3-(g9#0~Z|$wr_>WhOgL&y=xNHfB%S(;Z0q*fLJq>27tMXpMHORpe^3N5>KH9tf=+tYO7Z%iBr_P%Yq*Zv>BV6=XoLab5OhcjB+{EtySGyDL`6CsX#Q6jRDZmpKM<7Z?k4=$C=FWGZ}s#{*&lM~m6U zuE8D1f*I*nf$oAz3lY^B@C%i}&Y|nWVFJq@(MOrPV~&U#k!~6w%gG}a6=&y~0Y6BD zm2wQCgK4=Sz!|k&X350afiDmO&j)O!G4yfztdQRK(NM`HkDAm~Ru1WRjG$YVJw2kS zmCmx?I+N&@*1w8zGApy$hfIsAw`?M8q8tjGyBDo)C7uA#YxswRo{t4^Z=&l5u$scq z*D!#k0qtT{`nyTHqbdmjJ(>8>z2e@%0?mX3NBLH727$wi!r<|K8YneY)YPQJCnZ%` zLfJWaXnNinaP?A-oXlirvbvlvKFEvp2z+M73JUZp#>dB(eWQFlP@OBkwB5lL*a9Z} zaQ!iF5eO$R#0HzdAJu_qV9nV_CZ!;it<|3+nURi%2zHwsrimSam%fiBafZKy$pt zl2%}n8E^@d3K?eoyMeY@+t8du>eDk^|F_WbCXxXI#W3txQm zL0dPN%8FL@s866J-muro~4nz`fIs z!Le<-js_T05d_agK0Z$+wz#gh|4AL&=x?wMMY5ApV1HFf|DTEUl=UQQ?yqw1G-I8& zIdpPMSG`q~%~;~#XhsFN`#@M^!?0}eINaR!d|E%Ld$$t|Yp}-NONgq=4U0&3)&mPw zUA2twlBC_7-Gm+TcIc%L6{y=k@Q4>7?EF!66di;JXmeWGAeJLyo7R>NEJ{$F9AO<( zOPira(e}Jb`3yz&{@f@DNBS=Y^)9gQ&eoBPNLrkyWaR(vm(rGotDVf7wZOrN4%^J; zug0kBrr@3h6kEk|uxf&tn4~+h@60lTo#NcO<^%RIgDBE>H;}#knE^Ix3xzXsojHo7 zKQ+Ca@7d%dLgl9zEA2JI*jn-F43+8RA&Hm?S3j*PV@%#FKZqaLi z3!?wd%?!fmwKJ|){w5?k-zj%VB3H2m6T$%@omIde<%%Vq^OCTsR77 zgD7&wqpoV$jW*LAO}WmehheTUr_5*Edme*#i*jM`ae*BeC8xlSC@z!c-m8wB@No*H zKhk9(6K!TSbH&$vt-tvi>y~jLf3Waolp7n>d#@m95ZO!Gng02*Lg;pcq|vQ<$IF@H zu+!yucM->|z>Fun%^L2ZzaI^o7Uv}!#qlIRq47&@`~s#F{3 zQd{%(C`|$mE-}d9NbW@r&S3+m-t;DtCG#C_#S6&7UXU@M9HE5T=-EQ-#~>hJcpFJr07wV(r=PKt^z3YNhVVd50iySMwJ0j%pd^j zs~4HiX!;nYf6#WjUu1S*0p~|%)ZKBviD~3`%Q`r$KpwItSL3;oQDgUDO8tx=iQ_LTgIdtJOyC94|8W!pVrT86tSQhfEs9kC{8yW6C zJF6MmUq4i+bla1{3W&8J1VLXP&(sx9P~@(I5dGhlcBNrSqm~DXp-%-x_w)NB$t+D7 zVvzcts{q81BP6v-os{rEv-NneGS)*YC6FTFmnj{+`At^XaMpIgTX%4keQ<7VS5&$1DZbdfSHL_ z_wm)N_(hubmgUun1Eg>0ysKVZTq`x%-NQba z{}Ew|RvBj8?_q4>9ofddWy~Gd$|8_*scFL8dU2fyjg_)1LPd|hpm_c}-_MwujaR6Q z(c7E55l~9yIkoBQGN}%mQy&}}sxesdrvYYk-SUtnvP+Um<56jdB6ieUJ4=5Xas(acsO23Cd$lpY! zoy*-D(|cJ2D~m2`4S2bq`9M&}pF;Ing#{h>&%2hge29FDbVZK?N%_h&UQE zNZNmw-J27Pj29_TO8_{5QC~zg)=Ee7aS`MZkRF{AVEC&( z|Km1p!KGJkN!f+hF~AuOvCKRiZ-OPp@#^h;rr02HR?VGtXhqs+*DwlGZ%nqK49s7q@jsmvw86SkbwO7lw=3MFr2wC4Mn7wrzkJ9lJPAh>L@U3xjzHLwwzlFe+n{SlG(H z8z>i?xE;ERZ&hFlL8tGSSqB($;nG zrR4>ptuVn^`6vb;(dzcLA4Bkas!?rIJTZRPz#M@@>GB~sR?7cTT`+-Nj&BlP-p!}E zg>Yu-If!=2(Oo+#KiC(veZQy1<*H(q>1iFXACDC!*P&H$UBuMSQt3aHT>;FJQZ=Bl4gSsZ_2${Eq^MJXQ!Uq%DX=ua>{mPUZW)7bGM*fFTK zD)m=bU15~vPQ;*YP-F_%>6Xo^8L}nIqw{GFyO@^o5d_;w9k&X;jHtvkx$K-boVWN0 ziANL##~konVIApm>x0NktRpMm2q-b6rj|kvI>rE&$S!`>LE9!1^rYee(C3Lmq?dhX zMt=#Yr8xOWY>S`EkVAjKGACAWis-{2-djoPKTcz&68|H8fK4Bo8lJ%&xXfKY#jlLO zA4h#)x$9khpTt&-mVFXSFXZoNm7{pPg$UF|hixfN3K%5x?=|z{6=}}pNp*ntYRaS= zF^sVTKd*pxWYm%4ldGnStZtM`YtS z`q0XL3TzNZZliG1egtXuyCZ&pTnc1>0(JlZ_a*PJu!#jlfC7Gi0&5QEP{T zPkuUUxS+h|*e!C1Q?`3oQ$onwcksU}+24nW;P7rC}z1Us3ddV)Av5%&4w%ysME ze!skiafytpvNZVGnt|j%r))W2)s+qscV-4M`yVS1gXOquX4gsbO-xWBrffN`tde-W zKF}hqzoZ({YtDhR;}=#IL%Zcdxs;ddi59qQq|He0C|&q}18cB!gn#dpIU(qX#77ba zArCdGksxczp3;5+rF8Wi+VC9aO;r@6HLuE49*6uDy8wHB17Ax(Y5#}Zxe)?nOW_fm zasQEhlO6;v9<+&RfnC&?W+<`RYG}jbfhbx@46AaMQ6oc9c{^KvK3O?xhbltMHT8|% zSfU^FG09LO@r}FC%d^Sw8vm-;J`GIKiodZn$Ytu{q={QWA+x{LFY(-%eN>6k_lQU3 zhKdH<+MAKf>!ZJRalo^Ki+;rEq4&97X*c=QsnAeC43-^H>)WA;ck;DXUsikEK`zk) zM_#tSgA&4-oxAzH>U0wP!8KxWXN#Ls401k(^-k)T4*9ojMrN=#6wBA)rF<~mUCyIg z?S%3E9itLogS8usG`PpZG*fi(ZxK& zS_Dq2iLSSyvcHumM8h#}DI3Eo8v8CZGVCU8yAuwVSG5Q66^xr7ccuaRfTlc#uq;D6 z-E;;N{cOTqFL+;{m5$=&Kk8^bPX4XD(0X_$+15Nf-L#m_EM_i9VWEqVOKZh1_+xO2$J+4X*9QkPw6R-}s=cFoW!{g3+T6nK9t_?Ebqi4g~m`oQ~wfv{{7 zCsBy=ty!NB1MKx3g890?^~MmF#o1TdoOW+rF;Hs=;<-?}P4-{SxiTSUX*WQ}Xr$&x zRLzU0<)J)OSJ8z2=n4ymCR&(9owK|)dgU%ghf3ep?-nB>k1#D5P z+}uH7N`AXksVySDM6|tqut{s{@rhjt3GC4)eG>7E3@&Hn;I}Cpjv5 zwyp2?h+~qOg|HX(val_hl2WV0Lr#2#+UmFET9|`yip|t)8D@iB6p~0KFK^} zP6_Q5lcEEakVrjFd@!jir2EedPlsf2bJH|NUuQ;Ex@Oll5{_z`KOr!Ak975gF;1C9 zQ5UP!?3v0c+q5`o-@j+L55L|TELo6~Ef+bIQNYWM`MOK++%H0sHD|wXOxNSu!&P>@ zgd=AnTmNtZIKem{e%2S(q$e9X4px`y;w_5A_SPx2{_EKAgihfrCqMK!wxhGV00|kD zQ9d(xH3;oK0u?X|AeWo?K!A9S1zff>Yatua?Wfa`SCzpOxT_wxNCl+7vP_27N16MC z!+tWPa;AH6Zsrq6zSPn1X?%eY@EyICB=g1H;3qQYW)C^}nHI(L34@84oLTt_oV1j{ zR#d5aVqs|7xhsoHIc74`LoXwE+S72E_62ReH*WJ79bE-B1-u$cNW^V-&PYm5JZU!o zXIxpj2be?tbntBp$>+$i2x()fGta;zPC7wP3u#&aEX|%YKi7sw>-4`Jh^&EKQgejq zHQbUXh(8n@?W2%LeBkgMw3%CMS3BOOKx>dwcWKgFmPFs_+_n2C(G;=Gc?kAD7n1_x z*R*;49o&tY+~2|FcXc7EJZxUA^;HpwWanHOtZYZK6#x{pK3k{T7UlcyU{r5qX-NiU__gvMb&2%J&Yiupj{CmaKROl` zm6!LYi-S(*vwMg8!|_y2p<1hz8jWt>FU`@3iSvxuSlL3^MwWFe8VoMi%iGX~uI_HP zrj)6X!|;d*hBRr?t6zRAySb@}5%Yj#BKVKEtn4xyJ-v8=VmWm<0re3_SOgN8ZgX8- zxOTfEiQN@IPo?weTt4W-@iZ4Tb>LWpVBhGd+4n#QH2i+;Q#20Ov#M1N!W@Nk8a)9v zZf*{bSC7xbu?(Gp=O-nzoZKvtSR8IP?2`dKx*?ww-D*mGy@U2IsK?jOnw*^cu*c>i zeD(Cz==`3%(eCr{Z2r2I+0ekKQFe0ruh33RL?r!y^Pv%>r33&!QLod<47mVi!Z!+11wyrUEBGl$xg3YYE+eOyC;_Sz?H);EqHtRB$3N7KSTRu@T?X$*w3=anH+}NJjVNh$=9Bg?9_n@*VZ~wc^;#>fVZLsJI}ladV0~AY)yrE zEt#%@bP1-agSy53Z7~%_$T#TGupRQv)W1#F0czfZ&qFw|vvI7)MYmIH3W%0gO9sYX z^IkRV|ss&KJ*DR0h6EhuhyetkyfQttetqbQB67Hr~UYJw0sRzzNhRe`hJ6 z6^IECHXZbpp#JTtP2(My}BN}Ni;!vlr25KbWcsurnJm7$dow>r_gjF!wXKnf z%A>tU`|?B?ussq7_NwxUbR}zBLt4SZY;F>_S_vio{jOg~bA@YV75C)3WE7 z-0!3DfXy?`qYQ(JZ0(K2yN^}xPEhFMNqk5CNx5;*fj>*i31VDZbC_UdP_NkJz3cAfoe(d9a!oI)$8S7b zeL7pGWYoJK>Tn|ZixdOiLS(XIlt&4EqIs?3@%h|UGl-o_yv@OY==B~-r2Zl&lelXI8HaE+i=-q6lrrpIn2Xg_ed-E zL#FE#%cJXT55WRMKUb%BhhF9`oGnK#8H>Kwy3hz-3;i@zLf4BfA+}UkNj1Smgat?( zs<31r$k*IfCT$tVRr{Q-@Ac1c^M3P{#}b;PT)AXCE0OG@>~Ali!2mO_<)&FYItuAj#3a_VIKPo%3f6*%N z?ZEgC`h4)E-fiZ-ZoWlqAdwR}@xce%h}DOGOr-E?nEcLL^$O{4+%2+-6#0sQ5L?Uc z)HDHJg}9&zwMV0Gw?2iO`MJY=@Nysgp?@Fr8o$Ba!+w=VfCK^r0u%rVms@}?5GaBK zfdFxbKqv$oK!Er)&k6Y1Lp|dWK>l+YU_fBy$LGhFhyOJfgM#r9{##>^N9CU=S{A0w z^gLcdI^*hmHq{{C1o`FgZ}kZ5sSfXlBrgF;x_p9(N1C&ZT|j4zW)lbrQfpTWnu9Zt z1qWSCGZ7S1VSJZ#Dl^K|y1(^&2GQ zc~0ZH>!UweI-MhRfdExU^kt=s#TqMbDOjkJLQi$|xm0!-^NGfrbpBiOztEreq-s2EvrM8a3 z399b;?2|)Ky)p0R(%RtUeq^f2BbfdYTq7)3>ekPR32&5MxdCkx_0njvbQR67TVw>$ z>2BbtpgrTy%&cA=4HQYh7Z|V5Ns&mm4{&Q@KRK3(Hob+Cki^udKh?zbvC?5j>;oP# zyAVCJF?L|O?KNAaFJ8vf-SSq2wITKGv30&Vugn3TTKt{cE@_D66HCXcN;3)7<31~^ z?X#gv zCDpXRh?VDkHKm>E4lT)^qsX=-clgGRdG$MQospYczrc7;Y3+-Jl0|2k`@niBlmxthWKdSxyrRQ*f!lI=>TBqn{RmscNRa`TYIabs^vtD3 z1@b`HidD5``*eS`LNDQX&6zcN=+~GfiCtOk^J%G8&pas+;siB+B}pAsJw&IC!$qVp z&+U5hU-d+oNv{Z+0lZWz7yBS=w>J0|7Em;MS%!*q+=bfP%gKnqrxOKwS^C1|*fQ<) zgxENW2lFI7@Gfr5Gb$~BCg*57T7^5(({6xW0Nd4yE~^js@4Q$(;>ArhpP8t4su8_F zvz$^`jlKH-PA(5drhR7^W(;FQOJACFWl;IYbc=+iC3zmDtBC;L(1?5?A9|sZYgBjt zd$G=Bo4l8FU~q8h`} zGIwZvzVfg}2X5{jj26sTNgcXR?sT-aaWN^Uf(Z4DW9nAqLI(4E#i5>2Ndq}JkW-Sn zeHPZ-IZ0H*rKal6by;n@Diprj}k7@kxOD1Uu~Hw*>qy z%3>azTh|<%PEZSLCX30UvYw+=8a>A=XjO;A7z96<|8h1Q%bvRfrh?Lc$+gwRBIo-r z%kC0kP0Z{OByuh23i47NYW0^dFkR|C?eNcnFNKDOb;`iVzTHexXN_iNiNM(9C#>2iW=w-CF zDyA(O+R9F>j>1Yidc@Q@hH$~ioO z*sGNnQ`5%c_yCF#iP4xnv-^@rwxOrlW!EwR2UQZ_&Pg?JH&9N+L-f&5hvHbJ5`Mf9EB_31<8LP1_h}qfQ~os;Lu_{VMh}jBzrll%7xPH*m;N+ z#!vUuwwtvet2@PRs9!3~8g7|O4dHyzg74BM@%HCI|81M-(g}$g_2=1S%NF-ZyVYhR zyIARBzFrD)y^vD#wIYsTOM)1S7O~C<u#nV)pjakD#*U z>b?ATA>4$+))KIIy%#?dOWof16?iG_6?j7becE^}>ZtVv z&vLyAo01vO6St%?)dY?rtx)=Vr=X)-Db<3{^&%xNW^`jh24c}{fqdu7Fm4RzhpDOs zq||9v4}5+d?6ac3pe8^YxgE*In|-{sndw4}UHT!TVQ;Qh)JT;Rb~l?svg*3!926_C z{_OXH4U?4^sRw$lpECOO$z2imP$YrY+FOZk`1rLJN@ilmNwJVL37ohCgH>9Y1j2rl zgp_$)lXaC@AO1_M4q3re{{q%n-*^ zPj{-{2wu=dMdLA6c*_7rBP z$%&oNG|nf_BW*{G46h<;*(i>D;8)u&IEZ!&5bIXg4^jO~TJ6);`@(OVJ*x8cS$B zU17KpS6D;Qww1r~#X#UC3!O+hp=0dZuMnx@5lq@wRg3Blny|3hqEKh7ma!A5W|eRk zQN(v#1;c?fA7-NWHU#0d)hEyo~_Ttw@#Txrc z*oEkFXdErMpvsf9Q&c*An>Aei)w{eNcS4Ni;KI4~D0if7KYrb9@av)OI(l~qiUpi| ztfbU^I|IR4KTDrXKw)c4cfGnEjnow+6u(;gRoq#$U46|=J6P?uyv$(9**R=Mus*yY zSkyGL^uQSaliow0lZpQ7&v+5%5*|k3+Dug=$6;G@JbA9Egrw4t1n7Dwn2-w?ZX^fu z(Q`>_WLsMl;6VwO`Chrz_iY`LP0z2Kh*&`nrEv^TFu!}cibm2Q>0|11a8vAHU!BgB zhL6_W8Kxg^6Ma^%AKZI5>cy1HfsIn-2AYbcd@_{e(o!Q;RY)drcN&MIiZ@G*50YCK z>Irmd;C9k+1}n3%>mM^usYtbOi=vg!SPr*h;8j+~1lgHE{R$b7uF(ya(E0jQ)g2Go zm-`FuSZ@^_uF}t@KGP*F>i0%EWsAAcW$a`1eKTp+Df%QGT|)?z0_b#meWitZ>!FHn zbZ%R_^{NS9Dhf2)dEo)pbJ%1>Qj0=xR=eHK^2)_(iaf)?mrg$38qP1|g)CMXZYN0E zWT91=fh1$%m$w{su(QwiT)GHCME*I^;j*^UcViP(T#7Mo{ae62jRk}E9a?!mGCU1D zl6v{{YG+=wIm`HxNm#I2Em~z+t##NwKGDij7Tk11LRfjkjB(2N z4^yp-!C0Zg0RaGio@mVE)?@0s`$y6 z@l_Jp1e#()&{)04^9vIOR#Z@qdpsQDq@<+Om|r~q!i_T0($NC_Ik5Wm>WeM5H*$&a zFWLS+f$WOq%BZspRod-;-EO{zB9Llsw!1IxN?qQ~p2W1YxM26e1#D%=qVRIlnapyo zSL+W_bC+`c7H0kf9tYm5BZQ3J=ggT&sm&VKlKv2ha`z2{kGw`SXoA4N!shm4H))+B z>t~#x=b4QPE`7Y*bPftCDJ^`E$>n4fmzSqQriahb17TobXwuTs0>0eruno6UQc~7+ z5?7;n`}mv~_N&Rr(471$6cdt`miDdlU-h(MQ&V(ss1GbCF#J=sQYx35biDk-_E}t9 z?5Ixe^Lo2yZ$A2PGW)XD>2`y-x%v}x$yZIAEsGZR+Sd=T-8wNLNx?ns1cQi(xI`XF z*zNV=o1BqR0Ue_)B}D~nMz~)>kXKom`F_4s>DdaHsoH5!^Ly6g6%-ttOIS<{9*fa< ztT2?rFB_49qI@EiPVf9nI>y+r$&H@AH7W;G=;a-#KDBMukX z@qi61+ZW^c^ZgaYst@R?-xVD$LIhANkHu!IeRFi=!p|&rA-|_WPeC!R-{%hqi47OF z$*H&@I??0vfl8;@WbU^iZl_I&P0^1ybp30P^hry7GQac z;0VJme4F>J29CNC5Pm_Vudg2uax(WM1tF;evik@q8@5+#i;?id02G|?g-#S0#J^iB z^83D7yFoNjrM)xix#`o=Cm6=@cNJ}uh>|z*`~Qx zjybfJebjOC6${N=<4A+g~U-&dj6#0vWYp}Vc zzlow{05j)y8QDz<-V`T33p^)pwA3bw0o;HVi&p#$h-eGLBp5F!aDw(b3_Ne=Imd~c znBguWSGS?B&m&1TbzxGTEFJ4AQgPzu?>J*@qw6V6&h0x**xtDo%0>U-3cHx}Y*@zM zpnQ~=!DV8rGb_?kGFdvZQX0Kon_V(k-MeG~bmFuPf~8aEwA)Gk(5r*r8Uq#;6(%dp zS1ROVKrVjKj>^64-}qM^930M-RPYFaF=j^=?;QoG0v&k1D>e0~U+b5IhFD z4~%|O^|IIZHHu79!k=T+)& zoufUt{l`OCPf0kmS`?5Ct%#BhY5vnzW9wSAwzG&I{XxUOsFS*s&y!ND@^gc{Wmtzj z=AApqjArWvPY*n%+fNje)$#}4_h;1GP9l_4HbKufZZLJTBTRxO9iC3I^vnKVUu8x6 z1uUB~XY>ZqG3kJoGTI zJRn&8Gjzv;AtOGv)ApX7GIqEW`eq!&QOlJMT;@un0OlP_*sg%TC$_SdE#oA7e=FCQ z)!cZ#`NpquIpqZw$k#dBttMBq&wce0FPT1<)r1bfp4+`1qcv3V9pCe)gAgOepCF5c zAd3s>`m{_JailX2N+xppImr1i_$28^nIjn8jm%Gg)xm7J##rbgm zDlJxE!f!Nmh%7*PvB(;0JWzk0RqXg9#avrp0FA^aq0(ENro2jM({pOSrjJ2hq}GsD zVzSh7{8)S3HuKp{*!YmmHKL%8{4i2BDWQW_Zm^`D$C;#zS{8Wou){<9fTH~u8u0e? zeoW@Z5IF-r!1!a$NVYB=qCD6p%TBrM0yy;%JeD^i>l@$uizU8}e}2t;Bd9K_alPoof}z2fFOh zU-^Uhv`VrVe*SJM516_eLz{lsotomm25k#r-THzOx|l4WGJ0w1s#N!%H-^lZC>sDf z-corWCXb%}!m12-QkGx?Vt@mR!%#6w!9WpX&EGtgM_3SI`WI+~Dhm$N{o!Kp2UZiX za(*ZGdZNUVNdmL6K?A-iMt(Pie2c~2z>+ofQ%Ubs4D$N5Q>BECt9Z9zt;6_S_ zLBh$A^&yADwcmBL;wZ5Yogd}C!m4SKH$wDrIRs?Fp}J;~X_Ku;UEL5hus~+sz(gzCFw<4kJ#UU`}_sKOC4j zERE*$`qs#KZD6)IruVBqgA5b(!i8w-(VRg768Q1f9h6tP8~k6y$v0{#zz6RHo72)o zdx{r{*|w=O&habp-AqIR>_VnrQ2*KIa7bfPiy3%=0Av#M-~`Lo^NS_jQ?dAXQ5QK& z1)J#pcg3i_hjoa`s7tIBGRce@A!Ub1Sj~}4dWoD@w9j7M9aCh{jHi=pS9cY9a*a_d z&!zzC8^DL{@x!?vmX~?4_>AIq>MO4}Ub8GN+!-tmxVkFaRJ#(hiD2)w1ENM#qnmCn zizpd4<1lSBA%k?a)|*$)2lq}R8w$#UY3>IAI6f@cKTj50aGP`Vyb`2TJYEPtmuA$3 zrq`_ZV&y_wpS~_6vdTKVIA41(y7R-_)+s@MhpRL5m*!HBN)~U*;)C1G!>IyV?*Y8D zvXPQMur5~lH`650KK*|?j-Jk#`wU3MmyZrk#(mJuUDBxe;z(?1(tADEx|7NSnT410 z9%Xb*#01uByb{YjpCLFkl;^)GwUN;sQ z+q)ImU-8?~MaW=7`>h0-(dv7nd*jEG$~KN9PN4a)}%nY;F#H@!CDLHzgD%v@yrz|kZj zeNWE}G^Tc~UJw~RS2df^Wzk{3e^?FL)I1|L3Ts=6A>@fefQ4{BokM5#V(V;92QHXWX*_`}8@$sU}#Fy2wwwD!89x3zqvmj^DAoCz!!I5 zb97>hVs0DXAhSpe0RX?PPcI5!x@w;6t0lrr>`-Kq9v=|pNtR;^?LEu8*{_XD<*=_n z)X`&g>Yq-@NiLS9xYW0T)=}#T%moweV}^qM{&}R7rR{38+-Y*pxf#|6>;)xKr;XEn z0=SfC)o719g)%L9ZagTz(=FmqP5BurEM^8})-KWi{28xG2zdp-t;N=I9|~@#ttKh6 z&}+qwPtycwDu*hCmn8j?Gc5rEnqJ%2J@hiut=Im5%gOZGKisShtCC)e!CF>fJ)T|D z;fMr^qy$mmQi*qnko$2FGT6*@2}OQjUNP$_4egOpiU6rHh7EwjqR*RT3g8&{#`tWG zdeYu)VKJzX%B&@#CYJKEBr}t-Alzo~%Z#`Q%tdx9htG8H=<3MOx|smiYAa7BX>W1N}we$Shc8={zubw~H`yefoBt3_3(x@GBDH>sfRTqMwe zteiZBN|I_x4i*9>6?{*>ziEVli56{y(LC^-~-{ zyEPspxFxuTpc~xXJ-ACYI0Uy1?he5%5J+$hy0}|#5AN;`yNle-d%t_{AMjQGFf~)t zRnyfyXZktMInO*QC(eG-g!_7ami;iD!v-ymn>#-A8o=b3O$Ye=ZJh7AoX!#yekWRI zqp_2ELH3a^>xYt`mh3l^uH%!lfI-aMR=yP7dX&C0m zm^pJa_GFE8sIXSPVCr=y&gqw%HdpseGWp}dtgv-P>ZrDWIMY z2gXqoj-QyiTj$VmNZ@GMvR*8mz_talm2IwVY&zeHa8-yzXA9Hi4C#t0WSc>FwYDS4IKH>>I95wmf9bpp@4CNV}IHPw%zf~+@C z{hC38?Tpp;GcF*1%l#!)%{kTSH^1#yeu3wD0aFq#s*9f_IypzQSC_W8^4dnCvz|OhO<(!47BSQYG67kRW>0fao)SdMaAHoEN$nKd zsro^!Y=Qa^c2Q1^c;Z^U6=%+4fA(9h>^N)@+0gm=00*y}`33`gXd_N38OVQoV94E} z3t=~Zg}@bfKzXgs8rOK>yZ+MTu>^4{_(r5XtV4X3wkqW8s5`NECr?2VdzBrIJ3Hli zwLEe>h)a2xd}}$H89G>eYF*Ocysz##T_jfbRIpd&UF*=o_cn!h&4{R!jWu0zS6Gi_ zM#AG)8v~=7k7dE{whCk2o0H&gjG^nemjqjmxM7vf#CLVynE@BCNz zXH{>c`5So3x@Oc5rrv-2EH`t@f<*ml^@%7TPU?`I%g5D(8W9byCX2)%KOMkpW{$faVNMd8;o z4U+eOAcvDxDkLSjyL{}k{8X;1I-hLr@CNbEWnnr0>oq~(?ofLOSDL5sk(Jjh&>TF7 zIh!RqeHlHyjdbQ$h%74tAOaUXVPRB_)n|>(M>~Y2F#O%pAIBW${QZ+S^a%@}ZKr?J zKy3uDFTjOLb?}@o;ZWyf8-fZ*IL$g(P1JrtQaP)G-X?#aTa_J_v=9(wI>+__?)%X@ zOC=zf_^1sPD{EB}lVxl~rXWUToSxjRw1;rMm@X{&r}$uBHPlPo*yLMIqZ_dIVwc~p z3)p)KoqiE&#gc!F434;t3VdfT?s^r&VZ#xEtc|B~IWKb3z*1l2_?^Peitp!=POH7m zXJoWCm!uX?Q{1-&7p!VFQn3p6Wgku$ZyPZxJoFESvI*6+1}AVY*-soY_jiEQ0-lln z^`6C!Z*y$k|7vN|7J}^}=fFw@u!32S$-v>N8Txd)gUPRlj0Ij`tv+ zSZcElvPSx~7u5A*ja??}%dz=a&{56%^`X7<+W5A9#FhbIkMFt^TleC zhOY%U|GFWj`(9|7qp1~CN=r<{r+nOK;i$T5vzHn}YDdj&*zUa;cR5%cmn_dkrkfMr zS8c}m;*P)cS-V%IQoBdyY~r}l(&5o~q~HoGONa;`9G;?mH79)kP_VqtpeW>6e3q$V zzM>RI2wXxzBw|*Ln)X-X&5t``ej?zLOr3c@Ls@`dEj%P*TB3ME&N$REh*9{<-dGc; z7kfb46uXjj2bg`I2)V^CVGwP*?jw5Rp!BzR3yhy##GB=#=`o&FkB!p}eztyxM%MB0 z7oW*$^UZmld1||l+11M1mp433WJazY>5GX8P(NMu8I!fiWkNVeJO4kBW+&r`^qL;l zDQ!}Z2^gELIgy7=n8&L#dV6I2kiT78VTGkb{bR#pm@>*{2q%Uq1S( zKXZEtgW#>~aNBmy4XMUl=Rng@QK7kjo zz5(;lMEYdKDIxW&z2b<8yF@vSOYvbI6`|7h1>5$5bvl5y)kf7@pcH5n_bQZ9*O=b( ztR6gaakryR*RR)^c9rAc>HfzjA_z=iop2C2+u2WBOU3Y0c4q@L!q=a0tqu`T<7$!= z(?H$Y+wHgqFK6BHgYwLXY42?AFHl{jz_LvWGzo=`Dg*8Y^QPrR0vqZ3>Kek zc#r{-Cs~P(Q+Yu`?g2e8uT4KawZAvT{UmZq?k7pbg=}|mCK?R4ys#KUj1UHna^8)) zuy*ChzjT8kOLRYmh~;nf`l(n9`Y#~vZudi(e)l5H-!<(Rie^ijv|cq=6Ukxlo-3g!-%v4bjm4zZC-E`Gf zs#6(&?%&B{5npmumXJ$)l&v%2oEY}>K&Wu4FCa(!_4icu>fxf&Ftw;cqe9f&cuPBd zCS7Zveh~3G_h!YitGPprmuNmYHTyh;FTJ3APsVXBkby$sB7#VWq5tRn@EQDI&fn;z%VZqxz)&>*-I% z0xewvN$Uc%2ZyK_%1%u@x35j3DyY-KUO75WR3htK?lgs!dO0N$g+A%81$RWt5oG%j zq-2*rgH8~2bfufXZ*%O78(41(OLMbt9T_!+aw_IU&9uTzlhSi$I6IY8T8=YN?;B5+ zYL9H!GN`I3D+$!bzX{8@9yiyK1W1ncP2t#4b{^d3kW{%_-r*&WS<~h2Xz*zB@Yd;A zlro-Ni5yO3n(+>6$kyhbP9F<@W;qSvv9S#%u$!s0eyOysYglSfJtD1*nLnX1NUwp~ z?q=u^J6TA13XPt7G5D^HNF|j8XMJ__QFN3waBRH#%azm?zW6BL>L(7G@=;*m2ZP2V zW(=R1389n;bvbkqf?<2}O!feOYLmUCL&zoSO!!c?WvA8fpR&?F0hargtUJ@G4B>!F zaWXwn&1WW-l7~bGDc$P}D1PqGVIv-!z~J{e?<~&aGe3BO-@{gYgHj8zJK=0dSr5m8{Cpuj5pW6v0A3r5){Zeu$Ocx& zKetRQGwXmD70QBr8;_-WKM&K`)$ljdb$#p_hWcvHdM_V;nOp8^gj%jM19f+cm6}%7 zqs;R%sr-}11lgE>mS8I~)Rzxdazm&#RLJ?J-KXlQZZeD4Mt^`XN1d9I7}4Kv+}J&C zQoGEqocFT0<$jx@@r)1HKEdXH_wnldwA15@u0(^b`ZLgTYWS%)m!28C>~5RAS|m3< zN7ATxws*U^COp|0HfLW(Tl?MrOtM((_j+d%GAm*2GjC`dhRtPOK4-p+fg8R=V)Xti zA9bp_$D`%aeD&N#WpHq>Qxy)1_fBQMZDGgu{Iaj%&4+BV^xe)PIOE<5G#d$uB9<51on3kl3)M`>3s5AmICO5m9a0 zeiR2j-rRIinatn1c@Z+L0rroZw0g1+-UdScWnZ^JbdHoLwRLrKyu7^J&Bh#ezJNOg zKWXsdF-KyV;pe2LPD4?hdCa{xe-Wf8j7~vk8D3Gn6qN<_Yo9QI-%81@w8j zxm(k}f3HZ0p%!%AF>!J_r}U;8c$Dnj8_WJH#dNyA&;N>~prGK!_HUI5ZIsN`coy&R z&UB&j{ms$bF*X}LQjmlZ?FAYPeF|>;NXr!gxsv#Q>RLAJ@P$<62zym^U3Y4>c^uEr z1qB6-Lm-ghkAM;;3itI6KOdg&%+E?K&s8lHLhd%*|I)gcN9Wty+%M{6A2$4A5)!Cw zK7GP}OH6EuV>_9);h-)lneRQ8#cL!YDw?36sHn5`D5F9%Nq`a^6GOZ)jP(6NTUS>J zV>m<154y;0JwA$8(9IJtiNM{_(czYl002}YJ6{C;e2+wV4|lj2Z{l8UNE#RjcA9ni zPt48D)opGdJ>#L2UfWTRtM;KF@uEv)v#_x_yb5rhN0)CvMMYJ@-bDU_Y5M-|Y_$~y z$DDi%R(*KUlbW5q+&w?f$H2ogi*|%;9qsDsTKEC30i)pezStN%T?S8tfao6IAPVJh zJVS5obN+cz*a3ia{0drjb`yL;LT(}`jU@C(N_=c=eW04(KSW?KHfVC$9uO!IH5Lm`7HZm}6POGSDIZLr#trXWuS*Y|e_=$j zv$GR)#=~@a;^X;Qak^|-v85{eNiGMdw&GPMo|&`hx7Z#`_*G2_2uR3m@|a_ zJNxhU2>Ay(;xBIqMgNa?v>}G~mM-s7K&Sm*F7n|Tp9T#(;FVhkja*)i;68Pb=rOyNOjGp7uXfy#+YukPEgWAEi9)a26TwNaEY?PX~F!b*x%*u2;StZbMM0uLRGevz%%cUEJgS}?Iop()kS#4_I2z- zvHYmA!$b_K>Z}gVZUO{UC6f70Bzj;ltTCZPgc@8a!V5cc5{|1+6L|bNmWuM>lz;Ro zd0Ttc5})JY!r8)Kb2D0+*9_-H)I4NrsAG=>f>=?@+-`UAOsguZ36FKdni!A;z9(DC z((iiNK5ySX0`6{02$>i8jYm`Qx@@$$w#!a<7?<~H{k%v!qD+BHVJIwQyH2-KQx8Aa z)yfP#+~PVJIkkb$S(!euH}da1K`mvlDyCJrN7BJfFcpus{MXmVb|8`36^rGOg&_-N zQH+oJJ`~Lg-CwFVd!ejy0*4hNY6>eK>H(-kf71w=;V>^3hy-=?ZUnfruW_Bo5R$#o ze&;kp&TTB(qYQjUkVR$Tz=+JXY?xj2r}$@xXZ!l-8Y)~kCCCiShLW%8zj&Sm&NP1t z|5wJ&|83$!44lp!P!35jM%@@Pn?2+^W)YqC%b0JPG}^A(Z~E~@S~%i#k%hp_bLm>G zy6jK%>i)S{!{CSw(?t4ftd0=MSDk!XRruYVw|PM$c{BQfq3)CXYNsrS-sH%0cd4N) zabh@bzjA`r!-u5Hb ztrS~_n?}u$eJJZR46fOq%0oROouNPzlsEl!qm{{LJrG4pR)E`l*$wTGbcv7cZp|HB zG3Bb_ue6fr9cy3e_K55q(^_wF2j5*?f%!jv?RoGvqc=aVfg~*aD)0kV8H`O8^YFMcs`6$+~ z#C<)DoT#ur+-f25kFt04c3+EPAInocHyUYRvg1lb*K1ha9zPoO^eAh&+34-!;P4Dj zU#jDln4R*AC)N5B{`pC(t-87FJZKOF0nOFYPS~uKRgN3CXq}P|UKdpRZQZDM*FigY zT3BD3&2Y2HDUeH4_^I*bV%w=8F2!_ZO5v!To9T9{7JpTNQAhQ@5Wy&~S8nmk4i>7% zU5`5zx<`i_Fe3o7Uq2VCpr#0tJW+H!Nx`dN~?bTJ{{mp*%CVu>Mb0jXXXpv!iV>5yQW{VRRO_M<&55|0x$dx<5 z!Qa!jue|c&nK$@>tV6^a%WCvq$aSa!78XWAsO|-EAGA=D-$AWbOMamZd?)vrZj>o$ zU4@(QO6|SS6&ls(@^l?9Jf?315r%HE3N zY)i|Q=)^mJQiv|MkzZ1qKrV*ROI}*JYpEhw@$n zwg=}nU2_TVa{Y@N=)~{l?j(_&*ZDy6{e_D5w@{omN@N8F?Tu%O&LdpHvD$O12PwoH z>d!*t7>AGI?f%Hra@QXAxi|)@gGrk6Q|9i&->6)x@eA;Dj-8%?v8e0>IK!sbXV7{qmsQ|iYd9VK~K9$AY`=IK1fDMn)cAC0%hKP8>wtx)S%vmuoI$Q+{kSrHRCqS zAn(A3fIQ>dk2(TP{g&j|(#+ctjN$4q1k5Wc)HkOqYu&9@MQA^4^IQvwc%wb^1}Qj1 z3k67p5kX+~55iMQb^|s0i#o{=NfO+2p~giaeMOf+s^;fT9T()d#$^r>KCZC|?-cJ% zup1Rt<64Rv{pxF1XFVANLVGM>#@`aB($p3V$$iS(FL;(}0LoOJt0!E-8L`?1fr`GQ zJ@cR#Nb5G4_oh`a_Ew-PJ_l)Q_-CJ27@Ua>`b(>QVwiK%eHs`pp@<7?TzuOEpw)eg z0#9$FC>G7HcWP$_gJ2E!8*ZPOZ(HuP`Kl8<6CK%S@OOE>s@Sbl#e_^Vc-;p+f9n%9 zB)~;?3mLaYg})@wkae%gE`_ukvwF~Wm@i?a=zcm}@jUNmD*0N6+k~Wb?)GF#IJ^4n zH98=}oWJe`Uz$-<&DGTx%&_;mAD`zUE&#phN9jb8E6mllXimh!}eMIs6v{8&m|jyJmaD% z?&M+?j^YEKcrHB$Nkdp^r5t4Tz>)}vrx8seCN-K)ufzmw6mIvZV0)L`o!JGyzwi|( z*Bugt9I5i(3yYl{67L^aw)6XkzVw6mg_Zybw>8}9D1uT~3+H{N9Yyyjs|?!@?)8C> zdt>}xaQZVAjk1%}&LqCS@^#2cp~=rtL1Xfu6EFQrKp@1i{CQjwo1lcCDw+;L8)WrY z37>s8>T?fN%RYT043d>g?{#!pnT?`(^{5)XXTv80BFKW0tixgbHGN6LD8r9_dz(Fe=R;6r*md^d)m@7n-w_{gv;$I9z7MNWN zlpL1o3$Jn1CsUQn9-Ym&oqxQvWt0U)FH=P<&5E_sj5TDPzA3;z6SrKvaLLa!^%V27 z7{*#(!Tf-=i(~Wm7iB38Hu>Ubei#`JJh$H2l)R0J)+!UP-IDIO%XypvA{{+{x*o!9a6*YA!@G)sKhJw4WOCN{KDkz2XSXge>FM zYHyoNXs#dJW!r9obdqbEq~@GQM>s=#L^Mf$>FsyKPsKMq0WGKwWEusp?8XG|O<}*^ z;`s{7Is0FSkjM}ztFqj-Zk&-gHL&_dTKSg2dP@Dnqf^b+fzOw|`MT-jTm?yhN4v3` z!m+w@Ig<}&s2N@EccSNe_}1;*4w1T#toNum@~@Y5{6M5G(>KQc=~b)5ijc+kC6|X| zGm<5l)Yd~K(M58NC|T$|V2x86ag?|0BO(^!)6o)_DONK9w26|xH35x65L0wBJ8N1*EAaiIJ-cV|B-AMXF{V-RJ=Ui0RGEiG9hBo+T# zCKZ{fD5d!XDxWCN{Q`eZ=ci?dB+nhOSdTxW8pmXoavz&%(MWS+R)NUxus^ z9cta!(q~{9-`F_ary8U@c&Ui|8e00CqLp6 n1pf~k^zTUX|9fZ93vy#z%3Z;2pe_6t0SJn+YBE()=E463pe6L1 literal 0 HcmV?d00001 diff --git a/assets/recon_example.png b/assets/recon_example.png new file mode 100644 index 0000000000000000000000000000000000000000..3811d51d82ff3a8b6d97bb32d63946704c1c2138 GIT binary patch literal 45933 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

Vwmy7kM!WB$v*m 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 'FederationCLI\InteractiveMode\ClientManager::processCommand', + 'ConfigManager' => 'FederationCLI\InteractiveMode\ConfigurationManager::processCommand' + ]; + + private static $help_pointers =[ + 'ClientManager' => 'FederationCLI\InteractiveMode\ClientManager::help', + 'ConfigManager' => 'FederationCLI\InteractiveMode\ConfigurationManager::help' + ]; + + /** + * @var FederationLib|null + */ + private static $federation_lib = null; + + /** + * Main entry point for the interactive mode + * + * @param array $args + * @return void + */ + public static function main(array $args=[]): void + { + while(true) + { + print(sprintf('federation@%s:~$ ', self::$current_menu)); + $input = trim(fgets(STDIN)); + + self::processCommand($input); + } + } + + /** + * Processes a command from the user + * + * @param string $input + * @return void + */ + private static function processCommand(string $input): void + { + $parsed_input = Utilities::parseShellInput($input); + + switch(strtolower($parsed_input['command'])) + { + case 'help': + print('Available commands:' . PHP_EOL); + print(' help - displays the help menu for the current menu and global commands' . PHP_EOL); + print(' client_manager - enter client manager mode' . PHP_EOL); + print(' config_manager - enter config manager mode' . PHP_EOL); + print(' clear - clears the screen' . PHP_EOL); + print(' exit - exits the current menu, if on the main menu, exits the program' . PHP_EOL); + + if(isset(self::$help_pointers[self::$current_menu])) + { + call_user_func(self::$help_pointers[self::$current_menu]); + } + + break; + + case 'client_manager': + self::$current_menu = 'ClientManager'; + break; + + case 'config_manager': + self::$current_menu = 'ConfigManager'; + break; + + case 'clear': + print(chr(27) . "[2J" . chr(27) . "[;H"); + break; + + case 'exit': + if(self::$current_menu != 'Main') + { + self::$current_menu = 'Main'; + break; + } + + exit(0); + + default: + if(!isset(self::$menu_pointers[self::$current_menu])) + { + print(sprintf('Unknown command: %s', $parsed_input['command']) . PHP_EOL); + break; + } + + call_user_func(self::$menu_pointers[self::$current_menu], $input); + break; + } + } + + /** + * Returns the current menu + * + * @return string + */ + public static function getCurrentMenu(): string + { + return self::$current_menu; + } + + /** + * Sets the current menu to the specified value + * + * @param string $current_menu + */ + public static function setCurrentMenu(string $current_menu): void + { + self::$current_menu = $current_menu; + } + + /** + * Returns the FederationLib instance + * + * @return FederationLib + */ + public static function getFederationLib(): FederationLib + { + if(self::$federation_lib == null) + { + self::$federation_lib = new FederationLib(); + } + + return self::$federation_lib; + } + } \ No newline at end of file diff --git a/src/FederationCLI/InteractiveMode/ClientManager.php b/src/FederationCLI/InteractiveMode/ClientManager.php new file mode 100644 index 0000000..84174d6 --- /dev/null +++ b/src/FederationCLI/InteractiveMode/ClientManager.php @@ -0,0 +1,190 @@ +setName(Utilities::promptInput('Client name (default: Random): ')); + $client->setDescription(Utilities::promptInput('Client description (default: N/A): ')); + $client->setQueryPermission((int)Utilities::parseBoolean(Utilities::promptInput('Query permission (default: 1): '))); + $client->setUpdatePermission((int)Utilities::parseBoolean(Utilities::promptInput('Update permission (default: 0): '))); + + + try + { + $client_uuid = InteractiveMode::getFederationLib()->getClientManager()->registerClient($client); + } + catch(Exception $e) + { + print('Failed to register client: ' . $e->getMessage() . PHP_EOL); + Utilities::printExceptionStack($e); + return; + } + + print(sprintf('Client registered successfully, UUID: %s', $client_uuid) . PHP_EOL); + } + + /** + * @param array $args + * @return void + * @throws DatabaseException + * @throws \Doctrine\DBAL\Exception + * @throws \RedisException + */ + private static function listClients(array $args): void + { + $page = $args[0] ?? 1; + $filter = $args[1] ?? ListClientsFilter::CREATED_TIMESTAMP; + $order = $args[2] ?? FilterOrder::ASCENDING; + $max_items = $args[3] ?? 100; + + $clients = InteractiveMode::getFederationLib()->getClientManager()->listClients($page, $filter, $order, $max_items); + + if(count($clients) === 0) + { + print('No clients found' . PHP_EOL); + } + else + { + $builder = new Builder(); + foreach($clients as $client) + { + $builder->addRow($client->toArray()); + } + $builder->setTitle(sprintf('Clients (page %d, filter %s, order %s, max items %d)', $page, $filter, $order, $max_items)); + print($builder->renderTable() . PHP_EOL); + } + } + + private static function getClient(mixed $args) + { + $client_uuid = $args[0] ?? null; + + if(is_null($client_uuid)) + { + print('Client UUID required' . PHP_EOL); + return; + } + + try + { + $client = InteractiveMode::getFederationLib()->getClientManager()->getClient($client_uuid); + } + catch(Exception $e) + { + print('Failed to get client: ' . $e->getMessage() . PHP_EOL); + Utilities::printExceptionStack($e); + return; + } + + foreach($client->toArray() as $key => $value) + { + print match ($key) + { + 'id' => (sprintf(' UUID: %s', $value) . PHP_EOL), + 'enabled' => (sprintf(' Enabled: %s', (Utilities::parseBoolean($value) ? 'Yes' : 'No')) . PHP_EOL), + 'name' => (sprintf(' Name: %s', $value ?? 'N/A') . PHP_EOL), + 'description' => (sprintf(' Description: %s', $value ?? 'N/A') . PHP_EOL), + 'secret_totp' => (sprintf(' Secret TOTP: %s', $value ?? 'N/A') . PHP_EOL), + 'query_permission' => (sprintf(' Query permission Level: %s', $value) . PHP_EOL), + 'update_permission' => (sprintf(' Update permission Level: %s', $value) . PHP_EOL), + 'flags' => (sprintf(' Flags: %s', $value) . PHP_EOL), + 'created_timestamp' => (sprintf(' Created: %s', date('Y-m-d H:i:s', $value)) . PHP_EOL), + 'updated_timestamp' => (sprintf(' Updated: %s', date('Y-m-d H:i:s', $value)) . PHP_EOL), + 'seen_timestamp' => (sprintf(' Last seen: %s', date('Y-m-d H:i:s', $value)) . PHP_EOL), + default => (sprintf(' %s: %s', $key, $value) . PHP_EOL), + }; + } + + } + + private static function totalClients() + { + print(sprintf('Total clients: %d', InteractiveMode::getFederationLib()->getClientManager()->getTotalClients()) . PHP_EOL); + } + + private static function totalPages(mixed $args) + { + $max_items = $args[0] ?? 100; + + print(sprintf('Total pages: %d', InteractiveMode::getFederationLib()->getClientManager()->getTotalPages($max_items)) . PHP_EOL); + } + } \ No newline at end of file diff --git a/src/FederationCLI/InteractiveMode/ConfigurationManager.php b/src/FederationCLI/InteractiveMode/ConfigurationManager.php new file mode 100644 index 0000000..7d5c0c2 --- /dev/null +++ b/src/FederationCLI/InteractiveMode/ConfigurationManager.php @@ -0,0 +1,113 @@ + - reads the value of the specified configuration key' . PHP_EOL); + print(' write - writes the value of the specified configuration key' . PHP_EOL); + print(' save - saves the current configuration to disk' . PHP_EOL); + } + + /** + * Reads the current configuration or the value of a specific key + * + * @param string|null $key + * @return void + */ + private static function read(?string $key=null): void + { + if($key === null) + { + $value = Configuration::getConfiguration(); + } + else + { + $value = Configuration::getConfigurationObject()->get($key); + } + + if(is_null($value)) + { + print('No value found for key: ' . $key . PHP_EOL); + } + elseif(is_array($value)) + { + print(json_encode($value, JSON_PRETTY_PRINT) . PHP_EOL); + } + else + { + print($value . PHP_EOL); + } + } + + /** + * Writes the value of a specific configuration key + * + * @param string $key + * @param string $value + * @return void + */ + private static function write(string $key, string $value): void + { + Configuration::getConfigurationObject()->set($key, $value); + } + + /** + * Writes the current configuration to disk + * + * @return void + */ + private static function save(): void + { + try + { + Configuration::getConfigurationObject()->save(); + } + catch(Exception $e) + { + print('Failed to save configuration: ' . $e->getMessage() . PHP_EOL); + Utilities::printExceptionStack($e); + } + } + } \ No newline at end of file diff --git a/src/FederationCLI/Program.php b/src/FederationCLI/Program.php new file mode 100644 index 0000000..82f634c --- /dev/null +++ b/src/FederationCLI/Program.php @@ -0,0 +1,41 @@ + $command, + 'args' => $args + ]; + } + + /** + * Parses a boolean value from a string + * + * @param $input + * @return bool + */ + public static function parseBoolean($input): bool + { + if(is_null($input)) + return false; + + if(is_bool($input)) + return $input; + + if(is_numeric($input)) + return (bool) $input; + + if(is_string($input)) + $input = trim($input); + + switch(strtolower($input)) + { + case 'true': + case 'yes': + case 'y': + case '1': + return true; + + default: + case 'false': + case 'no': + case 'n': + case '0': + return false; + } + } + + /** + * Prompts the user for an input value + * + * @param string|null $prompt + * @param string|null $default_value + * @return string|null + */ + public static function promptInput(?string $prompt=null, ?string $default_value=null): ?string + { + if($prompt) + print($prompt . ' '); + + $input = trim(fgets(STDIN)); + + if(!$input && $default_value) + $input = $default_value; + + return $input; + } + + /** + * Prompts the user for a boolean value + * + * @param string|null $prompt + * @param bool $default_value + * @return bool + */ + public static function promptYesNo(?string $prompt=null, bool $default_value=false): bool + { + if($prompt) + print($prompt . ' '); + + $input = trim(fgets(STDIN)); + + if(!$input && $default_value) + $input = $default_value; + + return self::parseBoolean($input); + } + } \ No newline at end of file diff --git a/src/FederationLib/Classes/Base32.php b/src/FederationLib/Classes/Base32.php new file mode 100644 index 0000000..c4d0bb8 --- /dev/null +++ b/src/FederationLib/Classes/Base32.php @@ -0,0 +1,84 @@ +setDefault('database.driver', 'mysqli'); + self::$configuration->setDefault('database.host', 'localhost'); + self::$configuration->setDefault('database.port', 3306); + self::$configuration->setDefault('database.name', 'federation'); + self::$configuration->setDefault('database.username', 'root'); + self::$configuration->setDefault('database.password', 'root'); + self::$configuration->setDefault('database.charset', 'utf8mb4'); + self::$configuration->setDefault('database.reconnect_interval', 1800); + + /** Multi-Cache Configuration */ + self::$configuration->setDefault('cache_system.enabled', true); + // Cache System Configuration + // Client Objects + self::$configuration->setDefault('cache_system.cache.client_objects_enabled', true); + self::$configuration->setDefault('cache_system.cache.client_objects_ttl', 200); + self::$configuration->setDefault('cache_system.cache.client_objects_server_preference', 'redis_master'); + self::$configuration->setDefault('cache_system.cache.client_objects_server_fallback', 'any'); + // Peer Objects + self::$configuration->setDefault('cache_system.cache.peer_objects_enabled', true); + self::$configuration->setDefault('cache_system.cache.peer_objects_ttl', 200); + self::$configuration->setDefault('cache_system.cache.peer_objects_server_preference', 'redis_master'); + self::$configuration->setDefault('cache_system.cache.peer_objects_server_fallback', 'any'); + // Redis Configuration + self::$configuration->setDefault('cache_system.servers.redis_master.enabled', true); + self::$configuration->setDefault('cache_system.servers.redis_master.host', 'localhost'); + self::$configuration->setDefault('cache_system.servers.redis_master.port', 6379); + self::$configuration->setDefault('cache_system.servers.redis_master.driver', 'redis'); + self::$configuration->setDefault('cache_system.servers.redis_master.priority', 1); + self::$configuration->setDefault('cache_system.servers.redis_master.username', null); + self::$configuration->setDefault('cache_system.servers.redis_master.password', null); + self::$configuration->setDefault('cache_system.servers.redis_master.database', null); + self::$configuration->setDefault('cache_system.servers.redis_master.reconnect_interval', 1800); + // Memcached Configuration + self::$configuration->setDefault('cache_system.servers.memcached_master.enabled', false); + self::$configuration->setDefault('cache_system.servers.memcached_master.host', 'localhost'); + self::$configuration->setDefault('cache_system.servers.memcached_master.port', 11211); + self::$configuration->setDefault('cache_system.servers.memcached_master.driver', 'memcached'); + self::$configuration->setDefault('cache_system.servers.memcached_master.priority', 1); + self::$configuration->setDefault('cache_system.servers.memcached_master.username', null); + self::$configuration->setDefault('cache_system.servers.memcached_master.password', null); + self::$configuration->setDefault('cache_system.servers.memcached_master.database', null); + self::$configuration->setDefault('cache_system.servers.memcached_master.reconnect_interval', 1800); + + + + /** Federation Configuration */ + self::$configuration->setDefault('federation.events_retention', 1209600); // Two Weeks + self::$configuration->setDefault('federation.events_enabled.generic', true); + self::$configuration->setDefault('federation.events_enabled.client_Created', true); + + + /** Save the configuration's default values if they don't exist */ + try + { + self::$configuration->save(); + } + catch(Exception $e) + { + throw new RuntimeException('Failed to save configuration: ' . $e->getMessage(), $e); + } + } + + return self::$configuration->getConfiguration(); + } + + /** + * Returns the configuration object. + * + * @return \ConfigLib\Configuration + */ + public static function getConfigurationObject(): \ConfigLib\Configuration + { + if(self::$configuration === null) + { + self::getConfiguration(); + } + + return self::$configuration; + } + + /** + * Returns driver of the database. + * + * @return string + */ + public static function getDatabaseDriver(): string + { + return self::getConfiguration()['database']['driver']; + } + + /** + * Returns the host of the database. + * + * @return string + */ + public static function getDatabaseHost(): string + { + return self::getConfiguration()['database']['host']; + } + + /** + * Returns the port of the database. + * + * @return int + */ + public static function getDatabasePort(): int + { + return (int)self::getConfiguration()['database']['port']; + } + + /** + * Returns the name of the database. + * + * @return string + */ + public static function getDatabaseName(): string + { + return self::getConfiguration()['database']['name']; + } + + /** + * Returns the username of the database. + * + * @return string + */ + public static function getDatabaseUsername(): string + { + return self::getConfiguration()['database']['username']; + } + + /** + * Returns the password of the database. + * + * @return string|null + */ + public static function getDatabasePassword(): ?string + { + $password = self::getConfiguration()['database']['password']; + + if($password === '') + { + return null; + } + + return $password; + } + + /** + * Returns the charset of the database. + * + * @return string + */ + public static function getDatabaseCharset(): string + { + return self::getConfiguration()['database']['charset']; + } + + /** + * Returns the interval in seconds to reconnect to the database. + * + * @return int + */ + public static function getDatabaseReconnectInterval(): int + { + return (int)self::getConfiguration()['database']['reconnect_interval']; + } + + /** + * Returns True if the cache system is enabled for FederationLib + * and False if not, based on the configuration. + * + * @return bool + */ + public static function isCacheSystemEnabled(): bool + { + return (bool)self::getConfiguration()['cache_system']['enabled']; + } + + /** + * Returns the configuration for all the cache servers + * + * @return CacheServerConfiguration[] + */ + public static function getCacheServers(): array + { + $results = []; + foreach(self::getConfiguration()['cache_system']['servers'] as $server_name => $server) + { + if($server['enabled'] === true) + { + $results[] = new CacheServerConfiguration($server_name, $server); + } + } + + return $results; + } + } \ No newline at end of file diff --git a/src/FederationLib/Classes/Configuration/CacheServerConfiguration.php b/src/FederationLib/Classes/Configuration/CacheServerConfiguration.php new file mode 100644 index 0000000..3c880cd --- /dev/null +++ b/src/FederationLib/Classes/Configuration/CacheServerConfiguration.php @@ -0,0 +1,162 @@ +name = $configuration['name'] ?? $name; + $this->enabled = $configuration['enabled'] ?? false; + $this->host = $configuration['host'] ?? null; + $this->port = $configuration['port'] ?? null; + $this->driver = $configuration['driver'] ?? null; + $this->priority = $configuration['priority'] ?? null; + $this->username = $configuration['username'] ?? null; + $this->password = $configuration['password'] ?? null; + $this->database = $configuration['database'] ?? null; + $this->reconnect_interval = $configuration['reconnect_interval'] ?? null; + } + + /** + * Returns the name of the cache server + * + * @return string + */ + public function getName(): ?string + { + return $this->name; + } + + /** + * Returns whether the cache server is enabled + * + * @return bool + */ + public function getEnabled(): bool + { + return $this->enabled ?? false; + } + + /** + * Returns the host of the cache server + * + * @return string|null + */ + public function getHost(): ?string + { + return $this->host; + } + + /** + * @return int|null + */ + public function getPort(): ?int + { + return $this->port; + } + + /** + * @return string|null + */ + public function getDriver(): ?string + { + return $this->driver; + } + + /** + * @return int|null + */ + public function getPriority(): ?int + { + return $this->priority; + } + + /** + * @return string|null + */ + public function getUsername(): ?string + { + return $this->username; + } + + /** + * @return string|null + */ + public function getPassword(): ?string + { + return $this->password; + } + + /** + * @return string|null + */ + public function getDatabase(): ?string + { + return $this->database; + } + + /** + * @return int|null + */ + public function getReconnectInterval(): ?int + { + return $this->reconnect_interval; + } + + } \ No newline at end of file diff --git a/src/FederationLib/Classes/Configuration/CacheSystemConfiguration.php b/src/FederationLib/Classes/Configuration/CacheSystemConfiguration.php new file mode 100644 index 0000000..b4f5143 --- /dev/null +++ b/src/FederationLib/Classes/Configuration/CacheSystemConfiguration.php @@ -0,0 +1,144 @@ +client_objects_enabled = $configuration['cache_system.cache.client_objects_enabled'] ?? false; + $this->client_objects_ttl = $configuration['cache_system.cache.client_objects_ttl'] ?? 200; + $this->client_objects_server_preference = $configuration['cache_system.cache.client_objects_server_preference'] ?? 'any'; + $this->client_objects_server_fallback = $configuration['cache_system.cache.client_objects_server_fallback'] ?? 'any'; + + $this->peer_objects_enabled = $configuration['cache_system.cache.peer_objects_enabled'] ?? false; + $this->peer_objects_ttl = $configuration['cache_system.cache.peer_objects_ttl'] ?? 200; + $this->peer_objects_server_preference = $configuration['cache_system.cache.peer_objects_server_preference'] ?? 'any'; + $this->peer_objects_server_fallback = $configuration['cache_system.cache.peer_objects_server_fallback'] ?? 'any'; + } + + /** + * Returns True if client cache objects are enabled, false otherwise + * + * @return bool + */ + public function getClientObjectsEnabled(): bool + { + return $this->client_objects_enabled; + } + + /** + * Returns the TTL for client cache objects + * + * @return int + */ + public function getClientObjectsTtl(): int + { + return $this->client_objects_ttl; + } + + /** + * Returns the server preference to where client cache objects should be stored + * + * @return string + */ + public function getClientObjectsServerPreference(): string + { + return $this->client_objects_server_preference; + } + + /** + * Returns the server fallback to where client cache objects should be stored + * + * @return string + */ + public function getClientObjectsServerFallback(): string + { + return $this->client_objects_server_fallback; + } + + /** + * Returns True if peer cache objects are enabled, false otherwise + * + * @return bool + */ + public function getPeerObjectsEnabled(): bool + { + return $this->peer_objects_enabled; + } + + /** + * Returns the TTL for peer cache objects + * + * @return int + */ + public function getPeerObjectsTtl(): int + { + return $this->peer_objects_ttl; + } + + /** + * Returns the server preference to where peer cache objects should be stored + * + * @return string + */ + public function getPeerObjectsServerPreference(): string + { + return $this->peer_objects_server_preference; + } + + /** + * Returns the server fallback to where peer cache objects should be stored + * + * @return string + */ + public function getPeerObjectsServerFallback(): string + { + return $this->peer_objects_server_fallback; + } + } \ No newline at end of file diff --git a/src/FederationLib/Classes/Data/wordlist.txt b/src/FederationLib/Classes/Data/wordlist.txt new file mode 100644 index 0000000..f2847c7 --- /dev/null +++ b/src/FederationLib/Classes/Data/wordlist.txt @@ -0,0 +1,6082 @@ +aasvogels +ababdeh +abaca +abacate +abacination +aback +abactinally +abacus +abadia +abhenries +abietate +abiotrophy +abjudicator +ablepharon +abnormalness +abomasal +abounds +abricot +absconds +absinthes +absorbefacient +abstentious +absurds +abusee +acajou +acanthodian +acataleptic +accendibility +accessorily +accidentarily +acclimatised +accordionist +accountably +accretionary +aceanthrene +acemila +acerola +acetylrosaniline +acetoarsenite +acetotoluid +acettoluide +achroacyte +achromatized +acid +acidheads +aclinic +acocanthera +acopon +acotyledon +acrimoniousness +acrindoline +acrobystitis +acromiodeltoid +acron +actinomere +actinomeric +actinopterygian +actu +actualness +acuminulate +addax +added +additionally +adelea +adelphi +adenohypophyseal +adiaphorism +adiaphoristic +adipocellulose +adjudge +adjudicatory +adjurors +admerveylle +adopting +ador +adpao +adsorbent +adularescent +adulterant +adv +advecting +aegagri +aegagropila +aeolididae +aerobically +aerocamera +aeroducts +aeromechanics +aeronomy +aestheticize +affectively +affectlessness +affirmable +affronted +afghan +aflower +afshah +aftercome +afterthought +agamically +agaricales +agarics +agelessly +aggravative +aggress +aghas +agitpropist +aglint +agpaite +agrarianism +agriotypidae +agrostis +agrostographer +ahluwalia +ailsyte +aimless +airbills +airiferous +airmark +akala +akan +akela +alabastoi +alamo +alamodes +alaunt +albuminimeter +albuminuric +alcantarines +alcedines +alcoholimeter +alcovinometer +alertly +alestake +aleurobius +algarad +algieba +alginate +alible +alimentiveness +alisphenoidal +alkahest +alkalisation +alkaloids +allayer +allegation +allmouths +allocable +allochthonous +allogeneous +allophone +allotrophic +allotropies +almah +alodial +alorcinic +alphabetically +alphosis +alpiste +alterants +alternamente +although +amafingo +amalgam +amandin +amargosa +amazon +ambidextrousness +ambiences +amblyomma +ambrettolide +amellus +americanizes +ametrometer +amidase +amidoaldehyde +amygdalitis +amylenes +amine +ammines +ammunition +amniomancy +amoebalike +amores +ampelography +amphicarpium +amphichrom +amphion +amphistylic +amplexicauline +ampulliform +amzel +anacamptic +anacoluthon +anadenia +anaglypta +anagraph +analyst +anaphoric +anaplasia +anastasian +anastomus +anathematizer +anatripsis +ancien +andesine +andesite +androgynies +anear +anepigraphic +anerly +aneurysmally +angers +angiokeratoma +angiostomy +anglepod +angrily +angularize +anhydremia +animators +animis +animoseness +anisopodal +anisotropism +anjan +annabel +annoyers +anoa +anoas +anoestrus +anomodont +anoraks +anselm +answerability +antanemic +antefixa +antenodal +anteroclusion +antescript +antherozoidal +anthocerotaceae +anthologizer +anthracotheriidae +anthurium +anti +antiagglutinating +anticalculous +anticathode +anticholinesterase +anticlassicist +anticlimactic +anticum +antienzymatic +antiepileptic +antifederal +antiglobulin +antihistamine +antimalaria +antimaterialistic +antimelancholic +antiministerialist +antinion +antiparallelogram +antipestilential +antiphons +antiprecipitin +antiproteolysis +antiquer +antiroyalism +antiseptic +antitheft +antitheologian +antithetically +antonia +antrophore +anubing +aortoptosis +aosmic +aperulosid +aphids +aphoriser +aphorismical +aphthartodocetism +apioidal +apneumatosis +apogeotropism +apoise +apologetic +apomorphine +apostatizes +apostemation +apotheosised +appassionatamente +appellability +apperceptively +appetitious +applesauce +appraisable +apprest +approach +apsides +apterygote +aptychus +aquitanian +arabicize +arachnephobia +araneose +arank +arbutes +archaicism +archaizing +archcheater +archconspirator +archearl +archgomeral +archimpressionist +archpractice +archrascal +archurger +arctation +arcticized +arenig +arenites +argental +argyrose +argufied +aryepiglottidean +arilliform +aristocratical +arles +armadillos +armillaria +armlessness +aroxyl +arrace +arrage +arrojadite +arroz +arsenization +arsphenamine +arterialized +arterin +article +artly +artwork +arverni +arzan +ascalabota +ascidioidea +ascosporous +aselgeia +asimina +asymptotical +asystolism +aslope +asonia +aspidochirota +aspirates +assassinations +assents +assertrix +assident +assmanship +assoluto +assumptive +asterolepidae +asthenic +astor +astragalocalcaneal +astrologers +astute +atabal +atacaman +atheromas +athyrosis +atka +atmospheres +atomism +atonable +atrabilious +attainer +attestive +attire +attouchement +attractants +atwin +audiometries +augment +augustinian +aurify +aurifying +auriscalpium +auscultate +austenitizing +australioid +autobiographer +autocades +autodiagnostic +autogyros +autographometer +autoimmunization +autolyzate +autophytically +autopsical +autositic +auturgy +auxiliarly +avaricious +avener +aviation +aviatoriality +avocadoes +avoids +avoirdupois +awreck +axifugal +axlesmith +azathioprine +azimuthally +azocoralline +azoeosin +azotemias +babiche +bacchant +baccivorous +bacillite +back +backfilling +backset +backsetting +backstroking +backwinding +badhan +badinage +baedekerian +bagaudae +bagmaking +bagreef +bairnliness +baysmelts +baiting +balaenopteridae +balatronic +balconies +baldly +balija +ballies +ballooners +balneotherapeutics +balter +banago +bananaquit +banditism +bandonion +banker +bankers +bans +bantus +baphia +barbarousness +barche +bareback +bargainor +barythymia +barlafumble +barling +barmaster +barodynamics +barrenly +barrico +basaltoid +baselard +bashyle +bashlyks +basiliscan +bassanite +basset +bastioned +bated +batetela +batterfang +batteried +battleground +bauble +bavian +bawdstrot +beanballs +beanfeaster +beanies +beastlike +beatster +bebat +beblot +becall +becky +bedabbles +bedeafens +bedirty +bedraggling +bedraping +beduins +bedways +beeriest +beetled +befretting +befrilled +beginger +begluc +begorah +beguiles +behests +beknights +belamour +belfry +bellatrix +belling +bellmaster +bellonian +beloved +bemurmur +benchmen +beneaped +benignancy +bennettitaceous +benumbment +benzacridine +benzoles +bequests +bequote +bergama +bergerette +beryllonate +berith +beshout +beshriek +besleeve +besnows +bespy +bespreads +besra +bethuel +betinge +betire +betwattled +beveto +bewrayed +bezantee +bezzo +biacetyl +bibliognost +bibliomanianism +bibulously +biclinia +biddably +biddies +byerlite +bifara +bigheads +bigots +bilbies +bilineate +bylined +billons +biltongue +bimaxillary +bimuscular +binocularity +biogases +biologism +biometer +biopoesis +biotite +bipinnate +bipyramidal +byplays +birdeen +birthnight +bisector +bishoping +bismerpund +bystreet +bitchily +bitterbur +bitterest +bivoltine +blackfoot +blackguardism +bladderet +bladderwrack +blamefully +blamefulness +blanching +blazoner +bleachability +bleareyed +blenders +blepharydatis +blepharosynechia +blesseder +blitheful +blockhouses +blocs +bloke +bloom +blotchiness +bluchers +bluebill +bluegown +bluffy +blunge +boardinghouses +boastfulness +bobbies +bod +bodywear +bodywise +bogland +bogtrotting +bokom +boldacious +boletaceae +boltcutter +bombards +bombastical +bondon +bones +bongoes +bonnetieres +boogum +bookkeepers +boomster +boothian +bootleger +borachio +boreus +boroughmongery +borrower +borrowing +bossa +botong +bottega +bouch +bougee +boulterer +bourignianist +boutel +bowdrill +bowellike +boxball +braccia +brachinus +brachioganoidei +bractlets +bradyacousia +braeman +braes +bragger +brandi +brandishers +branglement +brant +brassicaceous +bravi +braxies +brazes +breathableness +breeziness +breme +brender +brewst +bribee +bricklining +brigandage +brightened +brilliantness +briolettes +brise +broadax +broadlooms +brodyaga +brogh +bromine +bronchiocele +bronchiocrisis +bronzewing +broodiest +brownishness +brownstones +brucins +brugh +brushing +brute +brutify +buchu +buckishness +buckram +buddhistic +bufflehorn +buggers +bugleweed +bulbocavernous +bulla +bullcart +bullhide +bumbee +bumptiously +bunching +buncoed +bunkhouse +buqsha +burgensic +burglarised +burladero +burnetize +burree +busboys +bushgoat +businesswoman +buskins +busty +butyrous +butterwoman +buzziest +buzzle +cabineted +cabinetwork +cabreuva +cabrie +caca +caddises +cadginess +cadish +cadres +cadweed +cafetal +cahokia +cainism +caitiffs +calathiform +calcaneoplantar +calcioferrite +calcitration +calcomp +calenturish +calibrater +calycozoan +caller +calliope +callo +caloric +caloris +calthrops +cambibia +camelias +camisados +camise +camphanyl +camshafts +camsteary +canalizes +canceleer +cancerdrops +canephoros +canette +caning +canisters +cannular +canoodler +canopy +cantoner +cantonize +canzo +capabilities +capernoity +capitular +caponizer +cappelenite +capshore +captivate +captived +captors +caract +carbethoxy +carbonification +carbonizers +carbonless +carcharias +cardias +cardioplegia +cardiorrhexis +cardol +caretook +caribbee +carinula +carnal +carnies +carotene +carotin +carpe +carpological +carpsucker +carrots +carts +cartwheeler +casekeeper +casemated +casing +casita +caspian +cassoulet +catacrotism +catalyses +catallactic +cataloguish +catamountain +catastrophism +catechised +categorised +catepuce +cathepsin +catkins +catnips +cattleship +caucussing +caudalward +causation +caustic +cavalcade +cckw +cebids +cecal +cedarware +celebration +celeste +celiodynia +celtologist +cenanthy +cental +centauric +centraliser +centranthus +centuples +centuply +cephalodynia +cephalosporin +cepous +cerebrology +cerebropathy +ceremonious +cereuses +ceric +ceruse +cesious +cespitose +chacona +chacun +chafewax +chaffs +chairmen +chalybite +chalked +chalutzim +chamal +champerty +chandelled +chandui +chaneling +chanticleer +charabanc +characteriser +charged +chariotlike +chariotman +charquis +chasmogamic +chattily +chauceriana +chaunting +cheatableness +checked +checkering +cheecha +cheiloplasties +chelating +chemin +chemises +chemoprophylaxis +chequeen +chermes +cherublike +chetah +chiastoneurous +chiaus +chiels +chiffoniers +chileanize +chili +chiliasm +chimango +chimesmaster +chyometer +chiriguano +chirk +chiromancy +chiropodous +chitchats +chlamydes +chloranaemia +chloridated +chloroformed +chlorophyl +choeropsis +choirboy +choledocholithiasis +choleraic +cholerrhagia +chondrofibromatous +chondrostean +chopsticks +choriambus +choroidoretinitis +chorusses +chosing +chowries +chrysograph +chrysophane +christenmas +chromite +chromone +chromoptometrical +chronogram +chronol +chukar +chumawi +churly +churm +cyanic +cyanomethaemoglobin +cyans +cyathophyllum +cyclitic +cycloacetylene +cyclone +cycloolefine +cyclopean +cygninae +ciliolum +cilium +cincinnatian +cincinnus +cinclidotus +cinene +cinevariety +cynomoriaceous +cyproterone +cypselomorph +circularism +circum +circummigrate +circumstantiality +circumstantiation +circumventing +circumventor +cirurgian +cystopyelitis +cystotomies +cytisus +cytochemistry +cytocyst +cytolymph +cytotoxic +civilisational +claybrained +claying +clairschacher +clangor +clapboards +clappering +claritude +classify +claudian +claver +cleanse +clearheadedly +cleaveful +clerestories +clerkly +clew +climaciaceae +climbable +clinologic +clinometrical +cloakwise +clocher +cloison +cloisonn +clokies +clothilda +clouding +clubwoman +clutcher +coachability +coachmaster +coadjust +coadunated +coalifying +coastguardsmen +coauthors +cobang +cobbler +cocaigne +coccids +cocineras +cockatoos +cockshot +cocodette +coconnection +codify +codrus +coelastraceae +coeloblastic +coenocentrum +coeternally +coextend +coextending +coffinless +cognizableness +cognoscitive +cohenite +coimplicate +coinquinate +cojoin +colaphize +coleopter +coleseeds +collaterally +colleague +collimation +collin +collinsia +colloquiquia +colocolo +coloner +colonies +colposcope +coltishness +columbotantalate +columelliform +columniform +comblike +combustive +comfortful +comfortroot +commercer +commercialising +commercialist +commissionerships +committible +communalised +commuters +compagnies +companionably +compassivity +compeller +complementation +complimentary +compline +componency +componental +compoundedness +compression +concameration +concealedly +conceptus +concertiser +concertizes +conchies +conclave +concretized +concubinage +condite +conditionalities +condominium +confact +conferencing +confervaceae +confining +confluxibility +confusably +congas +congeliturbate +congressed +congruencies +conidia +conjugators +connaisseur +connexities +conominee +conrectorship +consentfully +conservancy +conservancies +consonantized +conspection +constitutively +constrain +constrictors +contabescent +contains +contemplatist +contestability +continuing +contracivil +contractibly +contradiction +contraindication +contrepartie +conus +conveneries +conventionality +conventionalizing +conventually +convexes +convocant +cookware +coomb +copalm +copelata +copemate +copesettic +coppersmithing +coproduct +copt +coquita +corbiculum +cordwainer +coregonidae +corelates +corybantian +coryzal +cormac +cornbottle +cornett +corolitic +coronion +corporealization +corradiating +correspondent +corresponsively +corroborate +corticate +corticosterone +cosmetological +cosmine +cosmogonal +cosponsorships +costmaries +costulation +cotarius +cotyliscus +coughweed +coulier +counselful +counselorship +countenancer +counterdevelopment +counterevidence +counterlighting +countermand +counterpotency +counterstamp +counterstep +counterstruggle +counterworking +countrymen +courtezanry +cove +coverup +covin +cowherb +cowyard +cowpoxes +crabmill +craftsmanlike +craggiest +crain +crayoning +cranebill +craniologist +cratches +cratonic +crawdad +creasol +creatureless +creatureship +creditrix +cremule +crescentlike +crescentoid +crewels +cricetid +cricketer +criminal +crinula +cryoscopies +cryotrons +cryptogramma +crisis +crystallographer +crystallographically +crit +criticise +croyl +cromlechs +crookedly +crossed +crosspatch +crotaphic +crowdle +crozer +crozier +cruety +crumbers +crumbly +ctenodontidae +cu +cubs +cubti +cuckoopint +cucurbitine +cuittles +cully +cultelli +cumara +cunctative +cupel +cupid +cuppings +cura +curative +curetted +currycombs +curstness +curtseyed +curuba +curvaceously +cushiness +custodes +cutoffs +cuttyhunk +cuttoe +dacryocystoptosis +dactyliomancy +dactylology +dadaistically +dah +daydreamt +daimon +daised +daytime +damascene +dampish +danakil +dancette +daneflower +daphnaceae +dapples +daresay +darling +dasyproctidae +dasyproctine +datiscin +dauncy +davidic +dcollet +deacetylated +deaden +deaning +dearsenicator +deathsmen +deblaterate +deblock +deborah +decaffeinates +decanes +decapodal +decarburation +deceivableness +deciduously +deciles +decillion +deckhands +deckswabber +declarant +decompensate +deconcentrating +decrepitated +decretalist +deddy +deditician +deepmost +deepwaterman +deerdog +defaultant +defecting +defiatory +deflex +deforces +deforming +defrayable +degasser +degeneracy +deglutinating +deiced +dekaliters +deked +delaminate +delamination +delfts +delicat +delicts +deltation +deluminize +demagogy +demeter +demideity +demiditone +demilune +demiurge +demobilization +demonize +demonized +demulsion +denaturalized +denaturise +dendrocoelous +denotement +dentally +dentatocostate +dentel +denuding +depa +deperdite +depersonalise +depiction +deplanes +depravedly +depreciate +depressor +depthen +deranger +derepress +derm +dermatotomy +dermophlebitis +dermosclerite +desaturation +descendent +descendentalistic +designfulness +desynonymization +desisted +despair +desparple +desperateness +despondentness +destructions +desulphurized +detectible +deterioration +detersively +detroiter +deuterotype +deutoscolex +devast +developmentist +devildom +devoicing +dexies +dextrorsal +dezincing +dhikr +dhobis +dhooti +diachoretic +diadochite +dialytically +dialogic +dialoguer +diaphanously +diaphragmatically +diarian +diarticular +diastase +dibenzoyl +dicephalism +dichotomically +dicyemid +dickybird +dictatorially +didactical +dyeings +dieselized +diesinker +diffarreation +digamy +digestif +digitiform +digraphic +dihydrate +dilapidated +dilatableness +dilligrout +dimercuric +dimidiating +dynam +dynamitical +dinarchy +dinotheres +diobol +diodia +diophantine +diphylleia +diphonia +diphthongic +diplomatics +dippiest +dipsosaurus +dipus +directoral +dirigibility +disappendant +disappointment +disapprobations +disattune +disavowment +disbars +disciplinant +disclosive +disclusion +disconcerting +disconectae +discounting +discoverers +discure +discussant +discussants +diseme +disenfranchise +disequilibriums +disfeatured +disgustedness +disheveling +dishonorableness +dishonourary +disimitate +disincrease +disinhumed +dismay +dismissory +disobliges +disoccupying +disorders +disorganised +dispel +dispense +dispergation +dispread +disprove +dispurse +disqualifications +dissatisfyingly +dissemble +dissemblers +disserving +dissolutionist +dissuasiveness +distillator +distils +distorter +distributable +disulfate +disulphuret +disweapon +divarication +divertible +divertise +divest +divorcible +divulgences +doarium +dockmaster +docoglossan +doctrinism +doddart +dogfish +doggie +dogmatism +dogvanes +doled +dolicholus +dollmaker +domainal +dominoes +donax +dondine +doomsayer +doorless +doormaid +doryphoros +dorsilumbar +dorsoposteriad +doser +doublethink +doubleton +doubtsome +doulce +downbeat +downland +downstate +downthrow +dowset +drabler +dragonesque +dramatise +dramatizer +drate +drawcansir +drawtongs +dreamer +dreepiness +drily +drippers +drivehead +drogoman +droil +drolled +dropped +drouths +drumfires +drupel +duads +dualmutef +ductule +dudleya +duettist +dufrenite +duit +dumpler +dungarees +dunghills +dunt +duologues +dup +durabilities +durions +dutuburi +dwight +dwindled +earaches +earless +earthshine +easier +eastlin +ebonite +ecarinate +echelons +echeneis +echolalia +econometric +economise +ecrase +ectomorphic +ectosarcs +ectropium +editorializations +edmund +educabilian +edwardine +eerinesses +efficiently +effigy +effluvia +egglike +egre +eidograph +eyedropper +eyer +eigenfunction +eikonology +elains +elan +elaphebolion +elb +elchee +eldern +electrodissolution +electroionic +electrolier +electromagnetically +electroplated +electroplating +elegiacs +elemicin +elfish +elian +elicitation +elkuma +eloah +elohist +eluate +elutriator +embarrass +embiotocoid +emblematicalness +emblematology +embosks +embouchure +embrasured +embryonally +emeroid +emissaryship +emollescence +emote +emotivism +emotivity +empirema +emplastic +emplore +enanthematous +enantiomorphously +encallow +encapsuling +encaustically +encyclic +encist +encystments +encompass +endangers +endangitis +endeavourer +endocone +endoconidia +endomesoderm +endopleura +endosporium +endostracal +endowed +enflames +enfold +engagedly +engem +enghosted +engrails +engraves +enisled +enlodgement +enmanche +ennedra +ennoic +enolate +enscrolled +ensheaths +enslavements +enstore +entanglers +enterate +enteromegaly +enthalpy +entirely +entogastric +entomotaxy +entone +entrepreneurs +enturret +entwinement +envolume +eoside +epaleaceous +eparchate +epaulets +epibatholithic +epibatus +epicedian +epicoela +epigeal +epiklesis +epimer +epimerite +epimeritic +epinine +epiplastron +epistemological +epitheliogenetic +epithelium +epitoxoid +epode +epsomite +equalize +equestrienne +equidimensional +equivocality +erastian +erelong +ergatandrous +ergative +ergographic +erysipelatous +erythronium +erythroxylaceae +eroticomania +error +eructating +erump +escobilla +escrow +eslisor +esopgi +esopus +esseda +establishmentarian +esterizing +estheria +estonian +estudy +etamine +etheling +ethic +ethnohistorian +ethnologist +etymology +etymologically +etta +eubranchipus +eudiometer +euglenidae +eulogic +eupatory +euphony +euphuistically +eurydice +eurymus +eutannin +evaluations +evanescency +evaporate +everduring +everywhence +evictions +evilnesses +exalate +examen +examinership +exaspidean +excellencies +excitancy +excitants +excitovascular +exculpative +excursionist +executress +exendospermic +exes +exhedra +exhorters +exinguinal +exorbitation +exorcisms +exorcizement +exosporal +expansible +expansum +expatriates +explainingly +explodent +exploitatory +explosibility +expressless +expresso +expt +extacie +extensional +extinguish +extinguishes +extirpations +extrafascicular +extraocular +extrapleural +extravagance +extraviolet +extremists +fabledom +facia +facilitation +facit +facture +faddiness +faffy +faham +faithful +falchion +false +fame +familiar +famines +famose +fano +fantast +fanums +farrowing +fartherer +fascet +fasciole +fascistize +fastenings +fattiest +faulting +faunal +favored +feasible +featherbed +featherbrained +featy +fedora +feeb +feigher +felineness +felonies +felspar +felty +feminisms +ferlied +fermate +ferociously +ferreiro +ferreted +fertility +fervencies +fetidly +fetterbush +feverishly +feverously +fiars +fiberfill +fibre +fictionalization +fidation +fidgeter +fieldwards +fierasferoid +figbird +figurately +filasse +filices +fills +filose +finagle +finaglers +finicality +finicism +finitive +finitude +firehouse +firstlings +fishbolt +fishfinger +fishiness +fitted +fitter +fiveling +fivers +flabbella +flain +flammability +flapdock +flapperdom +flappier +flashness +flatteress +flaunched +flaxier +fleeten +fleme +fletcherism +fly +fliest +flimsiest +fliped +flirtier +flockless +floodways +florent +floridian +flosculous +flossiest +flote +fluency +flummadiddle +flunkeyism +fluorindine +fluorography +flustra +fluviatic +fluxibly +fogproof +foister +folic +folios +folksinger +followership +foolhardiest +foothook +footrope +footslogging +foraminifera +forceless +forcipal +forebroads +foredestined +foredispose +foreleech +foremade +foreordinated +forepast +foresaid +foreshadower +foretoken +forewit +forgeability +forgettably +forksful +formalizable +formfeeds +formularism +formulated +fortaxed +fortunes +forumize +fossilisation +fouettes +foundrymen +fourche +fourther +foveolas +frayedness +frailties +frameableness +frangula +frap +fraud +frecken +frecket +freeboots +frenchification +frenulum +frettiest +frictionize +friesian +frigotherapy +fringilloid +friponerie +fritillaria +frontogenesis +frontooccipital +fronts +frosting +frowsty +frozenness +fructifier +fuchsite +fueler +fuguing +fuirdays +fulgent +fumago +fumarium +fumblingly +funebrous +fungistatic +funky +furfuration +furfuroid +furiosa +fusains +fussbudget +fustianish +fuzzes +gabble +gabbroid +gaeltacht +gagwriter +gaits +galactolytic +galactosan +galenian +galerus +gallberry +galliard +galoubet +galumphed +galvanometrically +gamely +gammadion +gammoners +gams +gan +ganocephala +gantries +gapers +garial +garret +garroting +garshuni +gashest +gaskin +gaspingly +gastromancy +gastrostegal +gastrotaxis +gatch +gauchely +gavelman +gawgaw +gawky +gazers +geyserite +geisotherm +gelidities +gemlike +gemmipara +genealogy +general +generalate +generosities +gentlemanism +gentlemanship +geochronometric +geodiferous +geoff +geologising +geophyte +georgiana +gerkin +germanophilist +gerrymanderer +gertie +geste +gesticulator +getae +ghaznevid +gib +gibbous +gifted +gigeria +gigi +gile +gilpy +gymnocerata +gynaecol +gynandria +gingering +gyniolatry +gypaetus +gipon +girling +girnie +girondist +gyrophora +glacialism +gladdens +glaked +glandiferous +glareproof +glassen +glens +glent +glyceria +glycerination +glyconian +glyphic +gloating +gloeal +glorious +glossanthrax +glossarial +glossolaly +glowered +glucolipine +gluteoperineal +glutinant +gnaw +gnomesque +goalie +gobble +goddamned +godliness +goggle +goiterogenic +gomarist +gomart +gonangium +gonyocele +gonococci +goodeniaceous +goodyism +goodmen +gordian +gorraf +goshdarn +gospelmonger +gotta +goujay +governess +gowl +grabby +grainer +gralline +gramophonically +granadine +granatum +grandfather +grandparental +granulators +graphite +graptolitoidea +graspingly +gratifications +graves +gravigrade +greaseball +grecized +greeny +greenland +gresil +grewsome +grieveship +griffinhood +grimines +grinds +grise +groaning +gromia +groping +grossularia +groundwave +grovellings +grubless +grumblingly +grummer +grx +guaka +guardroom +guarrau +guelphish +guetare +guids +guilelessly +guiro +gulaman +gullability +gunman +gunnies +gunshot +gurjun +gustiest +guttled +haaf +haars +habilimental +hacksaw +hadean +hadnt +haemocyte +haffits +hagein +haglin +hayfields +haying +haircloths +haitian +halfungs +halicarnassean +halidoms +hallahs +hallucinogenic +halmawise +halolimnic +hammock +hancockite +handbills +handholds +handybook +handled +hangmanship +hangs +hankeringly +haquebut +harbors +hardenite +harelike +hariolate +hark +harlequin +harpwaytuning +hartin +harvestfish +hastilude +hatchettin +hatchettite +hatemongering +hausfrauen +haverer +hazanut +hazle +headbander +headquarter +headshrinker +heaper +heartburn +heartsomeness +heavenishly +heavy +hebraic +hederose +heehawing +heeltap +hegumene +hegumenes +helichrysum +helicoidally +heliochromoscope +heliologist +hells +helvetii +hemagogue +hemarthrosis +hematocrystallin +hematology +hematozoic +hemidactylous +hemiparaplegia +hemispheroidal +hemitriglyph +hemocytoblast +hemolymph +hendecyl +hennin +henotheistic +hepatectomized +hepatoenteric +heptateuch +heptatomic +herbiest +hereditative +hereof +herl +hermeticism +hermetics +hernsew +hertz +heterocaryosis +heterocentric +heterogangliate +heterograft +heterographical +heteronereid +heterotactic +heterotrophic +hexahydrobenzene +hexameron +hexapetaloideous +hgrnotine +hyacinthia +hyalospongia +hibernacular +hyblaean +hydatogenic +hydriatry +hydrocarbonaceous +hydroclimate +hydrodamalidae +hydrogeological +hydrolatry +hydrometallurgically +hydrophilicity +hydrosilicon +hydroxylactone +hydrozoal +hierography +hierology +hygeian +highpockets +hightailed +hillside +hylobates +hilts +hilum +hingeless +hyoidean +hypalgia +hyperaesthesia +hyperbatons +hypercalcinaemia +hypercharge +hypergolically +hyperhidrosis +hyperintellectualness +hyperkalemic +hypernatremia +hyperovaria +hyperpiesis +hypersensuously +hypertropical +hypnoanalyses +hypnoidize +hypnologic +hypobasal +hypocrinia +hypogastria +hyponome +hypopetaly +hypophyllum +hypostatically +hypotactic +hypothetize +hippic +hipponactean +hyps +hyracoidea +hircine +hirmologion +histidins +histiocyte +historical +hitchy +hlorrithi +hoaxes +hobbil +hobblers +hobnailer +hognuts +holdingly +holer +holiest +holmic +holocarpous +holoplanktonic +holotypic +homalopsinae +homeotherapy +hominian +homocycle +homoeotypical +homogenetic +homogenies +homologies +homophyly +honeybloom +honker +honorably +hoodooism +hoofed +hoondi +hopoff +hoptree +horizontical +hornfels +horreum +horrify +horrifiedly +hosiomartyr +hospitalism +hostship +hotbeds +hottonia +houri +houses +howks +hubb +huckmuck +huddledom +hues +hulloa +humate +humicubation +humorous +humorsomeness +hungar +hurdis +hurgila +husheen +huskened +huzzahed +yaguarundi +yamanai +yamshik +yaply +yarder +yarmulke +yawningly +icebreaker +iceleaf +ichneumonology +ichthyomorphous +iconicity +icteroid +ideationally +idee +ideoglyph +idiophanism +idioticalness +idlement +idol +yearock +yellowing +yens +yestern +igelstromite +igloo +igniting +ignote +ik +illess +illiberalism +illimitableness +illness +illustrator +illutation +imagerial +imbitters +iminourea +immaterialism +immatriculate +immersions +immethodic +immiscibly +immunologists +immunopathologist +immutableness +imparticipable +impawning +impending +imperceived +impersonalisation +imperspicuity +impersuasible +impervertible +implosions +importunes +impossibilist +imposting +impressa +imprimatura +imprimitivity +improcreant +impuration +inactivate +inantherate +inappropriableness +inarticulata +inartificially +incalescence +incapacitant +incave +inchangeable +incisor +incoagulable +incognito +incommodity +incompleted +inconfusion +inconsumptible +incontrovertibleness +incorporative +incorporator +inculcates +inculpatory +incuriosity +incuss +indemnization +independentism +indicible +indifferentness +indigenously +indirectly +indiscrimanently +indistinguishably +indophenin +indulgent +indult +induviate +inebrious +inefficacious +inequilobed +inexposable +inextricability +infall +infantlike +infections +infecund +infinitate +infinitesimality +inflictable +infortunateness +infralittoral +infrequence +infundibulata +infuriating +ingenital +ingratiate +ingulfment +inhumers +inimically +injun +inken +inket +inlanders +innovates +inobediently +inobservant +inquaintance +inquilinae +inracinate +insearch +insectaria +insep +insession +insidiosity +insoles +inst +instarring +institory +instrumentalism +insubmersible +insults +insurgently +intagliotype +intellected +intensional +intent +interattrition +interbanking +interchannel +intercharging +intercolumnal +intercourse +intercranial +interfold +interfraternally +intergalactic +intergrappling +interhemal +interlocated +intermarry +intermediateness +intermise +intern +internatl +interneuronic +interpel +interrace +interracialism +interrelationships +interresponsibility +intersector +intersperse +interstrial +intertubular +interwhistled +interwind +intimidity +intituling +intolerable +intraabdominal +intramental +intranslatable +intricable +intrigo +intrinsical +introducers +intubatting +intune +invader +inveneme +investigated +invigorated +invoice +involuntary +involvement +yocking +yogins +yogoite +ionic +youngers +youthlikeness +ipso +iranize +yrbk +ires +irishness +ironlike +irreduction +irreg +irrelevances +irresistibility +irresolvableness +irritatedly +isagon +isazoxy +iscose +isle +isochasmic +isoclasite +isogamete +isogonal +isogonics +isolate +isometries +isospories +isothiocyano +isovoluminal +israelite +itala +italici +ithomiid +yules +yurucare +ixodid +jacals +jackknives +jackpuddinghood +jacks +jacksnipes +jailership +jain +jamboree +jambul +japaconitin +japishly +jarrah +jarvies +javanine +jazzy +jeer +jeff +jejunities +jere +jessamy +jetty +jibbed +jibman +jicaque +jimmy +jins +jipper +jockeys +jocularly +joyleaf +jokingly +jolting +jonvalize +joshua +journalised +joutes +jowls +judicializing +jugums +jujutsus +juloid +jumbos +junctures +juramentados +justicia +juvenals +juvenilism +kabaka +kairotic +kakistocratical +kaleidophone +kalema +kaligenous +kanarese +kaons +karyatid +kartings +katacrotic +katagenesis +kathemoglobin +kauch +kebbie +keelboatmen +kellion +kellock +kelty +kenyans +keratinose +keratoidea +keratotic +ketchups +ketonimide +khaldian +khepesh +khitmatgar +kibbutz +kibitzers +kicktail +kilneye +kilo +kilohm +kinchin +kindred +kinetomer +kininogen +kinsman +kirve +kiss +kisses +kiswahili +klipspringer +klismos +knappy +knavery +kneads +knobbing +knorhmn +knothorn +knower +koibal +kolkozy +kolo +kopjes +korntunnur +koshered +kosmokrator +kreis +krypticism +kubera +kumyses +kutta +kwashiorkor +lability +labiogression +laboring +labrets +lachnosterna +laconicness +lacrimatories +lactimide +lactobaccilli +ladykin +laetrile +lagen +lagnappes +lagwort +lalapalooza +lamas +lametta +laming +lamnid +lampers +lamprophonia +lancasterian +landflood +landwrack +lankly +lansa +lapacho +laparohysterectomy +lapelled +lapideous +lapsability +laryngopharynxes +larkier +larkspurs +larvule +lashness +lassieish +lath +latham +latitudinarianism +latrias +launchable +laurae +lavational +lavishers +lavishness +laxatively +leachman +leafage +leakily +leathercraft +leatherleaf +lecidioid +lecturer +lecturette +leeroway +legger +legislatorship +legit +legitimatized +lemaneaceae +lemmus +lenaean +lentisco +lentor +lepidium +lepidosaurian +leporiform +leptorrhinism +letch +letitia +lettuce +leucocidic +leucostasis +leukemoid +leverage +levigation +lewises +liard +liberomotor +liberum +libidinous +lychees +lichenification +lyddites +lidless +lienopancreatic +lifts +lig +likeable +lyken +likes +likker +lilliputian +lilliputianize +limodorum +lymphangitic +lymphographic +lin +linalool +lindleyan +linearize +linendrapers +linguatulida +linwood +lyolytic +lionproof +lyophilize +lipomorph +liquate +liquefied +liquors +lysogenetic +literalizing +literarian +lithofellinic +lithographize +lithoidite +lithotomize +little +lituus +liverworts +loanmonger +loathed +lobachevskian +lobbygows +locative +lockages +locular +lodowick +loft +logics +logometrical +lokacara +lollygag +lolo +longear +loof +loofness +lookers +loots +lordlily +loriners +lotophagous +louden +loudspeaker +louk +lovers +lowlier +lubricator +luces +lucific +lucration +lugs +luite +luminate +lunary +lundinarium +lunkheads +lupicide +lures +lutecia +lutidinic +mabolo +macanese +macarized +macellum +machinify +macrodome +macrolepidopterous +macrometeorology +macroptic +macroseptum +madagascar +madded +maduros +magically +magistrand +magnetobell +magnetometrically +magnification +mahesh +mahogonies +mahound +mahuangs +mainward +mayoress +majordomos +majorize +makaraka +malacopoda +malaguetta +malcontents +maleability +malignantly +mallear +malmstone +malonic +malta +mammals +mammea +manageability +manciple +mandorla +mandrils +maneges +mangerite +manyema +manifesting +mannering +manneristically +mannites +mantelet +manufactor +manweed +mapo +maranatha +marblings +marchioness +marci +maremmese +mariology +marksmanship +marlins +marmoric +marquee +marrowless +marteline +maru +marvelling +masher +mashers +masochists +mastheading +mastigote +mastologist +mastoplastia +mat +maternities +maties +matrilaterally +mattery +maugre +mavournin +mavrodaphne +maximin +mazapilite +mbori +mealymouth +meatuses +mechoacan +meckelian +mecurial +mediaevalize +medicostatistic +medimno +medisance +megachiropteran +megalaema +megalosphere +megalosplenia +megaweber +megohmit +melamine +melancholia +melanitic +melitaea +mellah +mellitate +meloids +melonechinus +meltedness +memorable +menagerie +menfra +mensalize +menstruosity +mensuration +mentalists +mercenaries +mercurify +mercurize +meriquinonoid +meritocrat +merls +mesartim +mesentoderm +mesethmoid +mesiodistal +mesocolic +mesonephros +mesoscutal +mesoseme +messmen +metabolised +metachemic +metachemical +metallik +metallophone +metanephroi +metapectic +metapophyseal +metates +metazoea +metergram +methacrylic +methysergide +metis +metrocampsis +metromalacoma +meum +mezentian +miami +miaouing +mickery +mycosis +micraner +microbes +microcardius +microdiactine +microfibrillar +microgram +micronometer +microphytal +micropyle +microprocessor +microscopial +microsomal +microtheos +midas +midfield +midwifery +myelapoplexy +myelonic +myelosarcoma +mightnt +migonitis +milanese +milleped +milleporous +milliary +millier +millionairess +millrind +mylodei +minahassian +minatory +minguetite +miniaturization +minimal +ministrate +minoan +myoalbumin +myoelectric +myogenetic +myoplastic +miranha +myriare +mirkly +myrmotherine +misaccentuation +misaddressed +misapprehend +misatoned +misaver +miscal +miscondition +miscounseled +miscreancy +miscreated +miscrop +miserableness +miserliness +misfiles +misinflame +misinformer +mislain +misliker +mismarked +mysophilia +misotramontanism +mispronounced +misquote +misremember +missel +missourite +misstarted +misterming +mysticise +mistypings +mistreated +misword +mythos +mithraistic +mitiest +myxinoid +myxochondrosarcoma +mmf +mnemonization +moaned +mobcaps +modena +moderately +moier +moilers +moireed +moism +mola +molality +molesting +moment +monachal +monarchial +moneybags +moneylender +monetise +mongrel +moniliaceae +monoacetin +monocotyledon +monodrama +monoecia +monogenistic +monoglycerid +monomolecularly +monophonic +monopylaria +monopterous +monosulphide +monotrichate +montage +montaging +mooch +mool +moola +moonscape +moot +mopoke +moratory +morigeration +morish +mormyr +morning +moromancy +mort +morthwyrtha +moselle +mot +moted +motiles +motocross +motors +mouls +mountaineers +mountebankly +moustoc +mt +mucate +muchly +muckman +mucomembranous +mudra +muggy +mulching +muletress +mulita +mulletry +multifil +multiliteral +multiparae +multiplicities +multisacculate +multisonorousness +multitube +mumpishness +mundal +muralists +muring +murphying +murre +muscardinus +muscogee +musefulness +muskier +mussier +mutation +muticous +mutilative +muttonheadedness +mutwalli +nabataean +nailsmith +naivetes +nakir +nanawood +nanisms +naphthalene +naphthyl +narcotical +nargileh +naseberries +nasolachrymal +natatoriums +nationalists +nattock +nauseation +nauticals +nav +naval +nebulosus +necessitates +neckenger +necroscopic +necrotomic +nectarial +neeld +negativism +neighborhood +neighbourliness +nemocerous +neo +neolithic +neophytism +neornithes +neotoma +nephroparalysis +nephropathy +nervy +nestorian +nettier +neuralist +neuratrophic +neurepithelium +neurofibrillar +neurophile +neuropore +neutrophile +nevada +newborn +newsmongery +newswriter +nichrome +nicks +nicotize +nidering +niffers +niggling +nighed +nighting +nilous +nimming +ninnyish +nipper +nishiki +nitpick +nitrochloroform +nixies +noctiluca +noctilucence +noctuid +noel +noels +noisefully +nonabsolutistic +nonaccidentally +nonactionably +nonacuity +nonadditive +nonambiguities +nonanalogic +nonanalogically +nonappeasable +nonartistically +nonatrophied +nonbeneficialness +nonbetrayal +nonbodingly +nonbusyness +noncataclysmal +nonchaotically +noncharacteristically +noncollapsible +noncommutative +noncompensating +noncompos +nonconcordant +nonconsecutive +nonconstraining +noncontemptuously +noncontradiction +noncreditable +nonculpableness +nondeclarative +nondeclarer +nondegreased +nondeliquescent +nondepressing +nondescript +nondetractively +nondisciplinary +nondistortedly +nondivergencies +nondogmatical +noneffervescently +noneffusively +noneligible +nonenforceability +nonequitably +nonethyl +nonexistent +nonexpiration +nonextensible +nonextermination +nonextracted +nonfictional +nonforfeitable +nonforfeitures +nongame +nongeologic +nongilled +nonglandered +nonhyperbolic +nonigneous +nonignominious +nonimitable +nonindividualistic +noninjury +nonintimidation +nonintrospectiveness +nonkosher +nonliberation +nonlive +nonlucratively +nonlugubriousness +nonmetals +nonmimetic +nonmotorist +nonmutationally +nonnicotinic +nonobscurity +nonofficeholding +nonoptically +nonparalysis +nonparity +nonperceptual +nonpersuasively +nonpolarity +nonpornographic +nonpresentational +nonprintable +nonprobation +nonprolificacy +nonprominently +nonqualifying +nonrealities +nonrebel +nonrecurently +nonregenerate +nonrequisition +nonresilient +nonresponsibility +nonretraction +nonsatiric +nonsecession +nonsegmental +nonsenses +nonserif +nonshrinking +nonsingularity +nonsolvableness +nonsovereign +nonspored +nonsubstantiality +nonsubtractively +nonsurvival +nonswearing +nonteetotaler +nontelic +nonthermoplastic +nontyrannous +nonurgently +nonuses +nonvalidity +nonvaluable +nonverticality +nonvisaed +nonvital +norfolkian +norgine +northernmost +northwestwardly +nosebleed +nosocomial +notary +notedness +notoire +notwithstanding +noughts +novellae +nowheres +nubby +nucin +nuculiform +nugaciousness +nulls +numberlessness +nuncupative +nursable +nurseries +nutramin +nutritiously +obdormition +obelises +obelism +obelizing +obfirm +oblocution +obloquial +obscurantism +obsignation +obstructionism +obstructive +obversely +occamy +occipitoaxoid +oceanologist +ochery +octahedrical +octastylos +octoad +octocotyloid +octopuses +odelet +odious +odontotechny +odoured +oedipuses +oenotrian +offers +offtake +ogham +oglala +oh +oireachtas +oysterhood +olefines +oleocalcareous +oleose +olid +olympionic +olitory +oliver +omnisciently +omnisufficiency +omnivident +omnivorism +onager +onagraceous +oneirocritical +onker +onsetting +oodles +oometric +ootocoidea +opa +opedeldoc +operae +operative +ophiurida +ophthalmomycosis +opinions +opisthotonus +opobalsamum +opposer +oppositipolar +optimism +opusculum +oracler +orate +orbs +orchestian +orchioscheocele +ore +oreads +organogenetically +organon +organonomic +oriel +oryzanine +ornamenter +ornate +ornithorhynchidae +ortalidian +orthocarpous +orthoceratidae +orthodontia +orthodromics +orthovanadate +oscheal +oscinis +osmate +osphresis +osselet +ostearthritis +osteoclasia +osteomas +ostracoda +ostrichlike +otariidae +otoantritis +otodynia +ousels +outact +outargues +outbeg +outbleats +outcastness +outcrossed +outdrove +outen +outfabled +outfawn +outfields +outgrows +outjinx +outlean +outluster +outname +outpassion +outplanning +outpleased +outrange +outreaches +outsentries +outsizes +outsophisticate +outsparkling +outstand +outsteering +outsulks +outswears +outvote +outwiggled +ovariocele +ovatooblong +oven +overaction +overaggravated +overappreciativeness +overbeat +overbroil +overburthen +overcapitalization +overcheap +overchill +overconcentration +overconsiderately +overdearly +overdefined +overderisively +overdistantness +overdoses +overeffusive +overemotionalness +overexertion +overfervently +overfishing +overfloat +overflow +overhale +overhastily +overhearty +overhigh +overimpressibly +overindulge +overindustrialized +overkindly +overlaugh +overluscious +overluxuriantly +overmagnification +overmortgaging +overmultiplying +overofficious +overpersecute +overpiousness +overpoeticized +overproud +overproudness +overreducing +overregulated +overruffs +overscrub +oversensitiveness +overseriously +oversews +overslop +overspun +overstand +overstridently +oversuspiciously +overswarm +overtarry +overtimorousness +overtop +overwarm +overwide +overwrap +oviparousness +ovolos +owercome +oxadiazole +oxalan +oxbird +oxygenated +oxyhaemoglobin +oxyurous +ozone +pablum +pacately +pacesetter +pachysandra +pactional +paedology +paedological +paella +pagiopod +paguma +paijama +paired +palaemon +palaeoentomologist +palaeoniscoid +palaeontologic +palaeornithological +palechinoid +paleobiological +paleobotanically +paleological +paleopathologic +palgat +palladodiammine +palmcrist +palmellaceae +palmlike +palpitates +pamaquine +panaceas +panayano +pancreaticogastrostomy +panders +pang +pangamous +panglima +paniolo +panshard +pantachromatic +pantascopic +pantie +pantryman +papayan +papegay +papyrin +papyrotype +parabling +parabomb +paradenitis +paradisaic +paraenesize +parale +paralysis +paramedics +parameterizing +paramnesia +paraphonia +paraphraster +pararctalia +parasol +parawing +pardahs +pardanthus +paretic +parhelia +parietovisceral +parings +parisian +parodoi +parqueting +parrakeet +parrots +parsonity +parthenocarpically +parthenogone +partialed +parviscient +pasquillic +pasquino +passbands +pasted +pasteurisation +pastorals +patchworky +pate +pathogenesis +pathopoiesis +pato +patricio +patrilineal +paucal +pauciloquy +paulinistic +paved +paviser +pdn +peacockish +peanuts +peasanthood +peckishness +pectic +peculator +peculiarize +pedately +pedimana +pedipulation +peeoy +peerlessly +peesash +pein +peyotism +pelite +pellotin +pelu +pelvimetry +penbard +pendicle +pennatulacean +penniless +pennoned +pensioned +penstick +pentastich +penthouselike +pentosuria +penumbral +peploses +peppiest +perambulator +percy +percipi +percrystallization +perdure +performs +pergamene +pericaecal +pericarpic +pericles +pericu +perigynous +perihysteric +periphrased +periphrasis +periplastic +perirraniai +periscopal +peristeropodan +peritoneum +peritrochium +pernis +pernitrate +peroratorically +perpetuators +perradiate +perscrutator +persifleur +perspicable +persulphide +perversite +peshito +peskiness +petals +petcocks +petrea +petrographical +petrosphenoid +peugeot +phacoanaphylaxis +phageda +phagocytolytic +phanos +phantasying +phantom +phantoscope +pharisees +pharmacogenetics +phenazone +phenmetrazine +phenocopy +phenocrystic +phenolions +phygogalactic +phylesis +philiater +phylloceratidae +philocalist +phylon +phylonepionic +phylumla +physes +physicochemist +physiochemical +physophore +phytochlore +phytosociology +phlebographic +phlegm +phlegmaticness +phocenic +phonetician +phonodeik +phoronis +phosphatase +phosphonium +phosphorolytic +photoautotrophic +photoengrave +photolithographer +photomicrographical +photon +photophysicist +photopia +phototypically +photovoltaic +phthalanilic +phthiriasis +phulkari +pianistically +pickableness +pickaxed +pickeers +pycnodont +piecer +pieceworker +pieing +pyelitises +pierinae +pierrotic +pigful +pigsties +pilch +pilgrimdom +pilifer +pillowless +pilules +pimiento +pinards +pinckneya +pinelike +pinkishness +pinningly +pinocytotic +pinwales +pyopneumocholecystitis +pyorrhoea +pipelaying +piperidine +pyramidic +pyrenolichen +pyretogenesis +pyrocatechol +pyroglazer +pyrometallurgy +pyrophoric +pyroxenes +pisciculturist +pisciform +pitchfield +piteous +pythagoras +pities +piwut +pizzas +placeman +placemanship +plagiariser +plaint +playsomely +plaited +planaea +plancier +planosubulate +planting +plasmapheresis +plasmophagy +plastogene +platycercinae +platydolichocephalic +platypi +platonian +pleadingly +pleasurelessly +plectrum +pleiotropy +plenipo +plesiobiotic +plethodontid +plighting +plimmed +plinths +plosion +plotinism +plottingly +plumbable +plummy +pluricentral +pluripresence +pluriserial +plusia +plutologist +pneumonodynia +pneumonopexy +pocketful +podder +podometry +podware +poephagous +poetiised +poetised +pointleted +poisonfully +polarographically +poleyne +polyanthous +policeman +policemen +polycyttaria +polydactyly +polygalas +polygamize +polymere +polymorphy +polynya +polyphone +polypneic +polypodia +polysarcia +polysyllabical +polystellic +polytyped +pollyannish +pollinar +pollywogs +polonism +pome +pomes +ponderously +ponerid +poorga +popely +poplitaeal +popularizations +populous +porencephalic +porometer +porphyrized +portato +portresses +portunian +pose +poshest +posses +postdating +postelection +posterity +posteroterminal +postingly +postlegal +postmalarial +postphrenic +postscapula +postzygapophyseal +potance +potato +potator +potophobia +potshotting +poultry +poverish +powderize +pozzolan +practicalness +prague +praham +prasine +prated +preachy +preadjunct +preadvocating +preaggravation +preallegation +preallege +preartistic +prebronze +precanceling +precentorial +precyclone +precinct +precipitinogen +precollection +precommunicate +precomprehension +preconfusion +preconsumed +precool +precuneate +predamnation +prededicating +predeluded +predentary +predesperate +predined +predischarge +prediscriminating +preduplicate +preemancipation +preendorse +preening +preentertainment +preexcused +preferentially +preformist +prefulgency +pregain +pregeniculum +prehend +prehended +preimperial +preinsertion +prejudged +prelatical +prelatry +prelingual +preliteral +prelumbar +premonitory +premune +premuster +preobservance +preoccupy +preotic +prepaved +prepyramidal +preprints +prepromise +prequarantined +prerefine +prerevenging +presbyophrenia +presbyter +presental +preshape +preshelter +prespecifying +presphygmic +pressers +presters +presurround +pretasted +pretensiveness +preternormal +pretheological +prettifies +prevails +prevent +prewashed +prewiring +prideweed +priestess +primigenian +primly +primogenetrix +primuses +priors +prismoid +prizers +proacceptance +proalteration +probatum +problemistic +procatarctic +procerebral +procyoniform +procrastinated +proctor +proctoral +producers +proempiricist +profanableness +profanities +professionalisation +proficiency +prognosed +projectedly +projectionist +prolegomenist +prolifical +prologlike +prologuizing +prolongment +pronegotiation +proofy +proofs +propaedeutic +propanediol +propylation +propionaldehyde +proportionably +propulsory +propurchase +proscutellar +proselike +prosenchymatous +prosodically +prospecting +prospectively +protector +protectrix +protend +prothyl +protistological +protogenesis +protogynous +protoneurone +protopragmatic +prototyping +proudness +proveditor +provisioned +provitamin +proximolingual +pruh +psammocharid +psephisma +pseudoanthropoid +pseudoasymmetrically +pseudobacterium +pseudoclassical +pseudocoelome +pseudogyne +pseudoinvalid +pseudomilitaristic +pseudomorphine +pseudomutuality +pseudophallic +pseudopupal +psych +psychean +psychoanalytic +psychoid +psychologies +psychoneurotic +psilocin +psoriasiform +pterichthyodes +pterygomalar +pteron +ptyalize +publicizes +pudencies +pudendal +puebloization +puerpera +puggiest +pulmonectomies +pulper +pulton +pulverous +pumper +pumpmen +pundum +puny +punily +purgatively +purgatorian +purify +puritans +purlieumen +purpuroid +purveying +pushdown +putted +qere +qid +quader +quadricycler +quadrifolious +quadrilled +quadrisulcated +quaigh +quakiness +quantizer +quarle +quartered +quarterization +quaternionist +quatsino +queerly +quenselite +querulity +quibus +quiesced +quieted +quimper +quinate +quinquennalia +quinquepetaloid +quintain +quintessentially +quirkish +quistron +rabat +rabbets +rabific +rabot +rabulous +racecard +rackingly +radical +radicel +radiodynamic +radiophoto +radiosurgeries +raffinase +rafflesia +rahul +raillery +raines +rayon +ram +rambutans +rammass +ramrod +ranchmen +randn +randomness +ranselman +rappees +raptury +rashnesses +rasselas +rasty +rateen +ratines +rationless +raun +raxing +rea +reabsolve +reacclimate +reaccompanies +readies +readiness +readmission +realter +reamputation +reannotating +reapplies +reassembled +reassessing +reassimilating +reata +reawaking +rebox +rebred +rebronze +rebuff +rebuttoning +recable +recarbonize +recertificate +recharter +recidive +recitatif +recivilization +reclines +recolonize +recommencing +recompounding +reconcur +reconfirmed +reconnection +reconverged +recounter +recrayed +recruitage +rectococcygeus +rectorrhaphy +recurring +recurvirostra +rededicated +rededuction +redemptively +redeploy +redigestion +redisciplining +redistrain +redneck +redry +reearns +reechoing +reeden +reencouraged +reenjoy +reenjoyed +reexcavation +refects +referencing +refilters +refinances +reflexiue +reforming +refractures +refresher +refreshments +refrustrated +refunder +regild +regioned +reglossed +regrated +regretableness +reguardant +rehearhearing +reheats +reid +reimport +reinculcate +reined +reinquiring +reinstructing +reinstructs +reinterchange +reis +reiter +relaunders +relaxative +releases +reliances +relict +relievos +reliquaire +remagnified +rematched +reminders +remising +remisrepresentation +remitters +remodulated +reneague +renegading +renegotiating +renointestinal +renu +reoffer +reorienting +repad +repandly +repandousness +repersonalize +rephonate +replaned +replough +repondez +reprehensible +repressions +repry +reproclamation +repropitiation +republicanised +republished +reracker +rereader +rerummage +resample +reselecting +resentments +reseparated +resheathe +reshun +resignment +resinosis +resistlessly +resoundingly +resources +respectless +respirit +ressaldar +restaffed +restorationer +restraint +resubmerging +resurgency +resweep +retainership +retching +retentionist +reticencies +retinule +retore +retranquilize +retroceded +retrocostal +retrofracted +retrogressive +retrolocation +retroversion +revelationize +revenger +reverberators +reverendship +reversos +revindicate +revisee +revisory +revulsion +rewelds +rhachides +rhamnite +rhapontic +rhapsodic +rhapsodized +rhymers +rhynchocephalia +rhinoscopy +rhizocephalan +rhizomorphoid +rhodesians +rhodospirillum +ribbonwood +riboflavin +ribzuba +rickmatic +ricocheted +rifacimenti +righter +rigourism +riksdaalder +rillock +rymandra +ringlike +ripperman +ripplier +risen +rituals +rivets +rivetting +roadability +robles +roccellin +rockelay +rockwood +rodster +royalisation +rolamites +romancealist +romanize +romps +rontgenize +roodstone +roomful +roridulaceae +roritorious +rosaries +roseine +rotatoplane +rotta +rouche +roughdrying +roughy +roundwood +rouths +rows +rubensian +rubiginous +rubin +rubrician +rudous +rued +ruglike +rummes +rumourer +runnels +ruptures +ruralist +russel +russeting +russophobiac +saberlike +sabred +sabretache +saburration +saccharomycosis +saccoon +sacramentalis +sacropectineal +saddlecloth +saffarid +safranins +sagging +sai +sayest +saintly +salal +salamandarin +salify +sallymen +salm +salta +saltatorious +salticid +salubrious +salutatoria +sammel +samsam +sanctification +sandalling +sandburr +saned +sangu +sanidinic +sanity +santorinite +sapharensian +saprogenic +sapropels +sarcoidosis +sarcophagy +sarcophilus +sardinia +sassaby +sassed +sassoline +satirism +satyromaniac +satrapical +sauna +saurischian +savages +saviors +sawbill +sawtooth +scabbarding +scalade +scalar +scampers +scandix +scapegoats +scaphiopodidae +scaping +scarcen +scathful +scats +scepters +sceptical +schellingianism +schillerizing +schistosis +schizaeaceous +schizogenetically +schoolbook +schooltime +schorly +schraubthaler +schryari +sciagraph +scintling +scious +scytheless +scleromeninx +scleroparei +scogginism +scolopendridae +scooper +scopine +scores +scorpionfish +scorpionid +scraily +scranky +scrapping +scratchcat +screendom +screwbarrel +scrinch +scrinia +scrofulitic +scrouger +scruples +sculpturer +scumming +scumproof +scutellarin +scutiform +seagirt +seals +seated +seawan +secondly +secretaryships +secretively +sectionalize +secularizes +secundines +seedcase +seemingly +segnos +sehyo +seidlitz +seismologist +selachoid +selenitiferous +selter +semanteme +semeiology +semiamplexicaul +semianimated +semibaldly +semiconceal +semicubit +semideserts +semidiaphanousness +semidole +semiexternally +semiforeign +semilanceolate +semimonarchically +semimonthlies +seminaristic +seminate +semiocclusive +semipatterned +semiprotected +semiquietist +semisavagedom +semishade +semistory +semitae +semiweeklies +semostomeous +sempiternous +senora +sensuousness +sententiary +sentimentaliser +sep +sepal +septaugintal +septifolious +septimetritis +seraphin +serenify +sergeantfish +sergeantfishes +sericitic +sermonizes +serolactescent +serose +serpents +servers +servocontrol +seseli +sestuor +setoff +setworks +seve +sexdecillions +sexisms +sforzando +shackly +shadowly +shaharith +shakeout +shallots +shallowhearted +shamoys +shandyism +sharrag +shastraik +shavetail +shawms +shebeens +sheefishes +sheepcrook +shelta +shelvy +sherifa +sheriyat +sheristadar +sherpa +shillety +shinto +shinwood +shiphire +shirl +shmaltz +shocked +shoguns +shona +shorelines +shorthander +shotguns +shou +shovelard +shrewdest +shrievalty +shrivelled +shuffling +shutout +sialogogue +siamese +siccing +sices +sideband +sidelingwise +sidewinder +siemens +siennas +sigh +sigillariaceous +signalities +signifies +sikh +silicotalcose +silkwood +syllabical +syllabubs +sylleptical +silverizing +silverside +sylvicolidae +symmetricalness +symmetries +simonial +sympatheticity +symphilism +simulacra +simulates +simulioid +synalgic +synartetic +syncellus +syndrome +synechist +sinecureship +synemata +syngenesian +syngenesious +sinistrin +sinonism +sinsyne +synthermal +synthesizes +sinuatrial +sinusitis +siphoneous +syriacism +syringium +sisi +systematicness +sister +systolic +sitao +sixteenmo +skaalpund +skatiku +skeletonised +sketchier +sketchpad +skiddoo +skyey +skilligalee +skiplanes +skippel +skittyboot +skivers +slabber +slagless +slanguage +slashings +slavian +sleazy +sleet +sleetier +sleeveless +slimnesses +slipband +slipcase +slobbering +slote +slovaks +slovenlike +sludge +sludgier +slur +smartism +smegma +smidge +smilacaceae +sminthuridae +smithcraft +smokescreen +smudginess +snab +snakeblennies +snapping +snarlier +sneaky +sniffishness +sniggeringly +snobber +snobbier +snoozle +snowbrush +snowplowed +snuggles +soapolallie +soapstoner +soavemente +societyless +sociogram +sodomite +soiled +soilless +soken +solates +soldatesque +soldierize +solenial +solidomind +solonetzicity +solubly +somatopleural +somebodies +somegate +somewhats +somnambulency +soodly +soogeeing +soorkee +sophisticator +sorbates +sorcery +soricoid +sortation +sostinente +soubise +soubrette +sourwood +southeastwards +souush +sovereignness +sp +spacially +spalpeens +spanless +spanners +sparkling +spartacide +spatha +spathilae +specialism +specifications +specimenize +spectatress +spectrography +speils +spelterman +speltz +spermatitis +spermatozzoa +spete +spetrophoby +sphagnology +sphenoiditis +sphragistic +spicae +spicula +spidger +spilings +spilogale +spinoid +spinproof +spinulosely +spirillaceae +spirographic +spites +spizzerinctum +splashing +spleenish +splenical +splenotoxin +splotchy +spoilfive +spondylopyosis +spoolers +spoonerism +spoot +sporangiferous +sporidesm +sporozoid +spotrump +spridhogue +springeing +springwater +sprit +sprush +spuriousness +spurns +spurwing +squamulate +squarers +squatterproof +squids +squilloid +squinsy +squinty +squiralty +stablemeal +stabs +stageworthy +stagging +stainierite +staysails +stalinism +stalworth +stanging +stanitza +stanzas +stardusts +starets +starlighted +statable +statolatry +stauroscope +steaakhouse +stealability +stealages +steelie +stey +steles +stellerid +stellionate +stenosis +stepparents +steppers +stercorol +stereotapes +sterilities +sternfully +steroidogenic +stetch +stewardesses +sticken +stiffeners +styldia +stylistic +stylopidae +stimulating +stinger +stinting +stirling +stoccado +stochastical +stockhorn +stockpiles +stomatolalia +stonecrop +stonewise +stooges +stooth +stopping +storiette +stoupful +strabotomy +straitlacedly +strangering +strangerwise +strap +stravaiger +stravaigs +strawwalker +streetfighter +strewment +strychnic +strigilous +striolate +striplings +strockle +strokes +strombiform +struggle +stubbliness +studdingsail +studentship +studios +stunkard +stupefying +suantly +subakhmimic +subantiquities +subarration +subbings +subbranchial +subchelate +subclinically +subcommission +subcontrolling +subdecimal +subdefinition +subducted +subduedness +suberect +suberised +subgape +subgenual +subglottal +subidar +subindicate +sublibrarianship +sublimize +sublobular +submediocre +submissionist +submultiplexed +subordinaries +subparties +subphosphate +subpoenaed +subramous +subreport +subsections +subseptuple +subsetting +substances +substantival +substation +subsult +subtotal +subtractors +subtread +subumbonal +subversiveness +subverticilated +succeeders +succussed +suckler +sudiform +sufferant +suffragial +sugarless +suicidist +suing +sulfamidic +sulfatase +sulliage +sulphazide +sulphocarbonate +sulphogermanate +sulphonation +sulphurated +sulphurea +summational +summerly +sundrymen +sundriness +suns +superabound +superadd +superaffiuence +superambitiously +supercanine +superconfusion +superdiplomacy +superedify +supereffectiveness +superelegancies +supererogative +supergallantness +supergravitate +superindependently +superinduced +superlogical +superlogicality +supernaturalistic +superorder +superpersonal +superpositions +superromantic +superscripted +superscripts +superstoical +superthorough +superunfit +supervisive +supervive +supplicates +supportive +supraliminal +supralittoral +supraventricular +surbate +surcharge +surfeiting +surpriser +surrejoin +surrejoinders +susans +susie +susurr +suttapitaka +swagbelly +swainishness +swamped +swathband +sweated +swedenborgianism +sweetbriers +swell +swelldom +swinking +swiss +swith +swordgrass +swotted +szaibelyite +tabis +tabooing +tacheography +tacks +tacticians +tade +taels +taggle +tagraggery +taigas +takeout +tald +talismanical +talkatively +tallero +tallowing +talonid +tamaracks +tamperproof +tanglesome +tanglewrack +tanistic +tanistship +tannish +tapacura +tapiocas +tappertitian +taraf +targe +tariffism +tarquin +tarries +tartrazin +tartufian +tarzanish +tasted +tatterwallop +tautometrical +tautonym +tavastian +tawny +tawnily +tbssaraglot +tealess +teammates +teardrops +technographically +tedding +teeners +teenfuls +teetotumism +telamones +telechirograph +telegraf +teleocephali +teleports +telestial +telewriter +telium +telotrophic +temescal +templates +templetonia +tenantism +tenderable +tendre +tenesmus +tenonitis +tensiometry +tenthredinoidea +tenuis +terahertz +teraphim +terephthalic +tergiferous +terr +terrariia +terremotive +terrifying +terror +tessara +tesseratomy +testata +tetractinellid +tetradynamous +tetrahexahedral +tetrameralian +tetramorphism +tetranychus +tetranitromethane +teuchit +thai +thairm +thalamocele +thamnidium +thankfuller +thaw +theat +theezan +thegnworthy +theogeological +theologism +theophania +theophoric +theoretician +thereinbefore +theres +theriomorph +thermodynam +thermopower +theromora +thetic +thewed +thievishness +thigging +thymelic +thinkingly +thiocarbonate +thyreogenous +thyrohyal +thyroidectomy +thirteen +thitherward +thoom +thorning +thoroughstem +thoughtlessness +threne +thrilling +thripple +throbbed +thrombins +thuyopsis +thulir +thumbscrews +thunderingly +tiburtine +ticca +ticketless +tickseeded +tideless +tigroid +tikker +tylaster +tillable +tiltboard +timaliidae +timbreled +tineal +tinerer +tingidae +tinglingly +tinsels +tinselweaver +typhic +typhosis +tippling +tyrannic +tirasse +tireless +tirrit +tissuey +tithonographic +tiza +toadeat +toastmastery +tobies +tody +tofter +toggeries +toggling +tolerablish +tolowa +toman +tomorrower +tonemes +tonguesore +tonicities +tonsillitis +tool +toothless +topiary +topinambou +toprope +topside +tormenta +tormentive +torosities +torrone +tortilla +tost +toted +touchers +touching +touchup +towelling +towie +townland +townsboy +toxosis +tracheopharyngeal +trachyphonous +trachles +tractatule +tractite +traditores +tragedianess +traiteurs +trammelled +trams +trancoidal +tranquillizing +transcalent +transdesert +transfusing +transhumant +transimpression +translatrix +transmittable +transmuter +transperitoneally +transubstantiated +transuranian +trapezophozophora +traplight +trappoid +traumatopnea +treacle +treasuring +tregohm +tremolitic +tremorless +trenched +trestle +triac +triangulid +tribesmanship +trichatrophia +trichinisation +trichlorfon +trichoplax +trichostrongylid +tricycle +tricolette +trieteric +trifoliosis +trigoneutic +trigrammatic +trillionaire +trimellitic +trimer +trimly +trinary +triphenylmethane +tryphosa +triphthongal +triplegia +tripsis +trisceles +triskelion +trisulphone +tritones +triumvirs +trivially +trivoltine +trocks +trollol +tronador +trophedema +trophyless +tropomyosin +trotting +trp +trudgers +truehearted +trumpetry +trunch +trusteeism +tsarinas +tsarship +tuberculisation +tuberously +tubificid +tubuliform +tuckers +tucks +tumbrel +tumuli +tumultuous +tunist +tupek +turbinatoconcave +turbinatoglobose +turfier +turkeys +turnoffs +turpitude +turquoises +turtles +tusky +tutsan +tuxes +twelfthly +twerps +twigwithy +twiningly +twisted +tziganes +udalman +ugliest +ulcus +ulmaceae +ultimum +ultradolichocephalic +ultramodernism +ultranice +ultraroyalism +umbellate +umpiress +unabased +unabolishable +unabsolved +unabusable +unaccumulate +unadaptability +unadored +unafflicting +unairable +unalive +unallowable +unamiability +unanimities +unannotated +unanxiety +unarray +unarraignable +unartistlike +unassessed +unavenued +unawaredly +unbadged +unbadgered +unbaited +unbehoving +unbeloved +unbigamously +unbinds +unblameworthy +unbobbed +unbolstered +unbowled +unbreathableness +unbustling +unbuttons +uncallous +uncapitulated +uncaricatured +uncart +uncatechised +unchain +unchemically +unciform +uncircularised +uncitizenly +uncivilish +unclench +unclouded +uncolt +uncomical +uncommemorated +uncommissioned +uncompressible +unconditionedness +uncondoling +unconformably +unconjectured +unconstrainable +uncontemptibly +uncontortedly +unconventionalism +unconvertedly +uncored +uncourting +uncredit +unctuarium +uncudgelled +uncured +undawning +undecidedness +undeclaimed +undefectively +undelimited +undependable +undepicted +underachieve +underarch +underballast +undercooled +underdosed +underexercising +underfired +underfrock +undergrads +underinsured +underlings +undermusic +underoxidize +underrecompensing +underscored +underscoring +undersequence +understimuli +understocking +undersuggestion +undertook +undesirability +undesirable +undestructible +undialled +undiminishable +undiplomatically +undiscoverable +undiseased +undismissed +undivulgeable +undoneness +undramatized +undrowned +undrunk +undulancy +uneconomic +uneffectless +unemancipated +unended +unenigmatically +unenslaved +unenticeable +unequitable +unerosive +unevangelized +unexcerpted +unexcoriated +unexpedient +unextinguishably +unextortable +unfaced +unfastenable +unfastidiously +unfeeding +unfelled +unfine +unfirmness +unfixed +unfloored +unfluked +unforgetful +unframed +unfrequentative +unfricative +unfrugality +unfurl +ungainsaid +ungeographic +ungiveable +unglued +unglutinosity +ungoggled +ungreyed +ungroomed +unhafted +unhalter +unhappen +unheededly +unhermitic +unhesitating +unhypnotised +unhypothetical +unhurriedness +unhustling +uniambically +unidealised +uniformised +unigenesis +unillusory +unimpregnable +unimproving +unincarnate +unindurated +uninfusing +uninquisitively +unintegral +uninterred +uninterrogated +uninviting +uninwreathed +unions +unity +universitarian +unjoin +unjostled +unjuridical +unkindhearted +unkinger +unlaving +unleared +unliquidation +unlisty +unlit +unlogically +unluminiferous +unmagnanimously +unmajestic +unmaligned +unmassed +unmemorized +unmetamorphosed +unmete +unmewing +unmiasmatical +unmigrative +unmirthful +unmoralistic +unmothered +unmutative +unn +unnauseated +unneutralizing +unnewness +unnuzzled +unobtrusive +unoffensively +unopened +unoriginalness +unoverleaped +unoverpowered +unparceled +unparrel +unpatterned +unpearled +unpenetrable +unpenuriously +unpersonableness +unperturbably +unpitched +unpitiedness +unpleasingness +unplumbed +unpolishedness +unpoliteness +unpowerfulness +unpresaged +unpresupposed +unpretendingness +unproducible +unprofessionalness +unprophetic +unproscriptive +unprotrusive +unpunctured +unputridity +unquelled +unreachably +unreasoned +unrebuttableness +unrecognitory +unreconciled +unrecurring +unredeemableness +unremarkable +unremunerated +unreparted +unreprovably +unrequitedness +unreserved +unresultive +unrevealingly +unreverberating +unrightly +unroved +unruinous +unrupturable +unsabotaged +unsalty +unsanctifiedly +unsash +unsavage +unscribed +unsectarianism +unseemlily +unseignioral +unsenescent +unsepulchrally +unserviceably +unshamableness +unshoveled +unshrinkability +unsymptomatically +unsimulated +unsinisterness +unsinking +unslammed +unsmoothly +unsoldering +unsomnolently +unsoreness +unspatial +unsphering +unspitefully +unsquashable +unstably +unstern +unstigmatised +unstinting +unstrengthened +unsturdy +unsufferably +unsuperciliously +unsuperficial +unsupped +unsurpassed +unsurprised +untaintedness +unteachable +untellably +untenableness +unterrifiable +unthronged +untine +untittering +untoiling +untraceable +untrappable +untrespassed +untriteness +untuneful +unulcerous +ununiquely +unuxoriousness +unvassal +unvenereal +unvindicable +unvitalness +unvoluptuous +unvoweled +unweariably +unwearily +unweetingly +unwieldiest +unwiped +unwooed +unwriting +upboils +upcard +updrink +upfield +upflee +uphroes +upleaps +uprush +upshut +upsitten +upstates +upsurging +upward +uraemia +uranian +uranidine +ureterorectostomy +urethragraph +urgencies +urinalist +urinocryoscopy +urinogenous +urinose +ursuk +usednt +ustarana +ustilaginoidea +usufructuary +utilidor +utriculoid +uvver +vacantheartedness +vadimonium +vagabondized +vagogram +valediction +valeted +valkyr +valleyful +valorizations +vandalic +vandyked +vanlay +vaporlike +variating +varicolorous +variousness +vascular +vasculous +vatical +vaticanical +vauntmure +veering +vehemency +vehiculary +velloziaceae +velverets +vena +veneficness +venesia +venomsome +ventriloquize +venturer +venusty +verbalist +verdurous +verisimilarly +verjuices +vermiformity +verminicide +versifying +versine +vertebriform +vertugal +vestalship +vestiture +vestlet +veszelyite +vetoist +vialful +victless +victorians +victualless +vidicon +vigesimoquarto +vigilante +vignettists +vilipenditory +vinylate +vinny +vintem +violacean +vipers +vips +viridine +virologies +visaing +visionic +visualize +vitameric +vitaminization +vitry +vitriolically +vivific +vivisective +voces +voetian +voids +volatiles +volitive +voltammeter +volumenometry +voluming +vorticosely +vouchable +vowelled +vulcaniser +vulva +wac +wadi +wadmol +wagework +waggonsmith +wayfarers +waistless +waitress +waivers +walkovers +walla +wanapum +wanderlust +waning +wapperjaw +wardresses +warmed +warmonger +warrambool +warworn +was +washability +watchcase +watchmanship +waterbuck +watercaster +waterskin +wattlework +waxplants +weald +wealthily +wearing +wearingly +webber +wedfee +weigh +weighbridgeman +weisbachite +welfarism +wellcontent +wellring +welterweights +werner +whalebird +whampee +whapukee +whatnots +wheateared +wheencat +whelk +wherefrom +whimper +whinchat +whipstock +whirlbat +whirlwindy +whishes +whisperously +whitepot +whitmonday +whoppers +wicca +wych +wicketkeep +wifedoms +wigger +wiggly +wildfowler +williewaucht +windcheater +windfishes +windles +wyne +winemaker +winevat +winningness +wird +wireman +wirra +wispier +witan +withindoors +witling +witloof +wittingly +wizzens +woldy +wondermonger +wong +wongah +woodbark +woodlark +woodwind +wooliest +wordages +worked +workmistress +workways +wormil +wormils +wouhleche +wrappers +wreathed +wretchlessly +wrig +writheneck +wrocht +wronghearted +xanthocyanopy +xenarthra +xenicidae +xenophontic +xeroderma +xylariaceae +xylotomous +xiphias +zamarra +zamindary +zanze +zapote +zebralike +zeiss +zeks +zygenid +zygopteraceae +zygosperm +zillionths +zymotechny +zingaresca +zirconias +zirconoid +zohak +zoners +zoologically +zoopathy +zoophaga +zoophily +zoosperms +zooxanthellae \ No newline at end of file diff --git a/src/FederationLib/Classes/Database.php b/src/FederationLib/Classes/Database.php new file mode 100644 index 0000000..2d81f98 --- /dev/null +++ b/src/FederationLib/Classes/Database.php @@ -0,0 +1,88 @@ + Configuration::getDatabaseDriver(), + 'host' => Configuration::getDatabaseHost(), + 'port' => Configuration::getDatabasePort(), + 'dbname' => Configuration::getDatabaseName(), + 'user' => Configuration::getDatabaseUsername(), + 'password' => Configuration::getDatabasePassword(), + 'charset' => Configuration::getDatabaseCharset() + ]); + + $connection->getConfiguration()->setMiddlewares([new Middleware(new Psr('com.dbal.doctrine'))]); + $connection->connect(); + } + catch(Exception $e) + { + throw new DatabaseException('Failed to connect to the database: ' . $e->getMessage(), $e); + } + + self::$sql_connection = $connection; + self::$sql_last_connection_time = time(); + } + else + { + /** @noinspection NestedPositiveIfStatementsInspection */ + if(time() - self::$sql_last_connection_time > Configuration::getDatabaseReconnectInterval()) + { + Log::info('net.nosial.federationlib', sprintf('Interval to reconnect to the %s server has been reached, reconnecting...', Configuration::getDatabaseDriver())); + + // Reconnect to the database. + self::$sql_connection->close(); + self::$sql_connection = null; + + return self::getConnection(); + } + } + + return self::$sql_connection; + } + + /** + * Returns the time of the last connection to the database. (null if no connection has been made) + * + * @return int|null + */ + public static function getSqlLastConnectionTime(): ?int + { + return self::$sql_last_connection_time; + } + } \ No newline at end of file diff --git a/src/FederationLib/Classes/Memcached.php b/src/FederationLib/Classes/Memcached.php new file mode 100644 index 0000000..98bac1f --- /dev/null +++ b/src/FederationLib/Classes/Memcached.php @@ -0,0 +1,18 @@ +connect(Configuration::getRedisHost(), Configuration::getRedisPort()); + if(Configuration::getRedisPassword() !== null) + { + $redis->auth(Configuration::getRedisPassword()); + } + $redis->select(Configuration::getRedisDatabase()); + } + catch(Exception $e) + { + throw new RedisException('Failed to connect to the redis server: ' . $e->getMessage(), $e->getCode(), $e); + } + + self::$redis_connection = $redis; + self::$redis_last_connection_time = time(); + } + else + { + if(self::$redis_last_connection_time === null || self::$redis_last_connection_time < (time() - Configuration::getRedisReconnectInterval())) + { + Log::info('net.nosial.federationlib', 'Interval to reconnect to the redis server has been reached, reconnecting...'); + + try + { + self::$redis_connection->close(); + } + catch(Exception $e) + { + // Do nothing + unset($e); + } + + self::$redis_connection = null; + return self::getConnection(); + } + } + + return self::$redis_connection; + } + + /** + * Returns the last time the redis server was connected to. + * + * @return int|null + */ + public static function getRedisLastConnectionTime(): ?int + { + return self::$redis_last_connection_time; + } + } \ No newline at end of file diff --git a/src/FederationLib/Classes/Security.php b/src/FederationLib/Classes/Security.php new file mode 100644 index 0000000..edc68df --- /dev/null +++ b/src/FederationLib/Classes/Security.php @@ -0,0 +1,102 @@ +getMessage(), $e); + } + } + + /** + * Generates a TOTP code based on a secret and timestamp. + * + * @param $secret + * @param $timestamp + * @return string + */ + public static function generateCode($secret, $timestamp=null): string + { + if (!$timestamp) + { + $timestamp = time(); + } + + $hash = hash_hmac(self::$hash_algorithm, pack('N*', 0, $timestamp / self::$time_step + self::$time_offset), Base32::decode($secret), true); + $offset = ord(substr($hash, -1)) & 0x0F; + $value = unpack('N', substr($hash, $offset, 4))[1] & 0x7FFFFFFF; + $modulo = 10 ** self::$digits; + return str_pad($value % $modulo, self::$digits, '0', STR_PAD_LEFT); + } + + /** + * Validates a TOTP code. + * + * @param $secret + * @param $code + * @param $window + * @return bool + */ + public static function validateCode($secret, $code, $window = 1): bool + { + $timestamp = time(); + + for ($i = -$window; $i <= $window; $i++) + { + $generatedCode = self::generateCode($secret, $timestamp + $i * self::$time_step); + if ($code === $generatedCode) + { + return true; + } + + } + return false; + } + } \ No newline at end of file diff --git a/src/FederationLib/Classes/Utilities.php b/src/FederationLib/Classes/Utilities.php new file mode 100644 index 0000000..69ff3f5 --- /dev/null +++ b/src/FederationLib/Classes/Utilities.php @@ -0,0 +1,183 @@ +[a-zA-Z0-9_-]+):(?P[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)); + } + + /** + * Serializes an array into a string. + * + * @param array $data + * @param string $method + * @return string + */ + public static function serialize(array|SerializableObjectInterface $data, string $method): string + { + if($data instanceof SerializableObjectInterface) + { + $data = $data->toArray(); + } + + switch(strtolower($method)) + { + case SerializationMethod::JSON: + return json_encode($data); + + case SerializationMethod::MSGPACK: + return msgpack_pack($data); + + default: + Log::warning('net.nosial.federationlib', sprintf('Unknown serialization method: %s, defaulting to msgpack', $method)); + return msgpack_pack($data); + } + } + + /** + * 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. + * The key of the returned array is the index of the number in the original array. + * The value of the returned array is the probability of the number being an anomaly. + * Negative values are anomalies, positive values are not. + * The higher the absolute value, the more likely it is to be an anomaly. + * + * @param array $data An array of numbers to check for anomalies + * @param int $threshold The threshold to use for detecting anomalies + * @param bool $filter True to only return anomalies, false to return all values + * @return array An array of probabilities + */ + public static function detectAnomalies(array $data, int $threshold = 2, bool $filter=true): array + { + $mean = array_sum($data) / count($data); + $squares = array_map(static function($x) use ($mean) { return ($x - $mean) ** 2; }, $data); + $standard_deviation = sqrt(array_sum($squares) / count($data)); + $outliers = []; + foreach ($data as $key => $value) + { + $z_score = ($value - $mean) / $standard_deviation; + $probability = exp(-$z_score ** 2 / 2) / (sqrt(2 * M_PI) * $standard_deviation); + + if($filter) + { + if ($z_score >= $threshold) + { + $outliers[$key] = -$probability; + } + } + else + { + $outliers[$key] = $probability; + } + + } + return $outliers; + } + } \ No newline at end of file diff --git a/src/FederationLib/Classes/Validate.php b/src/FederationLib/Classes/Validate.php new file mode 100644 index 0000000..9c13184 --- /dev/null +++ b/src/FederationLib/Classes/Validate.php @@ -0,0 +1,60 @@ +getCode(), $previous); + } + else + { + parent::__construct($message); + } + } + } \ No newline at end of file diff --git a/src/FederationLib/Exceptions/RedisException.php b/src/FederationLib/Exceptions/RedisException.php new file mode 100644 index 0000000..da41813 --- /dev/null +++ b/src/FederationLib/Exceptions/RedisException.php @@ -0,0 +1,20 @@ +client_manager = new ClientManager($this); + $this->event_log_manager = new EventLogManager($this); + } + + /** + * Returns the Client manager instance + * + * @return ClientManager + */ + public function getClientManager(): ClientManager + { + return $this->client_manager; + } + + /** + * @return EventLogManager + */ + public function getEventLogManager(): EventLogManager + { + return $this->event_log_manager; + } } \ No newline at end of file diff --git a/src/FederationLib/Interfaces/CacheDriverInterface.php b/src/FederationLib/Interfaces/CacheDriverInterface.php new file mode 100644 index 0000000..50e5503 --- /dev/null +++ b/src/FederationLib/Interfaces/CacheDriverInterface.php @@ -0,0 +1,47 @@ +federationLib = $federationLib; + } + + /** + * Registers a client into the database, returns the UUID that was generated for the client. + * + * @param Client $client + * @return string + * @throws DatabaseException + */ + public function registerClient(Client $client): string + { + $qb = Database::getConnection()->createQueryBuilder(); + $qb->insert(DatabaseTables::CLIENTS); + $uuid = Uuid::v4()->toRfc4122(); + + foreach($client->toArray() as $key => $value) + { + switch($key) + { + case 'id': + $qb->setValue($key, ':' . $key); + $qb->setParameter($key, $uuid); + break; + + case 'name': + if($value === null || strlen($value) === 0 || !preg_match('/^[a-zA-Z0-9_\-]+$/', $value )) + { + $value = Utilities::generateName(4); + Log::debug('net.nosial.federationlib', sprintf('generated name for client: %s', $value)); + } + + $qb->setValue($key, ':' . $key); + $qb->setParameter($key, substr($value, 0, 64)); + break; + + case 'description': + if($value !== null) + { + $qb->setValue($key, ':' . $key); + $qb->setParameter($key, substr($value, 0, 255)); + } + break; + + case 'enabled': + $qb->setValue($key, ':' . $key); + $qb->setParameter($key, $value ? 1 : 0); + break; + + case 'seen_timestamp': + case 'updated_timestamp': + case 'created_timestamp': + $qb->setValue($key, ':' . $key); + $qb->setParameter($key, time()); + break; + + default: + $qb->setValue($key, ':' . $key); + $qb->setParameter($key, $value); + break; + } + } + + try + { + $qb->executeStatement(); + } + catch(Exception $e) + { + throw new DatabaseException('Failed to register client: ' . $e->getMessage(), $e); + } + + $this->federationLib->getEventLogManager()->logEvent( + EventCode::CLIENT_CREATED, EventPriority::LOW, null, + sprintf('Registered client with UUID %s', $uuid) + ); + + Log::info('net.nosial.federationlib', sprintf('Registered client with UUID %s', $uuid)); + return $uuid; + } + + /** + * Returns an existing client from the database. + * + * @param string|Client $uuid + * @return Client + * @throws ClientNotFoundException + * @throws DatabaseException + */ + public function getClient(string|Client $uuid): Client + { + if($uuid instanceof Client) + { + $uuid = $uuid->getUuid(); + } + + if(Configuration::isRedisCacheClientObjectsEnabled()) + { + try + { + if(Redis::getConnection()?->exists(sprintf('Client<%s>', $uuid))) + { + return Client::fromArray(Redis::getConnection()?->hGetAll(sprintf('Client<%s>', $uuid))); + } + } + catch(Exception $e) + { + Log::warning('net.nosial.federationlib', sprintf('Failed to get Client from redis: %s', $e->getMessage())); + } + } + + $qb = Database::getConnection()->createQueryBuilder(); + $qb->select('*'); + $qb->from(DatabaseTables::CLIENTS); + $qb->where('uuid = :uuid'); + $qb->setParameter('uuid', $uuid); + + try + { + $result = $qb->executeQuery(); + + if($result->rowCount() === 0) + { + throw new ClientNotFoundException($uuid); + } + + $client = Client::fromArray($result->fetchAssociative()); + } + catch(ClientNotFoundException $e) + { + throw $e; + } + catch(Exception $e) + { + throw new DatabaseException('Failed to get Client: ' . $e->getMessage(), $e); + } + + if(Configuration::isRedisCacheClientObjectsEnabled()) + { + try + { + Redis::getConnection()?->hMSet((string)$client, $client->toArray()); + Redis::getConnection()?->expire((string)$client, Configuration::getRedisCacheClientObjectsTTL()); + } + catch(Exception $e) + { + Log::warning('net.nosial.federationlib', sprintf('Failed to cache Client in redis: %s', $e->getMessage())); + } + } + + return $client; + } + + /** + * Updates a client record in the database, if the client does not exist it will be created. + * This function is cache aware, if the client is cached it will only update the changed values. + * + * @param Client $client + * @return void + * @throws DatabaseException + */ + public function updateClient(Client $client): void + { + $cached_client = null; + + if(Configuration::isRedisCacheClientObjectsEnabled()) + { + try + { + if(Redis::getConnection()?->exists(sprintf('Client<%s>', $client->getUuid()))) + { + $cached_client = Client::fromArray(Redis::getConnection()?->hGetAll(sprintf('Client<%s>', $client->getUuid()))); + } + } + catch(Exception $e) + { + Log::warning('net.nosial.federationlib', sprintf('Failed to get Client from redis: %s', $e->getMessage())); + } + } + + $qb = Database::getConnection()->createQueryBuilder(); + $qb->update(DatabaseTables::CLIENTS); + $qb->set('updated_timestamp', ':updated_timestamp'); + $qb->setParameter('updated_timestamp', time()); + $qb->where('uuid = :uuid'); + $qb->setParameter('uuid', $client->getUuid()); + + if($cached_client instanceof Client) + { + $data = array_diff($client->toArray(), $cached_client->toArray()); + } + else + { + $data = $client->toArray(); + } + + foreach($data as $key => $value) + { + switch($key) + { + case 'uuid': + case 'created_timestamp': + case 'updated_timestamp': + case 'seen_timestamp': + break; + + case 'name': + if($value === null || strlen($value) === 0 || !preg_match('/^[a-zA-Z0-9_\-]+$/', $value )) + { + break; + } + + $qb->set($key, ':' . $key); + $qb->setParameter($key, substr($value, 0, 64)); + break; + + case 'description': + if($value !== null) + { + $qb->set($key, ':' . $key); + $qb->setParameter($key, substr($value, 0, 255)); + } + break; + + case 'enabled': + $qb->set($key, ':' . $key); + $qb->setParameter($key, $value ? 1 : 0); + break; + + default: + $qb->set($key, ':' . $key); + $qb->setParameter($key, $value); + break; + } + } + + try + { + $qb->executeStatement(); + } + catch(Exception $e) + { + throw new DatabaseException('Failed to update client: ' . $e->getMessage(), $e); + } + + if(Configuration::isRedisCacheClientObjectsEnabled()) + { + // Update the differences in the cache + if($cached_client instanceof Client) + { + try + { + Redis::getConnection()?->hMSet((string)$client, array_diff($client->toArray(), $cached_client->toArray())); + Redis::getConnection()?->expire((string)$client, Configuration::getRedisCacheClientObjectsTTL()); + } + catch(Exception $e) + { + Log::warning('net.nosial.federationlib', sprintf('Failed to cache client in redis: %s', $e->getMessage())); + } + } + else + { + try + { + Redis::getConnection()?->hMSet((string)$client, $client->toArray()); + Redis::getConnection()?->expire((string)$client, $client->getUuid(), Configuration::getRedisCacheClientObjectsTTL()); + } + catch(Exception $e) + { + Log::warning('net.nosial.federationlib', sprintf('Failed to cache Client in redis: %s', $e->getMessage())); + } + } + } + } + + /** + * Updates a client's last seen timestamp. + * + * @param string|Client $uuid + * @return void + * @throws DatabaseException + * @noinspection PhpUnused + */ + public function updateLastSeen(string|Client $uuid): void + { + if($uuid instanceof Client) + { + $uuid = $uuid->getUuid(); + } + + $timestamp = time(); + + $qb = Database::getConnection()->createQueryBuilder(); + $qb->update(DatabaseTables::CLIENTS); + $qb->set('seen_timestamp', ':seen_timestamp'); + $qb->setParameter('seen_timestamp', $timestamp); + $qb->where('uuid = :uuid'); + $qb->setParameter('uuid', $uuid); + + try + { + $qb->executeStatement(); + } + catch(Exception $e) + { + throw new DatabaseException('Failed to update last seen timestamp: ' . $e->getMessage(), $e); + } + + // Update the 'seen_timestamp' only in the hash table in redis + if(Configuration::isRedisCacheClientObjectsEnabled()) + { + try + { + Redis::getConnection()?->hSet(sprintf('Client<%s>', $uuid), 'seen_timestamp', $timestamp); + } + catch(Exception $e) + { + Log::warning('net.nosial.federationlib', sprintf('Failed to update last seen timestamp in redis: %s', $e->getMessage())); + } + } + } + + /** + * Returns an array of clients based on the filter, order, and page. + * + * @param int $page + * @param string $filter + * @param string $order + * @param int $max_items + * @return array + * @throws DatabaseException + */ + public function listClients(int $page, string $filter=ListClientsFilter::CREATED_TIMESTAMP, string $order=FilterOrder::ASCENDING, int $max_items=100): array + { + $qb = Database::getConnection()->createQueryBuilder(); + $qb->select( + 'uuid', 'enabled', 'name', 'description', 'secret_totp', 'query_permission', 'update_permission', + 'flags', 'created_timestamp', 'updated_timestamp', 'seen_timestamp' + ); + $qb->from(DatabaseTables::CLIENTS); + $qb->setFirstResult(($page - 1) * $max_items); + $qb->setMaxResults($max_items); + + if($order !== FilterOrder::ASCENDING && $order !== FilterOrder::DESCENDING) + { + throw new DatabaseException('Invalid order: ' . $order); + } + + switch($filter) + { + case ListClientsFilter::CREATED_TIMESTAMP: + $qb->orderBy('created_timestamp', strtoupper($order)); + break; + + case ListClientsFilter::UPDATED_TIMESTAMP: + $qb->orderBy('updated_timestamp', strtoupper($order)); + break; + + case ListClientsFilter::SEEN_TIMESTAMP: + $qb->orderBy('seen_timestamp', strtoupper($order)); + break; + + case ListClientsFilter::ENABLED: + $qb->orderBy('enabled', strtoupper($order)); + break; + + default: + throw new DatabaseException('Invalid filter: ' . $filter); + } + + try + { + $result = $qb->executeQuery(); + $clients = []; + + while($row = $result->fetchAssociative()) + { + $clients[] = Client::fromArray($row); + } + } + catch(Exception $e) + { + throw new DatabaseException('Failed to list clients: ' . $e->getMessage(), $e->getCode(), $e); + } + + unset($client); + return $clients; + } + + /** + * Returns the total number of clients. + * + * @return int + * @throws DatabaseException + */ + public function getTotalClients(): int + { + $qb = Database::getConnection()->createQueryBuilder(); + $qb->select('COUNT(uuid)'); + $qb->from(DatabaseTables::CLIENTS); + + try + { + $result = $qb->executeQuery(); + $row = $result->fetchAssociative(); + } + catch(Exception $e) + { + throw new DatabaseException('Failed to get total clients: ' . $e->getMessage(), $e->getCode(), $e); + } + + return (int)$row['COUNT(uuid)']; + } + + /** + * Returns teh total number of pages. + * + * @param int $max_items + * @return int + * @throws DatabaseException + */ + public function getTotalPages(int $max_items=100): int + { + return (int)ceil($this->getTotalClients() / $max_items); + } + + /** + * Deletes an existing client from the database. + * + * @param string|Client $uuid + * @return void + * @throws DatabaseException + */ + public function deleteClient(string|Client $uuid): void + { + if($uuid instanceof Client) + { + $uuid = $uuid->getUuid(); + } + + $qb = Database::getConnection()->createQueryBuilder(); + $qb->delete(DatabaseTables::CLIENTS); + $qb->where('uuid = :uuid'); + $qb->setParameter('uuid', $uuid); + + try + { + $qb->executeStatement(); + } + catch(Exception $e) + { + throw new DatabaseException('Failed to delete client: ' . $e->getMessage(), $e->getCode(), $e); + } + + // Invalidate the cache + if(Configuration::isRedisCacheClientObjectsEnabled()) + { + try + { + Redis::getConnection()?->del(sprintf('Client<%s>', $uuid)); + } + catch(Exception $e) + { + Log::warning('net.nosial.federationlib', sprintf('Failed to invalidate client cache in redis: %s', $e->getMessage())); + } + } + } + } \ No newline at end of file diff --git a/src/FederationLib/Managers/PeerManager.php b/src/FederationLib/Managers/PeerManager.php new file mode 100644 index 0000000..29e906d --- /dev/null +++ b/src/FederationLib/Managers/PeerManager.php @@ -0,0 +1,172 @@ +federationLib = $federationLib; + } + + /** + * Returns the Peer Type of the federated address, returns "unknown" if the + * type is not supported by the server + * + * @param string $type + * @return string + */ + private function getPeerType(string $type): string + { + if(in_array(strtolower($type), InternetPeerType::ALL)) + return PeerType::INTERNET; + + if(in_array(strtolower($type), UserPeerType::ALL)) + return PeerType::USER; + + return PeerType::UNKNOWN; + } + + /** + * Parses a raw federated address and returns a ParsedFederatedAddress object + * + * @param string $federated_address + * @return ParsedFederatedAddress + * @throws UnsupportedPeerType + */ + private function parseAddress(string $federated_address): ParsedFederatedAddress + { + $parsed_address = new ParsedFederatedAddress($federated_address); + + if($this->getPeerType($parsed_address->getPeerType()) === PeerType::UNKNOWN) + { + throw new UnsupportedPeerType($parsed_address->getPeerType()); + } + + return $parsed_address; + } + + /** + * Registers a new peer into the database + * + * @param string|Client $client_uuid + * @param string $federated_address + * @return void + * @throws DatabaseException + * @throws UnsupportedPeerType + */ + public function registerPeer(string|Client $client_uuid, string $federated_address): void + { + // If the client_uuid is a Client object, get the UUID from it + if ($client_uuid instanceof Client) + { + $client_uuid = $client_uuid->getUuid(); + } + + // Check if the peer type is supported by the server + $parsed_address = $this->parseAddress($federated_address); + + try + { + // Generate a query to insert the peer into the database + $query_builder = Database::getConnection()->createQueryBuilder(); + $query_builder->insert(DatabaseTables::PEERS); + $timestamp = time(); + + $query_builder->values([ + 'federated_address' => $query_builder->createNamedParameter($parsed_address->getAddress()), + 'client_first_seen' => $query_builder->createNamedParameter($client_uuid), + 'client_last_seen' => $query_builder->createNamedParameter($client_uuid), + 'active_restriction' => $query_builder->createNamedParameter(null, ParameterType::NULL), + 'discovered_timestamp' => $query_builder->createNamedParameter($timestamp, ParameterType::INTEGER), + 'seen_timestamp' => $query_builder->createNamedParameter($timestamp, ParameterType::INTEGER), + ]); + + $query_builder->executeStatement(); + } + catch(Exception $e) + { + throw new DatabaseException(sprintf('Failed to register peer %s: %s', $parsed_address->getAddress(), $e->getMessage()), $e); + } + + Log::info(Misc::FEDERATIONLIB, sprintf('Registered new peer: %s', $parsed_address->getAddress())); + } + + public function cachePeerObject(Peer $peer): void + { + if(!Configuration::isRedisEnabled() && !Configuration::isPeerObjectsCached()) + { + return; + } + + + } + + /** + * Fetches a peer from the database by its federated address + * + * @param string $federated_address + * @return Peer + * @throws DatabaseException + * @throws PeerNotFoundException + * @throws UnsupportedPeerType + */ + public function getPeer(string $federated_address): Peer + { + // Check if the peer type is supported by the server + $parsed_address = $this->parseAddress($federated_address); + + try + { + $query_builder = Database::getConnection()->createQueryBuilder(); + $query_builder->select('*'); + $query_builder->from(DatabaseTables::PEERS); + $query_builder->where('federated_address = :federated_address'); + $query_builder->setParameter('federated_address', $parsed_address->getAddress()); + $query_builder->setMaxResults(1); + + $result = $query_builder->executeQuery(); + + if($result->rowCount() === 0) + { + throw new PeerNotFoundException($parsed_address->getAddress()); + } + + return Peer::fromArray($result->fetchAssociative()); + } + catch(PeerNotFoundException $e) + { + throw $e; + } + catch(Exception $e) + { + throw new DatabaseException(sprintf('Failed to get peer %s: %s', $parsed_address->getAddress(), $e->getMessage()), $e); + } + } + } \ No newline at end of file diff --git a/src/FederationLib/Objects/Client.php b/src/FederationLib/Objects/Client.php new file mode 100755 index 0000000..2ec4897 --- /dev/null +++ b/src/FederationLib/Objects/Client.php @@ -0,0 +1,380 @@ +enabled = true; + $this->flags = []; + $this->query_permission = 1; + $this->update_permission = 0; + } + + /** + * Returns the client's unique ID. + * + * @return string + */ + public function getUuid(): string + { + return $this->uuid; + } + + /** + * @return bool + */ + public function isEnabled(): bool + { + return $this->enabled; + } + + /** + * @param bool $enabled + */ + public function setEnabled(bool $enabled): void + { + $this->enabled = $enabled; + } + + /** + * @return string|null + */ + public function getName(): string + { + return $this->name; + } + + /** + * @param string|null $name + */ + public function setName(?string $name): void + { + $this->name = $name; + } + + /** + * @return string|null + */ + public function getDescription(): ?string + { + return $this->description; + } + + /** + * @param string|null $description + */ + public function setDescription(?string $description): void + { + $this->description = $description; + } + + /** + * Enables authentication for the client, returns the secret. + * + * @return string + */ + public function enableAuthentication(): string + { + $this->secret_totp = Security::generateSecret(); + return $this->secret_totp; + } + + /** + * Disables authentication for the client, wipes the secret. + * + * @return void + */ + public function disableAuthentication(): void + { + $this->secret_totp = null; + } + + /** + * Validates the given authentication code against the client's secret. + * + * @param string|null $authentication_code + * @param int|null $timestamp + * @return bool + */ + public function validateAuthentication(?string $authentication_code=null, ?int $timestamp=null): bool + { + if($this->secret_totp === null) + { + // Always authenticate if authentication is disabled. + return true; + } + + if($authentication_code === null) + { + // Authentication code not provided but required. + return false; + } + + // Authentication Code: sha1(client_id + totp_code) + return hash('sha1', $this->uuid . Security::generateCode($this->secret_totp, $timestamp)) === $authentication_code; + } + + /** + * Returns the client's query permission level. + * + * @return int + */ + public function getQueryPermission(): int + { + return $this->query_permission; + } + + /** + * Sets the client's query permission level. + * + * @param int $query_permission + */ + public function setQueryPermission(int $query_permission): void + { + $this->query_permission = $query_permission; + } + + /** + * Returns the client's update permission level. + * + * @return int + */ + public function getUpdatePermission(): int + { + return $this->update_permission; + } + + /** + * Sets the client's update permission. + * + * @param int $update_permission + */ + public function setUpdatePermission(int $update_permission): void + { + $this->update_permission = $update_permission; + } + + /** + * Returns the client's flags. + * + * @return string[] + */ + public function getFlags(): array + { + return $this->flags; + } + + /** + * Sets an array of flags for the client. + * This function overrides any existing flags. + * + * @param string[] $flags + */ + public function setFlags(array $flags): void + { + $this->flags = $flags; + } + + /** + * Appends a flag to the client's flags. + * + * @param string $flag + * @return void + */ + public function appendFlag(string $flag): void + { + if(!in_array($flag, $this->flags)) + { + $this->flags[] = $flag; + } + } + + /** + * Removes a flag from the client's flags. + * + * @param string $flag + * @return void + */ + public function removeFlag(string $flag): void + { + $this->flags = array_diff($this->flags, [$flag]); + } + + /** + * Returns True if the client has the given flag. + * + * @param string $flag + * @return bool + */ + public function hasFlag(string $flag): bool + { + return in_array($flag, $this->flags); + } + + /** + * Returns the Unix Timestamp for when the client was last created on the server + * + * @return int + */ + public function getCreatedTimestamp(): int + { + return $this->created_timestamp; + } + + /** + * Returns the Unix Timestamp for when the client was last updated on the server. + * + * @return int + */ + public function getUpdatedTimestamp(): int + { + return $this->updated_timestamp; + } + + /** + * Returns the Unix Timestamp for when the client was last seen by the server. + * + * @return int + */ + public function getSeenTimestamp(): int + { + return $this->seen_timestamp; + } + + /** + * Returns an array representation of the object + * + * @return array + */ + public function toArray(): array + { + $flags = null; + + if($this->flags !== null && count($this->flags) > 0) + { + $flags = implode(',', $this->flags); + } + + return [ + 'uuid' => $this->uuid, + 'enabled' => $this->enabled, + 'name' => $this->name, + 'description' => $this->description, + 'secret_totp' => $this->secret_totp, + 'query_permission' => $this->query_permission, + 'update_permission' => $this->update_permission, + 'flags' => $flags, + 'created_timestamp' => $this->created_timestamp, + 'updated_timestamp' => $this->updated_timestamp, + 'seen_timestamp' => $this->seen_timestamp, + ]; + } + + /** + * Constructs object from an array representation. + * + * @param array $array + * @return Client + */ + public static function fromArray(array $array): Client + { + $client = new self(); + + $client->uuid = $array['uuid'] ?? null; + $client->enabled = Utilities::parseBoolean($array['enabled']); + $client->name = $array['name'] ?? null; + $client->description = $array['description'] ?? null; + $client->secret_totp = $array['secret_totp'] ?? null; + $client->query_permission = $array['query_permission'] ?? 0; + $client->update_permission = $array['update_permission'] ?? 0; + if(isset($array['flags'])) + { + $client->flags = explode(',', $array['flags']); + } + else + { + $client->flags = []; + } + $client->created_timestamp = $array['created_timestamp'] ?? 0; + $client->updated_timestamp = $array['updated_timestamp'] ?? 0; + $client->seen_timestamp = $array['seen_timestamp'] ?? 0; + + return $client; + } + + /** + * Returns the object identifier. + * + * @return string + */ + public function __toString(): string + { + return sprintf('Client<%s>', $this->uuid ?? spl_object_hash($this)); + } + } \ No newline at end of file diff --git a/src/FederationLib/Objects/ParsedFederatedAddress.php b/src/FederationLib/Objects/ParsedFederatedAddress.php new file mode 100644 index 0000000..0388d96 --- /dev/null +++ b/src/FederationLib/Objects/ParsedFederatedAddress.php @@ -0,0 +1,78 @@ +[a-z0-9]+)\.(?[a-z0-9]+):(?.+)/", $federated_address, $matches); + + $this->source = $matches['source']; + $this->peer_type = $matches['type']; + $this->unique_identifier = $matches['id']; + } + + /** + * Returns the platform source of the peer + * + * @return string + */ + public function getSource(): string + { + return $this->source; + } + + /** + * Returns the peer type of the platform + * + * @return string + */ + public function getPeerType(): string + { + return $this->peer_type; + } + + /** + * Returns the Unique Identifier of the peer + * + * @return string + */ + public function getUniqueIdentifier(): string + { + return $this->unique_identifier; + } + + /** + * Returns the Standard Federated Address of the peer + * + * @return string + */ + public function getAddress(): string + { + return sprintf('%s.%s:%s', $this->source, $this->peer_type, $this->unique_identifier); + } + + } \ No newline at end of file diff --git a/src/FederationLib/Objects/Peer.php b/src/FederationLib/Objects/Peer.php new file mode 100644 index 0000000..4bfdb44 --- /dev/null +++ b/src/FederationLib/Objects/Peer.php @@ -0,0 +1,138 @@ +federated_address; + } + + /** + * Returns the client UUID that first saw the peer + * + * @return string + */ + public function getClientFirstSeen(): string + { + return $this->client_first_seen; + } + + /** + * Returns the client UUID that last saw the peer + * + * @return string + */ + public function getClientLastSeen(): string + { + return $this->client_last_seen; + } + + /** + * Optional. Returns the active restriction ID for the peer + * This is only available if the peer is currently restricted + * + * @return string|null + */ + public function getActiveRestriction(): ?string + { + return $this->active_restriction; + } + + /** + * Returns the Unix Timestamp for when the peer was first discovered by a client + * + * @return int + */ + public function getDiscoveredTimestamp(): int + { + return $this->discovered_timestamp; + } + + /** + * Returns the Unix Timestamp for when the peer was last seen by a client + * + * @return int + */ + public function getSeenTimestamp(): int + { + return $this->seen_timestamp; + } + + /** + * Returns an array representation of the object + * + * @return array + */ + public function toArray(): array + { + return [ + 'federated_address' => $this->federated_address, + 'client_first_seen' => $this->client_first_seen, + 'client_last_seen' => $this->client_last_seen, + 'active_restriction' => $this->active_restriction, + 'discovered_timestamp' => $this->discovered_timestamp, + 'seen_timestamp' => $this->seen_timestamp, + ]; + } + + /** + * Constructs object from an array representation + * + * @param array $array + * @return Peer + */ + public static function fromArray(array $array): Peer + { + $object = new self(); + + $object->federated_address = $array['federated_address'] ?? null; + $object->client_first_seen = $array['client_first_seen'] ?? null; + $object->client_last_seen = $array['client_last_seen'] ?? null; + $object->active_restriction = $array['active_restriction'] ?? null; + $object->discovered_timestamp = $array['discovered_timestamp'] ?? null; + $object->seen_timestamp = $array['seen_timestamp'] ?? null; + + return $object; + } + } \ No newline at end of file diff --git a/src/FederationLib/Objects/QueryDocument.php b/src/FederationLib/Objects/QueryDocument.php new file mode 100644 index 0000000..a0e3092 --- /dev/null +++ b/src/FederationLib/Objects/QueryDocument.php @@ -0,0 +1,17 @@ +setDescription('This is a test client generated by a test script.'); + + $uuid = $federationlib->getClientManager()->registerClient($client); + + print(sprintf('Client UUID: %s', $uuid) . PHP_EOL); + + $client = $federationlib->getClientManager()->getClient($uuid); + $client->setName('Test Client'); + $client->setDescription('This is a test client generated by a test script. It has been updated.'); + + $federationlib->getClientManager()->updateClient($client); + + $client = $federationlib->getClientManager()->getClient($uuid); + print(sprintf('Client name: %s', $client->getName()) . PHP_EOL); + print(sprintf('Client description: %s', $client->getDescription()) . PHP_EOL); + + exit(0); \ No newline at end of file diff --git a/tests/client_manager/create_client.php b/tests/client_manager/create_client.php new file mode 100644 index 0000000..e92e19a --- /dev/null +++ b/tests/client_manager/create_client.php @@ -0,0 +1,14 @@ +setDescription('This is a test client generated by a test script.'); + + $uuid = $federationlib->getClientManager()->registerClient($client); + + print(sprintf('Client UUID: %s', $uuid) . PHP_EOL); + exit(0); \ No newline at end of file diff --git a/tests/client_manager/create_secured_client.php b/tests/client_manager/create_secured_client.php new file mode 100644 index 0000000..effc672 --- /dev/null +++ b/tests/client_manager/create_secured_client.php @@ -0,0 +1,18 @@ +setDescription('This is a test client generated by a test script.'); + $client->enableAuthentication(); + + $uuid = $federationlib->getClientManager()->registerClient($client); + + print(sprintf('Client UUID: %s', $uuid) . PHP_EOL); + } diff --git a/tests/client_manager/list_clients.php b/tests/client_manager/list_clients.php new file mode 100644 index 0000000..f7ee2ad --- /dev/null +++ b/tests/client_manager/list_clients.php @@ -0,0 +1,8 @@ +getClientManager()->listClients(1)); \ No newline at end of file diff --git a/tests/totp_test.php b/tests/totp_test.php new file mode 100644 index 0000000..1fefb4a --- /dev/null +++ b/tests/totp_test.php @@ -0,0 +1,15 @@ +