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 0000000..75be0e7 Binary files /dev/null and b/assets/federated_email_address.png differ diff --git a/assets/federated_ipv4.png b/assets/federated_ipv4.png new file mode 100644 index 0000000..dbe5240 Binary files /dev/null and b/assets/federated_ipv4.png differ diff --git a/assets/federated_telegram_user.png b/assets/federated_telegram_user.png new file mode 100644 index 0000000..a5fcd02 Binary files /dev/null and b/assets/federated_telegram_user.png differ diff --git a/assets/recon_example.png b/assets/recon_example.png new file mode 100644 index 0000000..3811d51 Binary files /dev/null and b/assets/recon_example.png differ diff --git a/database/anomaly_tracking.sql b/database/anomaly_tracking.sql new file mode 100644 index 0000000..3c1caf0 --- /dev/null +++ b/database/anomaly_tracking.sql @@ -0,0 +1,23 @@ +create table anomaly_tracking +( + tracking_id varchar(36) default uuid() not null comment 'The UUID V4 Tracking ID for the anomaly' + primary key, + target_peer varchar(256) not null comment 'The target peer to which this anomaly is being tracked to', + event_type varchar(32) not null comment 'The event type that''s being tracked for the target peer', + usage_data mediumblob null comment 'The usage data of the event from the past 48 hours', + last_updated bigint default unix_timestamp() not null comment 'The Unix Timestamp for when this record was last updated', + created_timestamp bigint default unix_timestamp() not null comment 'The Unix Timestamp for when this record was first created', + constraint anomaly_tracking_event_type_target_peer_uindex + unique (event_type, target_peer), + constraint anomaly_tracking_target_peer_uindex + unique (target_peer), + constraint anomaly_tracking_tracking_id_uindex + unique (tracking_id), + constraint anomaly_tracking_peers_federated_address_fk + foreign key (target_peer) references peers (federated_address) +) + comment 'This table is used for tracking anomalies for events'; + +create index anomaly_tracking_event_type_index + on anomaly_tracking (event_type); + diff --git a/database/associations.sql b/database/associations.sql new file mode 100644 index 0000000..c32729f --- /dev/null +++ b/database/associations.sql @@ -0,0 +1,29 @@ +create table associations +( + child_peer varchar(256) not null comment 'The child peer', + parent_peer varchar(256) not null comment 'The parent peer', + type varchar(32) not null comment 'The association type between the parent peer to the child peer', + client_uuid varchar(36) not null comment 'The client UUID that made the association between the two peers', + timestamp bigint default unix_timestamp() not null comment 'The Unix Timestamp for when this assoication was made', + constraint associations_child_peer_parent_peer_uindex + unique (child_peer, parent_peer), + constraint associations_child_peer_type_parent_peer_uindex + unique (child_peer, type, parent_peer), + constraint associations_clients_uuid_fk + foreign key (client_uuid) references clients (uuid), + constraint associations_discovered_peers_federated_address_fk + foreign key (child_peer) references peers (federated_address), + constraint associations_discovered_peers_federated_address_fk2 + foreign key (parent_peer) references peers (federated_address) +) + comment 'A table used for housing associations detected between peers'; + +create index associations_child_peer_index + on associations (child_peer); + +create index associations_client_uuid_index + on associations (client_uuid); + +create index associations_parent_peer_index + on associations (parent_peer); + diff --git a/database/clients.sql b/database/clients.sql new file mode 100644 index 0000000..77802f5 --- /dev/null +++ b/database/clients.sql @@ -0,0 +1,34 @@ +create table clients +( + uuid varchar(36) default uuid() not null comment 'The UUID v4 ID for the client, null for anonymous users.', + enabled tinyint(1) default 1 not null comment 'Indicates if this client is enabled or not', + name text collate utf8mb4_unicode_ci not null comment 'Optional. Name of the client', + description text collate utf8mb4_unicode_ci null comment 'The optional description of the client', + secret_totp varchar(32) null comment 'The secret TOTP key used to generated authorization tokens for each request, if null then the client is not required to authenticate via providing generated authorization tokens', + query_permission tinyint(1) default 1 null comment 'The permission level set for the query permission + 0 = Cannot query the database for content analysis + 1 = Can query the database for content analysis + 2 = Can query for additional sensitive information + 3 = Can query and upate metadata', + update_permission tinyint(1) default 0 null comment 'The permission level set for the update permission + 0 = Cannot update the database in anyway whatsoever + 1 = Can update entity metadata + 2 = Can submit evidence + 3 = Can change entity flags & change entity association id', + flags varchar(255) null comment 'Comma separated flags associated with the client', + created_timestamp bigint default unix_timestamp() not null comment 'The Unix Timestamp for when this client was first registered into the database', + updated_timestamp bigint default unix_timestamp() not null comment 'The Unix Timestamp for when this client record was last updated in the database', + seen_timestamp bigint default unix_timestamp() not null comment 'The Unix Timestamp for when this client was last seen by the database', + constraint clients_uuid_uindex + unique (uuid) +) + comment 'A table for housing registered users/clients that can access the database'; + +create index clients_created_timestamp_seen_timestamp_updated_timestamp_index + on clients (created_timestamp, seen_timestamp, updated_timestamp) + comment 'Index for the client''s timestamps indicators'; + +create index clients_enabled_index + on clients (enabled) + comment 'Index for the client''s enabled state'; + diff --git a/database/events.sql b/database/events.sql new file mode 100644 index 0000000..894f8a2 --- /dev/null +++ b/database/events.sql @@ -0,0 +1,35 @@ +create table events +( + uuid varchar(36) default uuid() not null comment 'The unique universal identifier for the event' + primary key, + code varchar(36) default 'GENERIC' not null comment 'A short-code event name that represents the event that has occured, the message field will contain more details in regards to the event code used.', + type varchar(36) default 'INFO' not null comment 'The event type, can be INFO, WARN, or ERR', + client_uuid varchar(36) null comment 'The client that created/caused the event', + peer varchar(256) null comment 'Optional. The peer that is associated with this event', + message text null comment 'The event details', + exception blob null comment 'The optional exeception details in a serialized format (MSGPACK)', + timestamp bigint default unix_timestamp() not null comment 'The Unix Timestamp for the record', + constraint events_uuid_uindex + unique (uuid), + constraint events_clients_uuid_fk + foreign key (client_uuid) references clients (uuid), + constraint events_peers_federated_address_fk + foreign key (peer) references peers (federated_address) +) + comment 'A table for housing event logs created by FederationLib in regards to Database changes.'; + +create index events_client_uuid_index + on events (client_uuid); + +create index events_code_index + on events (code); + +create index events_peer_index + on events (peer); + +create index events_timestamp_index + on events (timestamp); + +create index events_type_index + on events (type); + diff --git a/database/peers.sql b/database/peers.sql new file mode 100644 index 0000000..7d5df7e --- /dev/null +++ b/database/peers.sql @@ -0,0 +1,29 @@ +create table peers +( + federated_address varchar(256) not null comment 'The standard federated address of the peer' + primary key, + client_first_seen varchar(36) not null comment 'The UUID of the client that first discovered the peer', + client_last_seen varchar(36) not null comment 'The UUID of the client that last saw this peer', + active_restriction varchar(36) null comment 'Optional. The UUID of the restriction that is currently active on the peer', + discovered_timestamp bigint default unix_timestamp() not null comment 'The Unix Timestamp for when this peer was first discovered', + seen_timestamp bigint default unix_timestamp() not null comment 'The Unix Timestamp for when this peer was last seen', + constraint discovered_peers_federated_address_uindex + unique (federated_address), + constraint discovered_peers_clients_uuid_fk + foreign key (client_first_seen) references clients (uuid), + constraint discovered_peers_clients_uuid_fk2 + foreign key (client_last_seen) references clients (uuid), + constraint peers_restrictions_uuid_fk + foreign key (active_restriction) references restrictions (uuid) +) + comment 'The table for housing all the discovered peers'; + +create index discovered_peers_client_first_seen_index + on peers (client_first_seen); + +create index discovered_peers_client_last_seen_index + on peers (client_last_seen); + +create index peers_active_restriction_index + on peers (active_restriction); + diff --git a/database/peers_telegram_chat.sql b/database/peers_telegram_chat.sql new file mode 100644 index 0000000..d7ba67f --- /dev/null +++ b/database/peers_telegram_chat.sql @@ -0,0 +1,23 @@ +create table peers_telegram_chat +( + federated_address varchar(255) not null comment 'A federated internet standard ID for this Telegram chat' + primary key, + id bigint not null comment 'Unique identifier for this chat. This number may have more than 32 significant bits and some programming languages may have difficulty/silent defects in interpreting it. But it has at most 52 significant bits, so a signed 64-bit integer or double-precision float type are safe for storing this identifier.', + type varchar(32) not null comment 'The type of chat, Type of chat, can be either “group”, “supergroup” or "channel". "private" is not accepted', + title varchar(255) collate utf8mb4_unicode_ci null comment 'Optional. Title, for supergroups, channels and group chats', + username varchar(255) null comment 'Optional. Username, for private chats, supergroups and channels if available', + is_forum tinyint(1) default 0 not null comment 'Optional. True, if the supergroup chat is a forum (has topics enabled)', + constraint telegram_chat_federated_address_uindex + unique (federated_address), + constraint telegram_chat_id_uindex + unique (id), + constraint telegram_chat_pk2 + unique (id), + constraint telegram_chat_discovered_peers_federated_address_fk + foreign key (federated_address) references peers (federated_address) +) + comment 'A table for housing telegram chats for channel references'; + +create index telegram_chat_username_index + on peers_telegram_chat (username); + diff --git a/database/peers_telegram_user.sql b/database/peers_telegram_user.sql new file mode 100644 index 0000000..bd6a338 --- /dev/null +++ b/database/peers_telegram_user.sql @@ -0,0 +1,23 @@ +create table peers_telegram_user +( + federated_address varchar(255) default concat(`id`, 'u@telegram.org') not null comment 'A federated Internet Standard ID for this entity' + primary key, + id bigint not null comment 'Unique identifier for this user or bot. This number may have more than 32 significant bits and some programming languages may have difficulty/silent defects in interpreting it. But it has at most 52 significant bits, so a 64-bit integer or double-precision float type are safe for storing this identifier.', + is_bot tinyint(1) default 0 not null comment 'True, if this user is a bot', + first_name varchar(255) collate utf8mb4_unicode_ci null comment 'Optional. User''s or bot''s last name', + last_name varchar(255) collate utf8mb4_unicode_ci null comment 'Optional. User''s or bot''s last name', + username varchar(255) collate utf8mb4_unicode_ci null comment 'Optional. User''s or bot''s username', + language_code tinyint(2) null comment 'Optional. IETF language tag of the user''s language', + is_premium tinyint(1) default 0 not null comment 'Optional. True, if this user is a Telegram Premium user', + constraint telegram_user_federated_id_uindex + unique (federated_address), + constraint telegram_user_pk2 + unique (id), + constraint telegram_user_discovered_peers_federated_address_fk + foreign key (federated_address) references peers (federated_address) +) + comment 'The table for housing Telegram user entity references'; + +create index telegram_user_username_index + on peers_telegram_user (username); + diff --git a/database/query_documents.sql b/database/query_documents.sql new file mode 100644 index 0000000..54a643a --- /dev/null +++ b/database/query_documents.sql @@ -0,0 +1,47 @@ +create table query_documents +( + document_id varchar(26) not null comment 'The unique reference ID of the document (UUID v4)' + primary key, + subject_type varchar(32) default 'RECON' not null comment 'The subject type of the query document', + client_id varchar(36) not null comment 'The ID of the client that submitted the document', + client_signature varchar(36) null comment 'The authorization token signed with the document by the client', + channel_peer varchar(256) null comment 'Optional. The channel that the content was sent on', + from_peer varchar(256) null comment 'Optional. The peer that sent the content', + to_peer varchar(256) null comment 'Optional. The peer that is the intended reciever of the content', + proxy_peer varchar(256) null comment 'Optional. The proxy peer used to send the content, may indicates "from_peer" is the original sender.', + retain tinyint default 0 not null comment 'Indicates if this record should be retained or not', + timestamp bigint default unix_timestamp() not null comment 'The Unix Timestamp of the document''s submission (based off the authorization token)', + constraint query_documents_document_id_uindex + unique (document_id), + constraint query_documents_discovered_peers_federated_address_fk + foreign key (channel_peer) references peers (federated_address), + constraint query_documents_discovered_peers_federated_address_fk2 + foreign key (from_peer) references peers (federated_address), + constraint query_documents_discovered_peers_federated_address_fk3 + foreign key (to_peer) references peers (federated_address), + constraint query_documents_peers_federated_address_fk + foreign key (proxy_peer) references peers (federated_address) +) + comment 'An archive of all the submitted query documents the database received'; + +create index query_documents_channel_peer_index + on query_documents (channel_peer); + +create index query_documents_client_id_index + on query_documents (client_id); + +create index query_documents_from_peer_index + on query_documents (from_peer); + +create index query_documents_proxy_peer_index + on query_documents (proxy_peer); + +create index query_documents_retain_index + on query_documents (retain); + +create index query_documents_subject_type_index + on query_documents (subject_type); + +create index query_documents_to_peer_index + on query_documents (to_peer); + diff --git a/database/reports.sql b/database/reports.sql new file mode 100644 index 0000000..022f468 --- /dev/null +++ b/database/reports.sql @@ -0,0 +1,45 @@ +create table reports +( + report_id varchar(36) default uuid() not null comment 'The UUID V4 for the report''s ID' + primary key, + type varchar(32) default 'OTHER' not null comment 'The standard report type', + client_uuid varchar(36) not null comment 'The Client UUID responsible for submitting the report to the database', + reporting_peer varchar(256) null comment 'The peer that submitted the report, if empty will assume the client is the reporter.', + target_peer varchar(36) not null comment 'The target peer that the report is against at', + automated tinyint default 0 not null comment 'Indicates if the report was automated or not', + confidence_score float null comment 'The confidence score of the scanner if the report was automated', + reference_document varchar(36) not null comment 'The reference document to which this report came from', + scan_mode varchar(32) null comment 'The scan mode that was used to conduct the automated report', + reason text null comment 'The reason for the report', + submission_timestamp bigint default unix_timestamp() null comment 'The Unix Timestamp for when this report was submitted to the database', + constraint reports_report_id_uindex + unique (report_id), + constraint reports_clients_uuid_fk + foreign key (client_uuid) references clients (uuid), + constraint reports_discovered_peers_federated_address_fk + foreign key (reporting_peer) references peers (federated_address), + constraint reports_discovered_peers_federated_address_fk2 + foreign key (target_peer) references peers (federated_address), + constraint reports_query_documents_document_id_fk + foreign key (reference_document) references query_documents (document_id) +) + comment 'The table for storing reports made against peers'; + +create index reports_client_uuid_index + on reports (client_uuid); + +create index reports_reference_document_index + on reports (reference_document); + +create index reports_reporting_peer_index + on reports (reporting_peer); + +create index reports_submission_timestamp_index + on reports (submission_timestamp); + +create index reports_target_peer_index + on reports (target_peer); + +create index reports_type_index + on reports (type); + diff --git a/database/restrictions.sql b/database/restrictions.sql new file mode 100644 index 0000000..0ad422f --- /dev/null +++ b/database/restrictions.sql @@ -0,0 +1,28 @@ +create table restrictions +( + uuid varchar(36) default uuid() not null comment 'The Unique UUID V4 for the restriction record' + primary key, + peer varchar(256) not null comment 'The standard federated address of the peer', + client_uuid varchar(36) null comment 'Optional. The Client UUID that placed the restriction on the peer, if empty then it was an automated restriction placed by the system.', + reason varchar(64) not null comment 'The reason for the restriction that was applied to the peer', + automated tinyint default 0 not null comment 'Indicates if this restriction was automated', + type varchar(32) null comment 'The restriction type that was applied to the peer', + created bigint default unix_timestamp() not null comment 'The Unix Timestamp for when the restriction was created', + expires bigint not null comment 'The Unix Timestamp for when the restriction expires', + constraint restrictions_peer_uindex + unique (peer) comment 'The main unique index for the primary key "peer"', + constraint restrictions_uuid_uindex + unique (uuid), + constraint restrictions_clients_uuid_fk + foreign key (client_uuid) references clients (uuid), + constraint restrictions_peers_federated_address_fk + foreign key (peer) references peers (federated_address) +) + comment 'Archive of past restrictions placed on peers'; + +create index restrictions_automated_index + on restrictions (automated); + +create index restrictions_client_uuid_index + on restrictions (client_uuid); + diff --git a/main b/main new file mode 100644 index 0000000..1888cda --- /dev/null +++ b/main @@ -0,0 +1,6 @@ + '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 @@ +