diff --git a/README.md b/README.md index ab0c8fc..b259013 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ users. 3PIDs can be anything that uniquely and globally identify a user, like: - Twitter handle - Facebook ID -If you are unfamiliar with the Identity vocabulary and concepts in Matrix, **please read this [introduction](docs/concepts.md)**. +If you are unfamiliar with the Identity vocabulary and concepts in Matrix, **please read this [introduction](docs/concepts.md)**. # Features [Identity](docs/features/identity.md): As a [regular Matrix Identity service](https://matrix.org/docs/spec/identity_service/r0.1.0.html#general-principles): @@ -53,6 +53,7 @@ As an enhanced Identity service: - Central Matrix Identity servers - [Session Control](docs/threepids/session/session.md): Extensive control of where 3PIDs are transmitted so they are not leaked publicly by users +- [Registration control](docs/features/registration.md): Control and restrict user registration based on 3PID patterns or criterias, like a pending invite - [Authentication](docs/features/authentication.md): Use your Identity stores to perform authentication in [synapse](https://github.com/matrix-org/synapse) via the [REST password provider](https://github.com/kamax-io/matrix-synapse-rest-auth) - [Directory search](docs/features/directory.md) which allows you to search for users within your organisation, diff --git a/build.gradle b/build.gradle index 5c60ca6..4ac3e55 100644 --- a/build.gradle +++ b/build.gradle @@ -145,6 +145,9 @@ dependencies { // HTTP server compile 'io.undertow:undertow-core:2.0.16.Final' + + // Command parser for AS interface + implementation 'commons-cli:commons-cli:1.4' testCompile 'junit:junit:4.12' testCompile 'com.github.tomakehurst:wiremock:2.8.0' @@ -152,6 +155,14 @@ dependencies { testCompile 'com.icegreen:greenmail:1.5.9' } +jar { + manifest { + attributes( + 'Implementation-Version': mxisdVersion() + ) + } +} + shadowJar { baseName = project.name classifier = null diff --git a/docs/features/authentication.md b/docs/features/authentication.md index abce66a..f3d7998 100644 --- a/docs/features/authentication.md +++ b/docs/features/authentication.md @@ -21,7 +21,7 @@ It allows to use Identity stores configured in mxisd to authenticate users on yo Authentication is divided into two parts: - [Basic](#basic): authenticate with a regular username. -- [Advanced](#advanced): same as basic with extra ability to authenticate using a 3PID. +- [Advanced](#advanced): same as basic with extra abilities like authenticate using a 3PID or do username rewrite. ## Basic Authentication by username is possible by linking synapse and mxisd together using a specific module for synapse, also @@ -145,7 +145,49 @@ Your VirtualHost should now look similar to: ``` +##### nginx + +The specific configuration to add under the relevant `server`: + +```nginx +location /_matrix/client/r0/login { + proxy_pass http://localhost:8090; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; +} +``` + +Your `server` section should now look similar to: + +```nginx +server { + listen 443 ssl; + server_name matrix.example.org; + + # ... + + location /_matrix/client/r0/login { + proxy_pass http://localhost:8090; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + } + + location /_matrix/identity { + proxy_pass http://localhost:8090/_matrix/identity; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + } + + location /_matrix { + proxy_pass http://localhost:8008/_matrix; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + } +} +``` + #### DNS Overwrite + Just like you need to configure a reverse proxy to send client requests to mxisd, you also need to configure mxisd with the internal IP of the Homeserver so it can talk to it directly to integrate its directory search. @@ -165,6 +207,12 @@ In case the hostname is the same as your Matrix domain and `server.name` is not `value` is the base internal URL of the Homeserver, without any `/_matrix/..` or trailing `/`. +### Optional features + +The following features are available after you have a working Advanced setup: + +- Username rewrite: Allows you to rewrite the username of a regular login/pass authentication to a 3PID, that then gets resolved using the regular lookup process. Most common use case is to allow login with numerical usernames on synapse, which is not possible out of the box. + #### Username rewrite In mxisd config: ```yaml diff --git a/docs/features/experimental/application-service.md b/docs/features/experimental/application-service.md index d5e3a5e..d592361 100644 --- a/docs/features/experimental/application-service.md +++ b/docs/features/experimental/application-service.md @@ -1,25 +1,106 @@ -# Integration as an Application Service +# Application Service **WARNING:** These features are currently highly experimental. They can be removed or modified without notice. -All the features requires a Homeserver capable of connecting Application Services. +All the features requires a Homeserver capable of connecting [Application Services](https://matrix.org/docs/spec/application_service/r0.1.0.html). -## Email notification for Room invites by Matrix ID +The following capabilities are provided in this feature: +- [Admin commands](#admin-commands) +- [Email Notification about room invites by Matrix IDs](#email-notification-about-room-invites-by-matrix-ids) +- [Auto-reject of expired 3PID invites](#auto-reject-of-expired-3pid-invites) + +## Setup +> **NOTE:** Make sure you are familiar with [configuration format and rules](../../configure.md). + +Integration as an Application service is a three steps process: +1. Create the baseline mxisd configuration to allow integration. +2. Integrate with the homeserver. +3. Configure the specific capabilities, if applicable. + +### Configuration +#### Variables +Under the `appsvc` namespace: + +| Key | Type | Required | Default | Purpose | +|-----------------------|---------|----------|---------|----------------------------------------------------------------| +| `enabled` | boolean | No | `true` | Globally enable/disable the feature | +| `user.main` | string | No | `mxisd` | Localpart for the main appservice user | +| `endpoint.toHS.url` | string | Yes | *None* | Base URL to the Homeserver | +| `endpoint.toHS.token` | string | Yes | *None* | Token to use when sending requests to the Homeserver | +| `endpoint.toAS.url` | string | Yes | *None* | Base URL to mxisd from the Homeserver | +| `endpoint.toAS.token` | string | Yes | *None* | Token for the Homeserver to use when sending requests to mxisd | + +#### Example +```yaml +appsvc: + endpoint: + toHS: + url: 'http://localhost:8008' + token: 'ExampleTokenToHS-ChangeMe!' + toAS: + url: 'http://localhost:8090' + token: 'ExampleTokenToAS-ChangeMe!' +``` +### Integration +#### Synapse +Under the `appsvc.registration.synapse` namespace: + +| Key | Type | Required | Default | Purpose | +|--------|--------|----------|--------------------|--------------------------------------------------------------------------| +| `id` | string | No | `appservice-mxisd` | The unique, user-defined ID of this application service. See spec. | +| `file` | string | Yes | *None* | If defined, the synapse registration file that should be created/updated | + +##### Example +```yaml +appsvc: + registration: + synapse: + file: '/etc/matrix-synapse/mxisd-appservice-registration.yaml' +``` + +Edit your `homeserver.yaml` and add a new entry to the appservice config file, which should look something like this: +```yaml +app_service_config_files: + - '/etc/matrix-synapse/mxisd-appservice-registration.yaml' + - ... +``` + +Restart synapse when done to register mxisd. + +#### Others +See your Homeserver documentation on how to integrate. + +## Capabilities +### Admin commands +#### Setup +Min config: +```yaml +appsvc: + feature: + admin: + allowedRoles: + - '+aMatrixCommunity:example.org' + - 'SomeLdapGroup' + - 'AnyOtherArbitraryRoleFromIdentityStores' +``` + +#### Use +The following steps assume: +- `matrix.domain` set to `example.org` +- `appsvc.user.main` set to `mxisd` or not set + +1. Invite `@mxisd:example.org` to a new direct chat +2. Type `!help` to get all available commands + +### Email Notification about room invites by Matrix IDs This feature allows for users found in Identity stores to be instantly notified about Room Invites, regardless if their account was already provisioned on the Homeserver. -### Requirements +#### Requirements - [Identity store(s)](../../stores/README.md) supporting the Profile feature - At least one email entry in the identity store for each user that could be invited. -### Configuration +#### Configuration In your mxisd config file: ```yaml -matrix: - listener: - url: '' - localpart: 'appservice-mxisd' - token: - hs: 'HS_TOKEN_CHANGE_ME' - synapseSql: enabled: false ## Do not use this line if Synapse is used as an Identity Store type: '' @@ -33,40 +114,8 @@ If you do not configure it, some placeholders will not be available in the notif You can also change the default template of the notification using the `generic.matrixId` template option. See [the Template generator documentation](../../threepids/notification/template-generator.md) for more info. -### Homeserver integration -#### Synapse -Create a new appservice registration file. Futher config will assume it is in `/etc/matrix-synapse/appservice-mxisd.yaml` -```yaml -id: "appservice-mxisd" -url: "http://127.0.0.1:8090" -as_token: "AS_TOKEN_CHANGE_ME" -hs_token: "HS_TOKEN_CHANGE_ME" -sender_localpart: "appservice-mxisd" -namespaces: - users: - - regex: "@*" - exclusive: false - aliases: [] - rooms: [] -``` -`id`: An arbitrary unique string to identify the AS. -`url`: mxisd to reach mxisd. This ideally should be HTTP and not going through any reverse proxy. -`as_token`: Arbitrary value used by mxisd when talking to the HS. Not currently used. -`hs_token`: Arbitrary value used by synapse when talking to mxisd. Must match `token.hs` in mxisd config. -`sender_localpart`: Username for the mxisd itself on the HS. Default configuration should be kept. -`namespaces`: To be kept as is. - -Edit your `homeserver.yaml` and add a new entry to the appservice config file, which should look something like this: -```yaml -app_service_config_files: - - '/etc/matrix-synapse/appservice-mxisd.yaml' - - ... -``` - -Restart synapse when done to register mxisd. - -#### Others -See your Homeserver documentation on how to integrate. - -### Test +#### Test Invite a user which is part of your domain while an appropriate Identity store is used. + +### Auto-reject of expired 3PID invites +*TBC* diff --git a/docs/features/identity.md b/docs/features/identity.md index 6e8d830..6805efe 100644 --- a/docs/features/identity.md +++ b/docs/features/identity.md @@ -1,6 +1,13 @@ # Identity Implementation of the [Identity Service API r0.1.0](https://matrix.org/docs/spec/identity_service/r0.1.0.html). +- [Lookups](#lookups) +- [Invitations](#invitations) + - [Expiration](#expiration) + - [Policies](#policies) + - [Resolution](#resolution) +- [3PIDs Management](#3pids-management) + ## Lookups If you would like to use the central matrix.org Identity server to ensure maximum discovery at the cost of potentially leaking all your contacts information, add the following to your configuration: @@ -12,8 +19,78 @@ forward: **NOTE:** You should carefully consider enabling this option, which is discouraged. For more info, see the [relevant issue](https://github.com/kamax-matrix/mxisd/issues/76). -## Room Invitations -Resolution can be customized using the following configuration: +## Invitations +### Expiration +#### Overview +Matrix does not provide a mean to remove/cancel pending 3PID invitations with the APIs. The current reference +implementations also do not provide any mean to do so. This leads to 3PID invites forever stuck in rooms. + +To provide this functionality, mxisd uses a workaround: resolve the invite to a dedicated User ID, which can be +controlled by mxisd or a bot/service that will then reject the invite. + +If this dedicated User ID is to be controlled by mxisd, the [Application Service](experimental/application-service.md) +feature must be configured and integrated with your Homeserver, as well as the *Auto-reject 3PID invite capability*. + +#### Configuration +```yaml +invite: + expiration: + enabled: true/false + after: 5 + resolveTo: '@john.doe:example.org' +``` +`enabled` +- Purpose: Enable or disable the invite expiration feature. +- Default: `true` + +`after` +- Purpose: Amount of minutes before an invitation expires. +- Default: `10080` (7 days) + +`resolveTo` +- Purpose: Matrix User ID to resolve the expired invitations to. +- Default: Computed from `appsvc.user.inviteExpired` and `matrix.domain` + +### Policies +3PID invite policies are the companion feature of [Registration](registration.md). While the Registration feature acts on +requirements for the invitee/register, this feature acts on requirement for the one(s) performing 3PID invites, ensuring +a coherent system. + +It relies on only allowing people with specific [Roles](profile.md) to perform 3PID invites. This would typically allow +a tight-control on a server setup with is "invite-only" or semi-open (relying on trusted people to invite new members). + +It's a middle ground between a closed server, where every user must be created or already exists in an Identity store, +and an open server, where anyone can register. + +#### Integration +Because Identity Servers do not control 3PID invites as per Matrix spec, mxisd needs to intercept a set of Homeserver +endpoints to apply the policies. + +##### Reverse Proxy +###### nginx +**IMPORTANT**: Must be placed before your global `/_matrix` entry: +```nginx +location ~* ^/_matrix/client/r0/rooms/([^/]+)/invite$ { + proxy_pass http://127.0.0.1:8090; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; +} +``` + +#### Configuration +The only policy currently available is to restrict 3PID invite to users having a specific (set of) role(s), like so: + +```yaml +invite: + policy: + ifSender: + hasRole: + - '' + - '' +``` + +### Resolution +Resolution of 3PID invitations can be customized using the following configuration: `invite.resolution.recursive` - Default value: `true` @@ -26,5 +103,5 @@ Resolution can be customized using the following configuration: - Default value: `1` - Description: How often, in minutes, mxisd should try to resolve pending invites. -## 3PID addition to user profile +## 3PIDs Management See the [3PID session documents](../threepids/session) diff --git a/docs/features/registration.md b/docs/features/registration.md new file mode 100644 index 0000000..54a3a7a --- /dev/null +++ b/docs/features/registration.md @@ -0,0 +1,111 @@ +# Registration +- [Overview](#overview) +- [Integration](#integration) + - [Reverse Proxy](#reverse-proxy) + - [nginx](#nginx) + - [Apache](#apache) + - [Homeserver](#homeserver) + - [synapse](#synapse) +- [Configuration](#configuration) + - [Example](#example) +- [Usage](#usage) + +## Overview +**NOTE**: This feature is beta: it is considered stable enough for production but is incomplete and may contain bugs. + +Registration is an enhanced feature of mxisd to control registrations involving 3PIDs on a Homeserver based on policies: +- Match pending 3PID invites on the server +- Match 3PID pattern, like a specific set of domains for emails +- In futher releases, use 3PIDs found in Identity stores + +It aims to help open or invite-only registration servers control what is possible to do and ensure only approved people +can register on a given server in a implementation-agnostic manner. + +**IMPORTANT:** This feature does not control registration in general. It only acts on endpoints related to 3PIDs during +the registration process. +As such, it relies on the homeserver to require 3PIDs with the registration flows. + +This feature is not part of the Matrix Identity Server spec. + +## Integration +mxisd needs to be integrated at several levels for this feature to work: +- Reverse proxy: intercept the 3PID register endpoints and act on them +- Homeserver: require 3PID to be part of the registration data + +Later version(s) of this feature may directly control registration itself to create a coherent experience +### Reverse Proxy +#### nginx +```nginx +location ^/_matrix/client/r0/register/[^/]/?$ { + proxy_pass http://127.0.0.1:8090; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; +} +``` + +#### apache +> TBC + +### Homeserver +#### Synapse +```yaml +enable_registration: true +registrations_require_3pid: + - email +``` + +## Configuration +See the [Configuration](../configuration.md) introduction doc on how to read the configuration keys. +An example of working configuration is avaiable at the end of this section. +### Enable/Disable +`register.allowed`, taking a boolean, can be used to enable/disable registration if the attempt is not 3PID-based. +`false` is the default value to prevent open registration, as you must allow it on the homeserver side. + +### For invites +`register.invite`, taking a boolean, controls if registration can be made using a 3PID which matches a pending 3PID invite. +`true` is the default value. + +### 3PID-specific +At this time, only `email` is supported with 3PID specific configuration with this feature. + +#### Email +**Base key**: `register.threepid.email` + +##### Domain whitelist/blacklist +If you would like to control which domains are allowed to be used when registrating with an email, the following sub-keys +are available: +- `domain.whitelist` +- `domain.blacklist` + +The value format is an hybrid between glob patterns and postfix configuration files with the following syntax: +- `*` will match the domain and any sub-domain(s) +- `.` will only match sub-domain(s) +- `` will only match the exact domain + +The following table illustrates pattern and maching status against example values: + +| Config value | Matches `example.org` | Matches `sub.example.org` | +|--------------- |-----------------------|---------------------------| +| `*example.org` | Yes | Yes | +| `.example.org` | No | Yes | +| `example.org` | Yes | No | + +### Example +For the following example configuration: +```yaml +register: + policy: + threepid: + email: + domain: + whitelist: + - '*example.org' + - '.example.net' + - 'example.com' +``` +- Users can register using 3PIDs of pending invites, being allowed by default. +- Users can register using an email from `example.org` and any sub-domain, only sub-domains of `example.net` and `example.com` but not its sub-domains. +- Otherwise, user registration will be denied. + +## Usage +Nothing special is needed. Register using a regular Matrix client. diff --git a/docs/stores/synapse.md b/docs/stores/synapse.md index 21d768b..584b276 100644 --- a/docs/stores/synapse.md +++ b/docs/stores/synapse.md @@ -1,5 +1,6 @@ # Synapse Identity Store -Synapse's Database itself can be used as an Identity store. +Synapse's Database itself can be used as an Identity store. This identity store is a regular SQL store with +built-in default queries that matches Synapse DB. ## Features | Name | Supported | @@ -9,7 +10,8 @@ Synapse's Database itself can be used as an Identity store. | [Identity](../features/identity.md) | Yes | | [Profile](../features/profile.md) | Yes | -Authentication is done by Synapse itself. +- Authentication is done by Synapse itself. +- Roles are mapped to communities. The Role name/ID uses the community ID in the form `+id:domain.tld` ## Configuration ### Basic diff --git a/mxisd.example.yaml b/mxisd.example.yaml index 171a8a8..cc40217 100644 --- a/mxisd.example.yaml +++ b/mxisd.example.yaml @@ -14,6 +14,11 @@ # NOTE: in Synapse Homeserver, the Matrix domain is defined as 'server_name' in configuration file. # # This is used to build the various identifiers in all the features. +# +# If the hostname of the public URL used to reach your Matrix services is different from your Matrix domain, +# per example matrix.domain.tld vs domain.tld, then use the server.name configuration option. +# See the "Configure" section of the Getting Started guide for more info. +# matrix: domain: '' diff --git a/src/main/java/io/kamax/mxisd/HttpMxisd.java b/src/main/java/io/kamax/mxisd/HttpMxisd.java index 83ce897..0ee9d9b 100644 --- a/src/main/java/io/kamax/mxisd/HttpMxisd.java +++ b/src/main/java/io/kamax/mxisd/HttpMxisd.java @@ -21,23 +21,30 @@ package io.kamax.mxisd; import io.kamax.mxisd.config.MxisdConfig; +import io.kamax.mxisd.http.undertow.handler.InternalInfoHandler; import io.kamax.mxisd.http.undertow.handler.OptionsHandler; import io.kamax.mxisd.http.undertow.handler.SaneHandler; import io.kamax.mxisd.http.undertow.handler.as.v1.AsNotFoundHandler; import io.kamax.mxisd.http.undertow.handler.as.v1.AsTransactionHandler; +import io.kamax.mxisd.http.undertow.handler.as.v1.AsUserHandler; import io.kamax.mxisd.http.undertow.handler.auth.RestAuthHandler; import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginGetHandler; import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginHandler; import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginPostHandler; import io.kamax.mxisd.http.undertow.handler.directory.v1.UserDirectorySearchHandler; import io.kamax.mxisd.http.undertow.handler.identity.v1.*; +import io.kamax.mxisd.http.undertow.handler.invite.v1.RoomInviteHandler; import io.kamax.mxisd.http.undertow.handler.profile.v1.InternalProfileHandler; import io.kamax.mxisd.http.undertow.handler.profile.v1.ProfileHandler; +import io.kamax.mxisd.http.undertow.handler.register.v1.Register3pidRequestTokenHandler; import io.kamax.mxisd.http.undertow.handler.status.StatusHandler; +import io.kamax.mxisd.http.undertow.handler.status.VersionHandler; import io.undertow.Handlers; import io.undertow.Undertow; import io.undertow.server.HttpHandler; +import java.util.Objects; + public class HttpMxisd { // Core @@ -46,6 +53,12 @@ public class HttpMxisd { // I/O private Undertow httpSrv; + static { + // Used in XNIO package, dependency of Undertow + // We switch to slf4j + System.setProperty("org.jboss.logging.provider", "slf4j"); + } + public HttpMxisd(MxisdConfig cfg) { m = new Mxisd(cfg); } @@ -54,9 +67,12 @@ public class HttpMxisd { m.start(); HttpHandler helloHandler = SaneHandler.around(new HelloHandler()); - HttpHandler asNotFoundHandler = SaneHandler.around(new AsNotFoundHandler(m.getAs())); + + HttpHandler asUserHandler = SaneHandler.around(new AsUserHandler(m.getAs())); HttpHandler asTxnHandler = SaneHandler.around(new AsTransactionHandler(m.getAs())); - HttpHandler storeInvHandler = SaneHandler.around(new StoreInviteHandler(m.getConfig().getServer(), m.getInvitationManager(), m.getKeyManager())); + HttpHandler asNotFoundHandler = SaneHandler.around(new AsNotFoundHandler(m.getAs())); + + HttpHandler storeInvHandler = SaneHandler.around(new StoreInviteHandler(m.getConfig().getServer(), m.getInvite(), m.getKeyManager())); HttpHandler sessValidateHandler = SaneHandler.around(new SessionValidateHandler(m.getSession(), m.getConfig().getServer(), m.getConfig().getView())); httpSrv = Undertow.builder().addHttpListener(m.getConfig().getServer().getPort(), "0.0.0.0").setHandler(Handlers.routing() @@ -65,6 +81,7 @@ public class HttpMxisd { // Status endpoints .get(StatusHandler.Path, SaneHandler.around(new StatusHandler())) + .get(VersionHandler.Path, SaneHandler.around(new VersionHandler())) // Authentication endpoints .get(LoginHandler.Path, SaneHandler.around(new LoginGetHandler(m.getAuth(), m.getHttpClient()))) @@ -77,40 +94,53 @@ public class HttpMxisd { // Key endpoints .get(KeyGetHandler.Path, SaneHandler.around(new KeyGetHandler(m.getKeyManager()))) .get(RegularKeyIsValidHandler.Path, SaneHandler.around(new RegularKeyIsValidHandler(m.getKeyManager()))) - .get(EphemeralKeyIsValidHandler.Path, SaneHandler.around(new EphemeralKeyIsValidHandler())) + .get(EphemeralKeyIsValidHandler.Path, SaneHandler.around(new EphemeralKeyIsValidHandler(m.getKeyManager()))) // Identity endpoints .get(HelloHandler.Path, helloHandler) .get(HelloHandler.Path + "/", helloHandler) // Be lax with possibly trailing slash - .get(SingleLookupHandler.Path, SaneHandler.around(new SingleLookupHandler(m.getIdentity(), m.getSign()))) + .get(SingleLookupHandler.Path, SaneHandler.around(new SingleLookupHandler(m.getConfig(), m.getIdentity(), m.getSign()))) .post(BulkLookupHandler.Path, SaneHandler.around(new BulkLookupHandler(m.getIdentity()))) .post(StoreInviteHandler.Path, storeInvHandler) .post(SessionStartHandler.Path, SaneHandler.around(new SessionStartHandler(m.getSession()))) .get(SessionValidateHandler.Path, sessValidateHandler) .post(SessionValidateHandler.Path, sessValidateHandler) .get(SessionTpidGetValidatedHandler.Path, SaneHandler.around(new SessionTpidGetValidatedHandler(m.getSession()))) - .post(SessionTpidBindHandler.Path, SaneHandler.around(new SessionTpidBindHandler(m.getSession(), m.getInvitationManager()))) + .post(SessionTpidBindHandler.Path, SaneHandler.around(new SessionTpidBindHandler(m.getSession(), m.getInvite()))) .post(SessionTpidUnbindHandler.Path, SaneHandler.around(new SessionTpidUnbindHandler(m.getSession()))) + .post(SignEd25519Handler.Path, SaneHandler.around(new SignEd25519Handler(m.getConfig(), m.getInvite(), m.getSign()))) // Profile endpoints .get(ProfileHandler.Path, SaneHandler.around(new ProfileHandler(m.getProfile()))) .get(InternalProfileHandler.Path, SaneHandler.around(new InternalProfileHandler(m.getProfile()))) + // Registration endpoints + .post(Register3pidRequestTokenHandler.Path, SaneHandler.around(new Register3pidRequestTokenHandler(m.getReg(), m.getClientDns(), m.getHttpClient()))) + + // Invite endpoints + .post(RoomInviteHandler.Path, SaneHandler.around(new RoomInviteHandler(m.getHttpClient(), m.getClientDns(), m.getInvite()))) + // Application Service endpoints - .get("/_matrix/app/v1/users/**", asNotFoundHandler) - .get("/users/**", asNotFoundHandler) // Legacy endpoint + .get(AsUserHandler.Path, asUserHandler) .get("/_matrix/app/v1/rooms/**", asNotFoundHandler) - .get("/rooms/**", asNotFoundHandler) // Legacy endpoint .put(AsTransactionHandler.Path, asTxnHandler) + + .get("/users/{" + AsUserHandler.ID + "}", asUserHandler) // Legacy endpoint + .get("/rooms/**", asNotFoundHandler) // Legacy endpoint .put("/transactions/{" + AsTransactionHandler.ID + "}", asTxnHandler) // Legacy endpoint + // Banned endpoints + .get(InternalInfoHandler.Path, SaneHandler.around(new InternalInfoHandler())) + ).build(); httpSrv.start(); } public void stop() { - httpSrv.stop(); + // Because it might have never been initialized if an exception is thrown early + if (Objects.nonNull(httpSrv)) httpSrv.stop(); + m.stop(); } diff --git a/src/main/java/io/kamax/mxisd/Mxisd.java b/src/main/java/io/kamax/mxisd/Mxisd.java index 547c445..e6470ea 100644 --- a/src/main/java/io/kamax/mxisd/Mxisd.java +++ b/src/main/java/io/kamax/mxisd/Mxisd.java @@ -20,8 +20,6 @@ package io.kamax.mxisd; -import io.kamax.matrix.crypto.KeyManager; -import io.kamax.matrix.crypto.SignatureManager; import io.kamax.mxisd.as.AppSvcManager; import io.kamax.mxisd.auth.AuthManager; import io.kamax.mxisd.auth.AuthProviders; @@ -29,6 +27,9 @@ import io.kamax.mxisd.backend.IdentityStoreSupplier; import io.kamax.mxisd.backend.sql.synapse.Synapse; import io.kamax.mxisd.config.MxisdConfig; import io.kamax.mxisd.crypto.CryptoFactory; +import io.kamax.mxisd.crypto.KeyManager; +import io.kamax.mxisd.crypto.SignatureManager; +import io.kamax.mxisd.crypto.ed25519.Ed25519KeyManager; import io.kamax.mxisd.directory.DirectoryManager; import io.kamax.mxisd.directory.DirectoryProviders; import io.kamax.mxisd.dns.ClientDnsOverwrite; @@ -46,9 +47,11 @@ import io.kamax.mxisd.notification.NotificationHandlers; import io.kamax.mxisd.notification.NotificationManager; import io.kamax.mxisd.profile.ProfileManager; import io.kamax.mxisd.profile.ProfileProviders; +import io.kamax.mxisd.registration.RegistrationManager; import io.kamax.mxisd.session.SessionManager; import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.ormlite.OrmLiteSqlStorage; +import org.apache.commons.lang.StringUtils; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; @@ -56,6 +59,10 @@ import java.util.ServiceLoader; public class Mxisd { + public static final String Name = StringUtils.defaultIfBlank(Mxisd.class.getPackage().getImplementationTitle(), "mxisd"); + public static final String Version = StringUtils.defaultIfBlank(Mxisd.class.getPackage().getImplementationVersion(), "UNKNOWN"); + public static final String Agent = Name + "/" + Version; + private MxisdConfig cfg; private CloseableHttpClient httpClient; @@ -63,8 +70,9 @@ public class Mxisd { private IStorage store; - private KeyManager keyMgr; + private Ed25519KeyManager keyMgr; private SignatureManager signMgr; + private ClientDnsOverwrite clientDns; // Features private AuthManager authMgr; @@ -75,6 +83,10 @@ public class Mxisd { private AppSvcManager asHander; private SessionManager sessMgr; private NotificationManager notifMgr; + private RegistrationManager regMgr; + + // HS-specific classes + private Synapse synapse; public Mxisd(MxisdConfig cfg) { this.cfg = cfg.build(); @@ -82,7 +94,7 @@ public class Mxisd { private void build() { httpClient = HttpClients.custom() - .setUserAgent("mxisd") + .setUserAgent(Agent) .setMaxConnPerRoute(Integer.MAX_VALUE) .setMaxConnTotal(Integer.MAX_VALUE) .build(); @@ -92,10 +104,10 @@ public class Mxisd { store = new OrmLiteSqlStorage(cfg); keyMgr = CryptoFactory.getKeyManager(cfg.getKey()); - signMgr = CryptoFactory.getSignatureManager(keyMgr, cfg.getServer()); - ClientDnsOverwrite clientDns = new ClientDnsOverwrite(cfg.getDns().getOverwrite()); + signMgr = CryptoFactory.getSignatureManager(keyMgr); + clientDns = new ClientDnsOverwrite(cfg.getDns().getOverwrite()); FederationDnsOverwrite fedDns = new FederationDnsOverwrite(cfg.getDns().getOverwrite()); - Synapse synapse = new Synapse(cfg.getSynapseSql()); + synapse = new Synapse(cfg.getSynapseSql()); BridgeFetcher bridgeFetcher = new BridgeFetcher(cfg.getLookup().getRecursive().getBridge(), srvFetcher); ServiceLoader.load(IdentityStoreSupplier.class).iterator().forEachRemaining(p -> p.accept(this)); @@ -105,10 +117,11 @@ public class Mxisd { pMgr = new ProfileManager(ProfileProviders.get(), clientDns, httpClient); notifMgr = new NotificationManager(cfg.getNotification(), NotificationHandlers.get()); sessMgr = new SessionManager(cfg.getSession(), cfg.getMatrix(), store, notifMgr, idStrategy, httpClient); - invMgr = new InvitationManager(cfg.getInvite(), store, idStrategy, signMgr, fedDns, notifMgr); + invMgr = new InvitationManager(cfg, store, idStrategy, keyMgr, signMgr, fedDns, notifMgr, pMgr); authMgr = new AuthManager(cfg, AuthProviders.get(), idStrategy, invMgr, clientDns, httpClient); dirMgr = new DirectoryManager(cfg.getDirectory(), clientDns, httpClient, DirectoryProviders.get()); - asHander = new AppSvcManager(cfg, store, pMgr, notifMgr, synapse); + regMgr = new RegistrationManager(cfg.getRegister(), httpClient, clientDns, invMgr); + asHander = new AppSvcManager(this); } public MxisdConfig getConfig() { @@ -119,6 +132,10 @@ public class Mxisd { return httpClient; } + public ClientDnsOverwrite getClientDns() { + return clientDns; + } + public IRemoteIdentityServerFetcher getServerFetcher() { return srvFetcher; } @@ -127,7 +144,7 @@ public class Mxisd { return keyMgr; } - public InvitationManager getInvitationManager() { + public InvitationManager getInvite() { return invMgr; } @@ -155,6 +172,10 @@ public class Mxisd { return signMgr; } + public RegistrationManager getReg() { + return regMgr; + } + public AppSvcManager getAs() { return asHander; } @@ -163,6 +184,14 @@ public class Mxisd { return notifMgr; } + public IStorage getStore() { + return store; + } + + public Synapse getSynapse() { + return synapse; + } + public void start() { build(); } diff --git a/src/main/java/io/kamax/mxisd/MxisdStandaloneExec.java b/src/main/java/io/kamax/mxisd/MxisdStandaloneExec.java index e5375df..bf0f7f9 100644 --- a/src/main/java/io/kamax/mxisd/MxisdStandaloneExec.java +++ b/src/main/java/io/kamax/mxisd/MxisdStandaloneExec.java @@ -37,21 +37,33 @@ public class MxisdStandaloneExec { public static void main(String[] args) { try { - log.info("------------- mxisd starting -------------"); MxisdConfig cfg = null; - Iterator argsIt = Arrays.asList(args).iterator(); while (argsIt.hasNext()) { String arg = argsIt.next(); - if (StringUtils.equals("-c", arg)) { + if (StringUtils.equalsAny(arg, "-h", "--help", "-?", "--usage")) { + System.out.println("Available arguments:" + System.lineSeparator()); + System.out.println(" -h, --help Show this help message"); + System.out.println(" --version Print the version then exit"); + System.out.println(" -c, --config Set the configuration file location"); + System.out.println(" "); + System.exit(0); + } else if (StringUtils.equalsAny(arg, "-c", "--config")) { String cfgFile = argsIt.next(); cfg = YamlConfigLoader.loadFromFile(cfgFile); + } else if (StringUtils.equals("--version", arg)) { + System.out.println(Mxisd.Version); + System.exit(0); } else { - log.info("Invalid argument: {}", arg); + System.err.println("Invalid argument: " + arg); + System.err.println("Try '--help' for available arguments"); System.exit(1); } } + log.info("mxisd starting"); + log.info("Version: {}", Mxisd.Version); + if (Objects.isNull(cfg)) { cfg = YamlConfigLoader.tryLoadFromFile("mxisd.yaml").orElseGet(MxisdConfig::new); } @@ -59,11 +71,11 @@ public class MxisdStandaloneExec { HttpMxisd mxisd = new HttpMxisd(cfg); Runtime.getRuntime().addShutdownHook(new Thread(() -> { mxisd.stop(); - log.info("------------- mxisd stopped -------------"); + log.info("mxisd stopped"); })); mxisd.start(); - log.info("------------- mxisd started -------------"); + log.info("mxisd started"); } catch (ConfigurationException e) { log.error(e.getDetailedMessage()); log.error(e.getMessage()); diff --git a/src/main/java/io/kamax/mxisd/as/AppSvcManager.java b/src/main/java/io/kamax/mxisd/as/AppSvcManager.java index 1dedac5..e389522 100644 --- a/src/main/java/io/kamax/mxisd/as/AppSvcManager.java +++ b/src/main/java/io/kamax/mxisd/as/AppSvcManager.java @@ -22,76 +22,183 @@ package io.kamax.mxisd.as; import com.google.gson.JsonObject; import io.kamax.matrix.MatrixID; -import io.kamax.matrix.ThreePidMedium; import io.kamax.matrix._MatrixID; -import io.kamax.matrix._ThreePid; +import io.kamax.matrix.client.MatrixClientContext; +import io.kamax.matrix.client.as.MatrixApplicationServiceClient; import io.kamax.matrix.event.EventKey; import io.kamax.matrix.json.GsonUtil; -import io.kamax.mxisd.backend.sql.synapse.Synapse; -import io.kamax.mxisd.config.MatrixConfig; +import io.kamax.mxisd.Mxisd; +import io.kamax.mxisd.as.processor.event.EventTypeProcessor; +import io.kamax.mxisd.as.processor.event.MembershipEventProcessor; +import io.kamax.mxisd.as.processor.event.MessageEventProcessor; +import io.kamax.mxisd.as.registration.SynapseRegistrationYaml; +import io.kamax.mxisd.config.AppServiceConfig; import io.kamax.mxisd.config.MxisdConfig; +import io.kamax.mxisd.exception.ConfigurationException; import io.kamax.mxisd.exception.HttpMatrixException; import io.kamax.mxisd.exception.NotAllowedException; -import io.kamax.mxisd.notification.NotificationManager; -import io.kamax.mxisd.profile.ProfileManager; import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.ormlite.dao.ASTransactionDao; import io.kamax.mxisd.util.GsonParser; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.introspector.BeanAccess; +import org.yaml.snakeyaml.representer.Representer; +import java.io.FileOutputStream; +import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; public class AppSvcManager { - private transient final Logger log = LoggerFactory.getLogger(AppSvcManager.class); + private static final Logger log = LoggerFactory.getLogger(AppSvcManager.class); - private final GsonParser parser; + private final AppServiceConfig cfg; + private final IStorage store; + private final GsonParser parser = new GsonParser(); - private MatrixConfig cfg; - private IStorage store; - private ProfileManager profiler; - private NotificationManager notif; - private Synapse synapse; + private MatrixApplicationServiceClient client; + private Map processors = new HashMap<>(); + private Map> transactionsInProgress = new ConcurrentHashMap<>(); - private Map> transactionsInProgress; + public AppSvcManager(Mxisd m) { + this.cfg = m.getConfig().getAppsvc(); + this.store = m.getStore(); - public AppSvcManager(MxisdConfig cfg, IStorage store, ProfileManager profiler, NotificationManager notif, Synapse synapse) { - this.cfg = cfg.getMatrix(); - this.store = store; - this.profiler = profiler; - this.notif = notif; - this.synapse = synapse; + /* + We process the configuration to make sure all is fine and setting default values if needed + */ - parser = new GsonParser(); - transactionsInProgress = new ConcurrentHashMap<>(); + // By default, the feature is enabled + cfg.setEnabled(ObjectUtils.defaultIfNull(cfg.isEnabled(), false)); + + if (!cfg.isEnabled()) { + return; + } + + if (Objects.isNull(cfg.getEndpoint().getToAS().getUrl())) { + throw new ConfigurationException("App Service: Endpoint: To AS: URL"); + } + + if (Objects.isNull(cfg.getEndpoint().getToAS().getToken())) { + throw new ConfigurationException("App Service: Endpoint: To AS: Token", "Must be set, even if to an empty string"); + } + + if (Objects.isNull(cfg.getEndpoint().getToHS().getUrl())) { + throw new ConfigurationException("App Service: Endpoint: To HS: URL"); + } + + if (Objects.isNull(cfg.getEndpoint().getToHS().getToken())) { + throw new ConfigurationException("App Service: Endpoint: To HS: Token", "Must be set, even if to an empty string"); + } + + // We set a default status for each feature individually + cfg.getFeature().getAdmin().setEnabled(ObjectUtils.defaultIfNull(cfg.getFeature().getAdmin().getEnabled(), cfg.isEnabled())); + cfg.getFeature().setCleanExpiredInvite(ObjectUtils.defaultIfNull(cfg.getFeature().getCleanExpiredInvite(), cfg.isEnabled())); + cfg.getFeature().setInviteById(ObjectUtils.defaultIfNull(cfg.getFeature().getInviteById(), false)); + + if (cfg.getFeature().getAdmin().getEnabled()) { + if (StringUtils.isBlank(cfg.getUser().getMain())) { + throw new ConfigurationException("App Service admin feature is enabled, but no main user configured"); + } + + if (cfg.getUser().getMain().startsWith("@") || cfg.getUser().getMain().contains(":")) { + throw new ConfigurationException("App Service: Users: Main ID: Is not a localpart"); + } + } + + if (cfg.getFeature().getCleanExpiredInvite()) { + if (StringUtils.isBlank(cfg.getUser().getInviteExpired())) { + throw new ConfigurationException("App Service user for Expired Invite is not set"); + } + + if (cfg.getUser().getMain().startsWith("@") || cfg.getUser().getMain().contains(":")) { + throw new ConfigurationException("App Service: Users: Expired Invite ID: Is not a localpart"); + } + } + + MatrixClientContext mxContext = new MatrixClientContext(); + mxContext.setDomain(m.getConfig().getMatrix().getDomain()); + mxContext.setToken(cfg.getEndpoint().getToHS().getToken()); + mxContext.setHsBaseUrl(cfg.getEndpoint().getToHS().getUrl()); + client = new MatrixApplicationServiceClient(mxContext); + + processors.put("m.room.member", new MembershipEventProcessor(client, m)); + processors.put("m.room.message", new MessageEventProcessor(m, client)); + + processSynapseConfig(m.getConfig()); + } + + private void processSynapseConfig(MxisdConfig cfg) { + String synapseRegFile = cfg.getAppsvc().getRegistration().getSynapse().getFile(); + + if (StringUtils.isBlank(synapseRegFile)) { + log.info("No synapse registration file path given - skipping generation..."); + return; + } + + SynapseRegistrationYaml syncCfg = SynapseRegistrationYaml.parse(cfg.getAppsvc(), cfg.getMatrix().getDomain()); + + Representer rep = new Representer(); + rep.getPropertyUtils().setBeanAccess(BeanAccess.FIELD); + Yaml yaml = new Yaml(rep); + + // SnakeYAML set the type of object on the first line, which can fail to be parsed on synapse + // We therefore need to split the resulting string, remove the first line, and then write it + List lines = new ArrayList<>(Arrays.asList(yaml.dump(syncCfg).split("\\R+"))); + if (StringUtils.equals(lines.get(0), "!!" + SynapseRegistrationYaml.class.getCanonicalName())) { + lines.remove(0); + } + + try (FileOutputStream os = new FileOutputStream(synapseRegFile)) { + IOUtils.writeLines(lines, System.lineSeparator(), os, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException("Unable to write synapse appservice registration file", e); + } + } + + private void ensureEnabled() { + if (!cfg.isEnabled()) { + throw new HttpMatrixException(503, "M_NOT_AVAILABLE", "This feature is disabled"); + } } public AppSvcManager withToken(String token) { + ensureEnabled(); + if (StringUtils.isBlank(token)) { throw new HttpMatrixException(401, "M_UNAUTHORIZED", "No HS token"); } - if (!StringUtils.equals(cfg.getListener().getToken().getHs(), token)) { + if (!StringUtils.equals(cfg.getEndpoint().getToAS().getToken(), token)) { throw new NotAllowedException("Invalid HS token"); } return this; } + public void processUser(String userId) { + client.createUser(MatrixID.asAcceptable(userId).getLocalPart()); + } + public CompletableFuture processTransaction(String txnId, InputStream is) { + ensureEnabled(); + if (StringUtils.isEmpty(txnId)) { throw new IllegalArgumentException("Transaction ID cannot be empty"); } synchronized (this) { - Optional dao = store.getTransactionResult(cfg.getListener().getLocalpart(), txnId); + Optional dao = store.getTransactionResult(cfg.getUser().getMain(), txnId); if (dao.isPresent()) { log.info("AS Transaction {} already processed - returning computed result", txnId); return CompletableFuture.completedFuture(dao.get().getResult()); @@ -122,7 +229,7 @@ public class AppSvcManager { try { log.info("Saving transaction details to store"); - store.insertTransactionResult(cfg.getListener().getLocalpart(), txnId, end, result); + store.insertTransactionResult(cfg.getUser().getMain(), txnId, end, result); } finally { log.debug("Removing CompletedFuture from transaction map"); transactionsInProgress.remove(txnId); @@ -139,7 +246,7 @@ public class AppSvcManager { return future; } - public void processTransaction(List eventsJson) { + private void processTransaction(List eventsJson) { log.info("Processing transaction events: start"); eventsJson.forEach(ev -> { @@ -165,54 +272,14 @@ public class AppSvcManager { _MatrixID sender = MatrixID.asAcceptable(senderId); log.debug("Sender: {}", senderId); - if (!StringUtils.equals("m.room.member", GsonUtil.getStringOrNull(ev, "type"))) { - log.debug("This is not a room membership event, skipping"); + String evType = StringUtils.defaultIfBlank(EventKey.Type.getStringOrNull(ev), ""); + EventTypeProcessor p = processors.get(evType); + if (Objects.isNull(p)) { + log.debug("No event processor for type {}, skipping", evType); return; } - if (!StringUtils.equals("invite", GsonUtil.getStringOrNull(ev, "membership"))) { - log.debug("This is not an invite event, skipping"); - return; - } - - String inviteeId = EventKey.StateKey.getStringOrNull(ev); - if (StringUtils.isBlank(inviteeId)) { - log.warn("Invalid event: No invitee ID, skipping"); - return; - } - - _MatrixID invitee = MatrixID.asAcceptable(inviteeId); - if (!StringUtils.equals(invitee.getDomain(), cfg.getDomain())) { - log.debug("Ignoring invite for {}: not a local user"); - return; - } - - log.info("Got invite from {} to {}", senderId, inviteeId); - - boolean wasSent = false; - List<_ThreePid> tpids = profiler.getThreepids(invitee).stream() - .filter(tpid -> ThreePidMedium.Email.is(tpid.getMedium())) - .collect(Collectors.toList()); - log.info("Found {} email(s) in identity store for {}", tpids.size(), inviteeId); - - for (_ThreePid tpid : tpids) { - log.info("Found Email to notify about room invitation: {}", tpid.getAddress()); - Map properties = new HashMap<>(); - profiler.getDisplayName(sender).ifPresent(name -> properties.put("sender_display_name", name)); - try { - synapse.getRoomName(roomId).ifPresent(name -> properties.put("room_name", name)); - } catch (RuntimeException e) { - log.warn("Could not fetch room name", e); - log.info("Unable to fetch room name: Did you integrate your Homeserver as documented?"); - } - - IMatrixIdInvite inv = new MatrixIdInvite(roomId, sender, invitee, tpid.getMedium(), tpid.getAddress(), properties); - notif.sendForInvite(inv); - log.info("Notification for invite of {} sent to {}", inviteeId, tpid.getAddress()); - wasSent = true; - } - - log.info("Was notification sent? {}", wasSent); + p.process(ev, sender, roomId); log.debug("Event {}: processing end", evId); }); diff --git a/src/main/java/io/kamax/mxisd/as/processor/command/CommandProcessor.java b/src/main/java/io/kamax/mxisd/as/processor/command/CommandProcessor.java new file mode 100644 index 0000000..bb9f9f5 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/as/processor/command/CommandProcessor.java @@ -0,0 +1,32 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sarl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.as.processor.command; + +import io.kamax.matrix.client._MatrixClient; +import io.kamax.matrix.hs._MatrixRoom; +import io.kamax.mxisd.Mxisd; +import org.apache.commons.cli.CommandLine; + +public interface CommandProcessor { + + void process(Mxisd m, _MatrixClient client, _MatrixRoom room, CommandLine cmdLine); + +} diff --git a/src/main/java/io/kamax/mxisd/as/processor/command/InviteCommandProcessor.java b/src/main/java/io/kamax/mxisd/as/processor/command/InviteCommandProcessor.java new file mode 100644 index 0000000..adc32f0 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/as/processor/command/InviteCommandProcessor.java @@ -0,0 +1,117 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sarl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.as.processor.command; + +import io.kamax.matrix.client._MatrixClient; +import io.kamax.matrix.hs._MatrixRoom; +import io.kamax.mxisd.Mxisd; +import io.kamax.mxisd.invitation.IThreePidInviteReply; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.text.StrBuilder; + +import java.util.List; + +public class InviteCommandProcessor implements CommandProcessor { + + public static final String Command = "invite"; + + @Override + public void process(Mxisd m, _MatrixClient client, _MatrixRoom room, CommandLine cmdLine) { + if (cmdLine.getArgs().length < 2) { + room.sendNotice(buildHelp()); + } else { + String arg = cmdLine.getArgList().get(1); + String response; + if (StringUtils.equals("list", arg)) { + + StrBuilder b = new StrBuilder(); + + List invites = m.getInvite().listInvites(); + if (invites.isEmpty()) { + b.appendln("No invites!"); + response = b.toString(); + } else { + b.appendln("Invites:"); + + + for (IThreePidInviteReply invite : invites) { + b.appendNewLine().append("ID: ").append(invite.getId()); + b.appendNewLine().append("Room: ").append(invite.getInvite().getRoomId()); + b.appendNewLine().append("Medium: ").append(invite.getInvite().getMedium()); + b.appendNewLine().append("Address: ").append(invite.getInvite().getAddress()); + b.appendNewLine(); + } + + response = b.appendNewLine().append("Total: " + invites.size()).toString(); + } + } else if (StringUtils.equals("show", arg)) { + if (cmdLine.getArgList().size() < 3) { + response = buildHelp(); + } else { + String id = cmdLine.getArgList().get(2); + IThreePidInviteReply invite = m.getInvite().getInvite(id); + StrBuilder b = new StrBuilder(); + b.appendln("Details for Invitation #" + id); + b.appendNewLine().append("Room: ").append(invite.getInvite().getRoomId()); + b.appendNewLine().append("Sender: ").append(invite.getInvite().getSender().toString()); + b.appendNewLine().append("Medium: ").append(invite.getInvite().getMedium()); + b.appendNewLine().append("Address: ").append(invite.getInvite().getAddress()); + b.appendNewLine().append("Display name: ").append(invite.getDisplayName()); + b.appendNewLine().appendNewLine().append("Properties:"); + invite.getInvite().getProperties().forEach((k, v) -> { + b.appendNewLine().append("\t").append(k).append("=").append(v); + }); + b.appendNewLine(); + + response = b.toString(); + } + } else if (StringUtils.equals("revoke", arg)) { + if (cmdLine.getArgList().size() < 3) { + response = buildHelp(); + } else { + m.getInvite().expireInvite(cmdLine.getArgList().get(2)); + response = "OK"; + } + } else { + response = buildError("Unknown invite action: " + arg, true); + } + + room.sendNotice(response); + } + } + + private String buildError(String message, boolean showHelp) { + if (showHelp) { + message = message + "\n\n" + buildHelp(); + } + + return message; + } + + private String buildHelp() { + return "Available actions:\n\n" + + "list - List invites\n" + + "show ID - Show detailed info about a specific invite\n" + + "revoke ID - Revoke a pending invite by resolving it to the configured Expiration user\n"; + } + +} diff --git a/src/main/java/io/kamax/mxisd/as/processor/command/LookupCommandProcessor.java b/src/main/java/io/kamax/mxisd/as/processor/command/LookupCommandProcessor.java new file mode 100644 index 0000000..40f148d --- /dev/null +++ b/src/main/java/io/kamax/mxisd/as/processor/command/LookupCommandProcessor.java @@ -0,0 +1,78 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sarl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.as.processor.command; + +import io.kamax.matrix.client._MatrixClient; +import io.kamax.matrix.hs._MatrixRoom; +import io.kamax.mxisd.Mxisd; +import io.kamax.mxisd.lookup.SingleLookupReply; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.lang.text.StrBuilder; +import org.apache.commons.lang3.StringUtils; + +import java.util.Optional; + +public class LookupCommandProcessor implements CommandProcessor { + + public static final String Command = "lookup"; + + @Override + public void process(Mxisd m, _MatrixClient client, _MatrixRoom room, CommandLine cmdLine) { + if (cmdLine.getArgList().size() != 3) { + room.sendNotice(getUsage()); + return; + } + + String medium = cmdLine.getArgList().get(1); + String address = cmdLine.getArgList().get(2); + if (StringUtils.isAnyBlank(medium, address)) { + room.sendNotice(getUsage()); + return; + } + + room.sendNotice("Processing..."); + Optional r = m.getIdentity().find(medium, address, true); + if (!r.isPresent()) { + room.sendNotice("No result"); + return; + } + + SingleLookupReply lookup = r.get(); + StrBuilder b = new StrBuilder(); + b.append("Result for 3PID lookup of ").append(medium).append(" ").appendln(address).appendNewLine(); + b.append("Matrix ID: ").appendln(lookup.getMxid().getId()); + b.appendln("Validity:") + .append(" Not Before: ").appendln(lookup.getNotBefore()) + .append(" Not After: ").appendln(lookup.getNotAfter()); + b.appendln("Signatures:"); + lookup.getSignatures().forEach((host, signs) -> { + b.append(" ").append(host).appendln(":"); + signs.forEach((key, sign) -> b.append(" ").append(key).append(" -> ").appendln("OK")); + }); + + room.sendNotice(b.toString()); + } + + public String getUsage() { + return "lookup MEDIUM ADDRESS"; + } + +} diff --git a/src/main/java/io/kamax/mxisd/as/processor/command/PingCommandProcessor.java b/src/main/java/io/kamax/mxisd/as/processor/command/PingCommandProcessor.java new file mode 100644 index 0000000..32f8364 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/as/processor/command/PingCommandProcessor.java @@ -0,0 +1,37 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sarl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.as.processor.command; + +import io.kamax.matrix.client._MatrixClient; +import io.kamax.matrix.hs._MatrixRoom; +import io.kamax.mxisd.Mxisd; +import org.apache.commons.cli.CommandLine; + +public class PingCommandProcessor implements CommandProcessor { + + public static final String Command = "ping"; + + @Override + public void process(Mxisd m, _MatrixClient client, _MatrixRoom room, CommandLine cmdLine) { + room.sendNotice("Pong!"); + } + +} diff --git a/src/main/java/io/kamax/mxisd/as/processor/event/EventTypeProcessor.java b/src/main/java/io/kamax/mxisd/as/processor/event/EventTypeProcessor.java new file mode 100644 index 0000000..a0a506a --- /dev/null +++ b/src/main/java/io/kamax/mxisd/as/processor/event/EventTypeProcessor.java @@ -0,0 +1,30 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sarl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.as.processor.event; + +import com.google.gson.JsonObject; +import io.kamax.matrix._MatrixID; + +public interface EventTypeProcessor { + + void process(JsonObject ev, _MatrixID sender, String roomId); + +} diff --git a/src/main/java/io/kamax/mxisd/as/processor/event/MembershipEventProcessor.java b/src/main/java/io/kamax/mxisd/as/processor/event/MembershipEventProcessor.java new file mode 100644 index 0000000..2f83fc9 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/as/processor/event/MembershipEventProcessor.java @@ -0,0 +1,172 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sarl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.as.processor.event; + +import com.google.gson.JsonObject; +import io.kamax.matrix.MatrixID; +import io.kamax.matrix.ThreePidMedium; +import io.kamax.matrix._MatrixID; +import io.kamax.matrix._ThreePid; +import io.kamax.matrix.client.as.MatrixApplicationServiceClient; +import io.kamax.matrix.event.EventKey; +import io.kamax.matrix.hs._MatrixRoom; +import io.kamax.mxisd.Mxisd; +import io.kamax.mxisd.backend.sql.synapse.Synapse; +import io.kamax.mxisd.config.MxisdConfig; +import io.kamax.mxisd.invitation.IMatrixIdInvite; +import io.kamax.mxisd.invitation.MatrixIdInvite; +import io.kamax.mxisd.notification.NotificationManager; +import io.kamax.mxisd.profile.ProfileManager; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class MembershipEventProcessor implements EventTypeProcessor { + + private final static Logger log = LoggerFactory.getLogger(MembershipEventProcessor.class); + + private MatrixApplicationServiceClient client; + + private final MxisdConfig cfg; + private ProfileManager profiler; + private NotificationManager notif; + private Synapse synapse; + + public MembershipEventProcessor( + MatrixApplicationServiceClient client, + Mxisd m + ) { + this.client = client; + this.cfg = m.getConfig(); + this.profiler = m.getProfile(); + this.notif = m.getNotif(); + this.synapse = m.getSynapse(); + } + + @Override + public void process(JsonObject ev, _MatrixID sender, String roomId) { + JsonObject content = EventKey.Content.findObj(ev).orElseGet(() -> { + log.debug("No content found, falling back to full object"); + return ev; + }); + + String targetId = EventKey.StateKey.getStringOrNull(ev); + if (StringUtils.isBlank(targetId)) { + log.warn("Invalid event: No invitee ID, skipping"); + return; + } + + _MatrixID target = MatrixID.asAcceptable(targetId); + if (!StringUtils.equals(target.getDomain(), cfg.getMatrix().getDomain())) { + log.debug("Ignoring invite for {}: not a local user"); + return; + } + + log.info("Got membership event from {} to {} for room {}", sender.getId(), targetId, roomId); + + boolean isForMainUser = StringUtils.equals(target.getLocalPart(), cfg.getAppsvc().getUser().getMain()); + boolean isForExpInvUser = StringUtils.equals(target.getLocalPart(), cfg.getAppsvc().getUser().getInviteExpired()); + boolean isUs = isForMainUser || isForExpInvUser; + + if (StringUtils.equals("join", EventKey.Membership.getStringOrNull(content))) { + if (!isForMainUser) { + log.warn("We joined the room {} for another identity as the main user, which is not supported. Leaving...", roomId); + + client.getUser(target.getLocalPart()).getRoom(roomId).tryLeave().ifPresent(err -> { + log.warn("Could not decline invite to room {}: {} - {}", roomId, err.getErrcode(), err.getError()); + }); + } + } else if (StringUtils.equals("invite", EventKey.Membership.getStringOrNull(content))) { + if (isForMainUser) { + processForMainUser(roomId, sender); + } else if (isForExpInvUser) { + processForExpiredInviteUser(roomId, target); + } else { + processForUserIdInvite(roomId, sender, target); + } + } else if (StringUtils.equals("leave", EventKey.Membership.getStringOrNull(content))) { + _MatrixRoom room = client.getRoom(roomId); + if (!isUs && room.getJoinedUsers().size() == 1) { + // TODO we need to find out if this is only us remaining and leave the room if so, using the right client for it + } + } else { + log.debug("This is not an supported type of membership event, skipping"); + } + } + + private void processForMainUser(String roomId, _MatrixID sender) { + boolean isAllowed = profiler.hasAnyRole(sender, cfg.getAppsvc().getFeature().getAdmin().getAllowedRoles()); + if (!isAllowed) { + log.info("Sender does not have any of the required roles, denying"); + client.getRoom(roomId).tryLeave().ifPresent(err -> { + log.warn("Could not decline invite to room {}: {} - {}", roomId, err.getErrcode(), err.getError()); + }); + } else { + client.getRoom(roomId).tryJoin().ifPresent(err -> { + log.warn("Could not join room {}: {} - {}", roomId, err.getErrcode(), err.getError()); + client.getRoom(roomId).tryLeave().ifPresent(err1 -> { + log.warn("Could not decline invite to room {} after failed join: {} - {}", roomId, err1.getErrcode(), err1.getError()); + }); + }); + } + } + + private void processForExpiredInviteUser(String roomId, _MatrixID invitee) { + client.getUser(invitee.getLocalPart()).getRoom(roomId).tryLeave().ifPresent(err -> { + log.warn("Could not decline invite to room {}: {} - {}", roomId, err.getErrcode(), err.getError()); + }); + } + + private void processForUserIdInvite(String roomId, _MatrixID sender, _MatrixID invitee) { + String inviteeId = invitee.getId(); + + boolean wasSent = false; + List<_ThreePid> tpids = profiler.getThreepids(invitee).stream() + .filter(tpid -> ThreePidMedium.Email.is(tpid.getMedium())) + .collect(Collectors.toList()); + log.info("Found {} email(s) in identity store for {}", tpids.size(), inviteeId); + + for (_ThreePid tpid : tpids) { + log.info("Found Email to notify about room invitation: {}", tpid.getAddress()); + Map properties = new HashMap<>(); + profiler.getDisplayName(sender).ifPresent(name -> properties.put("sender_display_name", name)); + try { + synapse.getRoomName(roomId).ifPresent(name -> properties.put("room_name", name)); + } catch (RuntimeException e) { + log.warn("Could not fetch room name", e); + log.info("Unable to fetch room name: Did you integrate your Homeserver as documented?"); + } + + IMatrixIdInvite inv = new MatrixIdInvite(roomId, sender, invitee, tpid.getMedium(), tpid.getAddress(), properties); + notif.sendForInvite(inv); + log.info("Notification for invite of {} sent to {}", inviteeId, tpid.getAddress()); + wasSent = true; + } + + log.info("Was notification sent? {}", wasSent); + } + +} diff --git a/src/main/java/io/kamax/mxisd/as/processor/event/MessageEventProcessor.java b/src/main/java/io/kamax/mxisd/as/processor/event/MessageEventProcessor.java new file mode 100644 index 0000000..06850dc --- /dev/null +++ b/src/main/java/io/kamax/mxisd/as/processor/event/MessageEventProcessor.java @@ -0,0 +1,127 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sarl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.as.processor.event; + +import com.google.gson.JsonObject; +import io.kamax.matrix._MatrixID; +import io.kamax.matrix._MatrixUserProfile; +import io.kamax.matrix.client.as.MatrixApplicationServiceClient; +import io.kamax.matrix.hs._MatrixRoom; +import io.kamax.matrix.json.event.MatrixJsonRoomMessageEvent; +import io.kamax.mxisd.Mxisd; +import io.kamax.mxisd.as.processor.command.CommandProcessor; +import io.kamax.mxisd.as.processor.command.InviteCommandProcessor; +import io.kamax.mxisd.as.processor.command.LookupCommandProcessor; +import io.kamax.mxisd.as.processor.command.PingCommandProcessor; +import org.apache.commons.cli.*; +import org.apache.commons.lang.text.StrBuilder; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public class MessageEventProcessor implements EventTypeProcessor { + + private static final Logger log = LoggerFactory.getLogger(MessageEventProcessor.class); + + private final Mxisd m; + private final MatrixApplicationServiceClient client; + private Map processors; + + public MessageEventProcessor(Mxisd m, MatrixApplicationServiceClient client) { + this.m = m; + this.client = client; + + processors = new HashMap<>(); + processors.put("?", (m1, client1, room, cmdLine) -> room.sendNotice(getHelp())); + processors.put("help", (m1, client1, room, cmdLine) -> room.sendNotice(getHelp())); + processors.put(PingCommandProcessor.Command, new PingCommandProcessor()); + processors.put(InviteCommandProcessor.Command, new InviteCommandProcessor()); + processors.put(LookupCommandProcessor.Command, new LookupCommandProcessor()); + } + + @Override + public void process(JsonObject ev, _MatrixID sender, String roomId) { + MatrixJsonRoomMessageEvent msgEv = new MatrixJsonRoomMessageEvent(ev); + if (StringUtils.equals("m.notice", msgEv.getBodyType())) { + log.info("Ignoring automated message"); + return; + } + + _MatrixRoom room = client.getRoom(roomId); + + if (!m.getProfile().hasAnyRole(sender, m.getConfig().getAppsvc().getFeature().getAdmin().getAllowedRoles())) { + room.sendNotice("You are not allowed to interact with me."); + return; + } + + List<_MatrixID> joinedUsers = room.getJoinedUsers().stream().map(_MatrixUserProfile::getId).collect(Collectors.toList()); + boolean joinedWithMainUser = joinedUsers.contains(client.getWhoAmI()); + boolean isAdminPrivate = joinedWithMainUser && joinedUsers.size() == 2; + + if (!StringUtils.equals("m.text", msgEv.getBodyType())) { + log.info("Unsupported message event type: {}", msgEv.getBodyType()); + return; + } + + String command = msgEv.getBody(); + if (!isAdminPrivate) { + if (!StringUtils.startsWith(command, "!" + Mxisd.Name + " ")) { + // Not for us + return; + } + + command = command.substring(("!" + Mxisd.Name + " ").length()); + } + + try { + CommandLineParser p = new DefaultParser(); + CommandLine cmdLine = p.parse(new Options(), command.split(" ", 0)); + String cmd = cmdLine.getArgList().get(0); + + CommandProcessor cp = processors.get(cmd); + if (Objects.isNull(cp)) { + room.sendNotice("Unknown command: " + command + "\n\n" + getHelp()); + } else { + cp.process(m, client, room, cmdLine); + } + } catch (ParseException e) { + room.sendNotice("Invalid input" + "\n\n" + getHelp()); + } catch (RuntimeException e) { + room.sendNotice("Error when running command: " + e.getMessage()); + } + } + + public String getHelp() { + StrBuilder builder = new StrBuilder(); + builder.appendln("Available commands:"); + for (String cmd : processors.keySet()) { + builder.append("\t").appendln(cmd); + } + return builder.toString(); + } + +} diff --git a/src/main/java/io/kamax/mxisd/as/registration/SynapseRegistrationYaml.java b/src/main/java/io/kamax/mxisd/as/registration/SynapseRegistrationYaml.java new file mode 100644 index 0000000..167333b --- /dev/null +++ b/src/main/java/io/kamax/mxisd/as/registration/SynapseRegistrationYaml.java @@ -0,0 +1,176 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sarl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.as.registration; + +import io.kamax.mxisd.config.AppServiceConfig; + +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class SynapseRegistrationYaml { + + public static SynapseRegistrationYaml parse(AppServiceConfig cfg, String domain) { + SynapseRegistrationYaml yaml = new SynapseRegistrationYaml(); + + yaml.setId(cfg.getRegistration().getSynapse().getId()); + yaml.setUrl(cfg.getEndpoint().getToAS().getUrl()); + yaml.setAsToken(cfg.getEndpoint().getToHS().getToken()); + yaml.setHsToken(cfg.getEndpoint().getToAS().getToken()); + yaml.setSenderLocalpart(cfg.getUser().getMain()); + + if (cfg.getFeature().getCleanExpiredInvite()) { + Namespace ns = new Namespace(); + ns.setExclusive(true); + ns.setRegex("@" + cfg.getUser().getInviteExpired() + ":" + domain); + yaml.getNamespaces().getUsers().add(ns); + } + + if (cfg.getFeature().getInviteById()) { + Namespace ns = new Namespace(); + ns.setExclusive(false); + ns.setRegex("@*:" + domain); + yaml.getNamespaces().getUsers().add(ns); + } + + return yaml; + } + + public static class Namespace { + + private String regex; + private boolean exclusive; + + public String getRegex() { + return regex; + } + + public void setRegex(String regex) { + this.regex = regex; + } + + public boolean isExclusive() { + return exclusive; + } + + public void setExclusive(boolean exclusive) { + this.exclusive = exclusive; + } + + } + + public static class Namespaces { + + private List users = new ArrayList<>(); + private List aliases = new ArrayList<>(); + private List rooms = new ArrayList<>(); + + public List getUsers() { + return users; + } + + public void setUsers(List users) { + this.users = users; + } + + public List getAliases() { + return aliases; + } + + public void setAliases(List aliases) { + this.aliases = aliases; + } + + public List getRooms() { + return rooms; + } + + public void setRooms(List rooms) { + this.rooms = rooms; + } + + } + + private String id; + private String url; + private String as_token; + private String hs_token; + private String sender_localpart; + private Namespaces namespaces = new Namespaces(); + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public void setUrl(URL url) { + if (Objects.isNull(url)) { + this.url = null; + } else { + this.url = url.toString(); + } + } + + public String getAsToken() { + return as_token; + } + + public void setAsToken(String as_token) { + this.as_token = as_token; + } + + public String getHsToken() { + return hs_token; + } + + public void setHsToken(String hs_token) { + this.hs_token = hs_token; + } + + public String getSenderLocalpart() { + return sender_localpart; + } + + public void setSenderLocalpart(String sender_localpart) { + this.sender_localpart = sender_localpart; + } + + public Namespaces getNamespaces() { + return namespaces; + } + + public void setNamespaces(Namespaces namespaces) { + this.namespaces = namespaces; + } + +} diff --git a/src/main/java/io/kamax/mxisd/backend/sql/SqlProfileProvider.java b/src/main/java/io/kamax/mxisd/backend/sql/SqlProfileProvider.java index bf208c1..1adaf52 100644 --- a/src/main/java/io/kamax/mxisd/backend/sql/SqlProfileProvider.java +++ b/src/main/java/io/kamax/mxisd/backend/sql/SqlProfileProvider.java @@ -23,7 +23,9 @@ package io.kamax.mxisd.backend.sql; import io.kamax.matrix.ThreePid; import io.kamax.matrix._MatrixID; import io.kamax.matrix._ThreePid; +import io.kamax.mxisd.UserIdType; import io.kamax.mxisd.config.sql.SqlConfig; +import io.kamax.mxisd.exception.InternalServerError; import io.kamax.mxisd.profile.ProfileProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,16 +35,14 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Optional; public abstract class SqlProfileProvider implements ProfileProvider { - private transient final Logger log = LoggerFactory.getLogger(SqlProfileProvider.class); + private static final Logger log = LoggerFactory.getLogger(SqlProfileProvider.class); private SqlConfig.Profile cfg; - private SqlConnectionPool pool; public SqlProfileProvider(SqlConfig cfg) { @@ -50,6 +50,12 @@ public abstract class SqlProfileProvider implements ProfileProvider { this.pool = new SqlConnectionPool(cfg); } + private void setParameters(PreparedStatement stmt, String value) throws SQLException { + for (int i = 1; i <= stmt.getParameterMetaData().getParameterCount(); i++) { + stmt.setString(i, value); + } + } + @Override public Optional getDisplayName(_MatrixID user) { String stmtSql = cfg.getDisplayName().getQuery(); @@ -94,7 +100,33 @@ public abstract class SqlProfileProvider implements ProfileProvider { @Override public List getRoles(_MatrixID user) { - return Collections.emptyList(); + log.info("Querying roles for {}", user.getId()); + + List roles = new ArrayList<>(); + + String stmtSql = cfg.getRole().getQuery(); + try (Connection conn = pool.get()) { + PreparedStatement stmt = conn.prepareStatement(stmtSql); + if (UserIdType.Localpart.is(cfg.getRole().getType())) { + setParameters(stmt, user.getLocalPart()); + } else if (UserIdType.MatrixID.is(cfg.getRole().getType())) { + setParameters(stmt, user.getId()); + } else { + throw new InternalServerError("Unsupported user type in SQL Role fetching: " + cfg.getRole().getType()); + } + + ResultSet rSet = stmt.executeQuery(); + while (rSet.next()) { + String role = rSet.getString(1); + roles.add(role); + log.debug("Found role {}", role); + } + + log.info("Got {} roles", roles.size()); + return roles; + } catch (SQLException e) { + throw new RuntimeException(e); + } } } diff --git a/src/main/java/io/kamax/mxisd/backend/sql/generic/GenericSqlStoreSupplier.java b/src/main/java/io/kamax/mxisd/backend/sql/generic/GenericSqlStoreSupplier.java index 4983369..21eb465 100644 --- a/src/main/java/io/kamax/mxisd/backend/sql/generic/GenericSqlStoreSupplier.java +++ b/src/main/java/io/kamax/mxisd/backend/sql/generic/GenericSqlStoreSupplier.java @@ -32,7 +32,7 @@ public class GenericSqlStoreSupplier implements IdentityStoreSupplier { @Override public void accept(Mxisd mxisd) { if (mxisd.getConfig().getSql().getAuth().isEnabled()) { - AuthProviders.register(() -> new GenericSqlAuthProvider(mxisd.getConfig().getSql(), mxisd.getInvitationManager())); + AuthProviders.register(() -> new GenericSqlAuthProvider(mxisd.getConfig().getSql(), mxisd.getInvite())); } if (mxisd.getConfig().getSql().getDirectory().isEnabled()) { diff --git a/src/main/java/io/kamax/mxisd/backend/sql/synapse/SynapseQueries.java b/src/main/java/io/kamax/mxisd/backend/sql/synapse/SynapseQueries.java index c775c66..40f621f 100644 --- a/src/main/java/io/kamax/mxisd/backend/sql/synapse/SynapseQueries.java +++ b/src/main/java/io/kamax/mxisd/backend/sql/synapse/SynapseQueries.java @@ -43,6 +43,10 @@ public class SynapseQueries { return "SELECT medium, address FROM user_threepids WHERE user_id = ?"; } + public static String getRoles() { + return "SELECT DISTINCT(group_id) FROM group_users WHERE user_id = ?"; + } + public static String findByDisplayName(String type, String domain) { if (StringUtils.equals("sqlite", type)) { return "select " + getUserId(type, domain) + ", displayname from profiles p where displayname like ?"; diff --git a/src/main/java/io/kamax/mxisd/backend/sql/synapse/SynapseSqlStoreSupplier.java b/src/main/java/io/kamax/mxisd/backend/sql/synapse/SynapseSqlStoreSupplier.java index 018885a..ef9a331 100644 --- a/src/main/java/io/kamax/mxisd/backend/sql/synapse/SynapseSqlStoreSupplier.java +++ b/src/main/java/io/kamax/mxisd/backend/sql/synapse/SynapseSqlStoreSupplier.java @@ -26,9 +26,13 @@ import io.kamax.mxisd.config.MxisdConfig; import io.kamax.mxisd.directory.DirectoryProviders; import io.kamax.mxisd.lookup.ThreePidProviders; import io.kamax.mxisd.profile.ProfileProviders; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class SynapseSqlStoreSupplier implements IdentityStoreSupplier { + private static final Logger log = LoggerFactory.getLogger(SynapseSqlStoreSupplier.class); + @Override public void accept(Mxisd mxisd) { accept(mxisd.getConfig()); @@ -44,6 +48,7 @@ public class SynapseSqlStoreSupplier implements IdentityStoreSupplier { } if (cfg.getSynapseSql().getProfile().isEnabled()) { + log.debug("Profile is enabled, registering provider"); ProfileProviders.register(() -> new SynapseSqlProfileProvider(cfg.getSynapseSql())); } } diff --git a/src/main/java/io/kamax/mxisd/config/AppServiceConfig.java b/src/main/java/io/kamax/mxisd/config/AppServiceConfig.java new file mode 100644 index 0000000..24f39d3 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/config/AppServiceConfig.java @@ -0,0 +1,287 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2018 Kamax Sarl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.config; + +import io.kamax.mxisd.Mxisd; +import io.kamax.mxisd.exception.ConfigurationException; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class AppServiceConfig { + + public static class Users { + + private String main = "mxisd"; + private String inviteExpired = "_mxisd_invite-expired"; + + public String getMain() { + return main; + } + + public void setMain(String main) { + this.main = main; + } + + public String getInviteExpired() { + return inviteExpired; + } + + public void setInviteExpired(String inviteExpired) { + this.inviteExpired = inviteExpired; + } + + public void build() { + // no-op + } + + } + + public static class Endpoint { + + private String url; + private String token; + + private transient URL cUrl; + + public URL getUrl() { + return cUrl; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public void build() { + if (Objects.isNull(url)) { + return; + } + + try { + cUrl = new URL(url); + } catch (MalformedURLException e) { + throw new ConfigurationException("AppService endpoint(s) URL definition"); + } + } + + } + + public static class Endpoints { + + private Endpoint toAS = new Endpoint(); + private Endpoint toHS = new Endpoint(); + + public Endpoint getToAS() { + return toAS; + } + + public void setToAS(Endpoint toAS) { + this.toAS = toAS; + } + + public Endpoint getToHS() { + return toHS; + } + + public void setToHS(Endpoint toHS) { + this.toHS = toHS; + } + + public void build() { + toAS.build(); + toHS.build(); + } + + } + + public static class Synapse { + + private String id = "appservice-" + Mxisd.Name; + private String file; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getFile() { + return file; + } + + public void setFile(String file) { + this.file = file; + } + + public void build() { + // no-op + } + + } + + public static class Registration { + + private Synapse synapse = new Synapse(); + + public Synapse getSynapse() { + return synapse; + } + + public void setSynapse(Synapse synapse) { + this.synapse = synapse; + } + + public void build() { + synapse.build(); + } + + } + + public static class AdminFeature { + + private Boolean enabled; + private List allowedRoles = new ArrayList<>(); + + public Boolean getEnabled() { + return enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public List getAllowedRoles() { + return allowedRoles; + } + + public void setAllowedRoles(List allowedRoles) { + this.allowedRoles = allowedRoles; + } + + public void build() { + // no-op + } + + } + + public static class Features { + + private AdminFeature admin = new AdminFeature(); + private Boolean inviteById; + private Boolean cleanExpiredInvite; + + public AdminFeature getAdmin() { + return admin; + } + + public void setAdmin(AdminFeature admin) { + this.admin = admin; + } + + public Boolean getInviteById() { + return inviteById; + } + + public void setInviteById(Boolean inviteById) { + this.inviteById = inviteById; + } + + public Boolean getCleanExpiredInvite() { + return cleanExpiredInvite; + } + + public void setCleanExpiredInvite(Boolean cleanExpiredInvite) { + this.cleanExpiredInvite = cleanExpiredInvite; + } + + public void build() { + admin.build(); + } + + } + + private Boolean enabled; + private Features feature = new Features(); + private Endpoints endpoint = new Endpoints(); + private Registration registration = new Registration(); + private Users user = new Users(); + + public Boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public Features getFeature() { + return feature; + } + + public void setFeature(Features feature) { + this.feature = feature; + } + + public Endpoints getEndpoint() { + return endpoint; + } + + public void setEndpoint(Endpoints endpoint) { + this.endpoint = endpoint; + } + + public Registration getRegistration() { + return registration; + } + + public void setRegistration(Registration registration) { + this.registration = registration; + } + + public Users getUser() { + return user; + } + + public void setUser(Users user) { + this.user = user; + } + + public void build() { + endpoint.build(); + feature.build(); + registration.build(); + user.build(); + } + +} diff --git a/src/main/java/io/kamax/mxisd/config/InvitationConfig.java b/src/main/java/io/kamax/mxisd/config/InvitationConfig.java index e814fd6..d0f5d8c 100644 --- a/src/main/java/io/kamax/mxisd/config/InvitationConfig.java +++ b/src/main/java/io/kamax/mxisd/config/InvitationConfig.java @@ -20,18 +20,53 @@ package io.kamax.mxisd.config; -import io.kamax.mxisd.util.GsonUtil; +import io.kamax.matrix.json.GsonUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.List; + public class InvitationConfig { - private transient final Logger log = LoggerFactory.getLogger(InvitationConfig.class); + private static final Logger log = LoggerFactory.getLogger(InvitationConfig.class); + + public static class Expiration { + + private Boolean enabled; + private long after = 60 * 24 * 7; // One calendar week (60min/1h * 24 = 1d * 7 = 1w) + private String resolveTo; + + public Boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public long getAfter() { + return after; + } + + public void setAfter(long after) { + this.after = after; + } + + public String getResolveTo() { + return resolveTo; + } + + public void setResolveTo(String resolveTo) { + this.resolveTo = resolveTo; + } + + } public static class Resolution { private boolean recursive = true; - private long timer = 1; + private long timer = 5; public boolean isRecursive() { return recursive; @@ -51,7 +86,43 @@ public class InvitationConfig { } + public static class SenderPolicy { + + private List hasRole = new ArrayList<>(); + + public List getHasRole() { + return hasRole; + } + + public void setHasRole(List hasRole) { + this.hasRole = hasRole; + } + } + + public static class Policies { + + private SenderPolicy ifSender = new SenderPolicy(); + + public SenderPolicy getIfSender() { + return ifSender; + } + + public void setIfSender(SenderPolicy ifSender) { + this.ifSender = ifSender; + } + } + + private Expiration expiration = new Expiration(); private Resolution resolution = new Resolution(); + private Policies policy = new Policies(); + + public Expiration getExpiration() { + return expiration; + } + + public void setExpiration(Expiration expiration) { + this.expiration = expiration; + } public Resolution getResolution() { return resolution; @@ -61,9 +132,19 @@ public class InvitationConfig { this.resolution = resolution; } + public Policies getPolicy() { + return policy; + } + + public void setPolicy(Policies policy) { + this.policy = policy; + } + public void build() { log.info("--- Invite config ---"); - log.info("Resolution: {}", GsonUtil.build().toJson(resolution)); + log.info("Expiration: {}", GsonUtil.get().toJson(getExpiration())); + log.info("Resolution: {}", GsonUtil.get().toJson(getResolution())); + log.info("Policies: {}", GsonUtil.get().toJson(getPolicy())); } } diff --git a/src/main/java/io/kamax/mxisd/config/ListenerConfig.java b/src/main/java/io/kamax/mxisd/config/ListenerConfig.java deleted file mode 100644 index 9a468b2..0000000 --- a/src/main/java/io/kamax/mxisd/config/ListenerConfig.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * mxisd - Matrix Identity Server Daemon - * Copyright (C) 2018 Kamax Sarl - * - * https://www.kamax.io/ - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package io.kamax.mxisd.config; - -import io.kamax.mxisd.exception.ConfigurationException; -import org.apache.commons.lang.StringUtils; - -import java.net.MalformedURLException; -import java.net.URL; - -public class ListenerConfig { - - public static class Token { - - private String as; - private String hs; - - public String getAs() { - return as; - } - - public void setAs(String as) { - this.as = as; - } - - public String getHs() { - return hs; - } - - public void setHs(String hs) { - this.hs = hs; - } - - } - - private transient URL csUrl; - private String url; - private String localpart; - private Token token = new Token(); - - public URL getUrl() { - return csUrl; - } - - public void setUrl(String url) { - this.url = url; - } - - public String getLocalpart() { - return localpart; - } - - public void setLocalpart(String localpart) { - this.localpart = localpart; - } - - public Token getToken() { - return token; - } - - public void setToken(Token token) { - this.token = token; - } - - public void build() { - try { - if (StringUtils.isBlank(url)) { - return; - } - - csUrl = new URL(url); - - if (StringUtils.isBlank(getLocalpart())) { - throw new IllegalArgumentException("localpart for matrix listener is not set"); - } - - if (StringUtils.isBlank(getToken().getAs())) { - throw new IllegalArgumentException("AS token is not set"); - } - - if (StringUtils.isBlank(getToken().getHs())) { - throw new IllegalArgumentException("HS token is not set"); - } - } catch (MalformedURLException e) { - throw new ConfigurationException(e); - } - } - -} diff --git a/src/main/java/io/kamax/mxisd/config/MatrixConfig.java b/src/main/java/io/kamax/mxisd/config/MatrixConfig.java index 0fe9db1..c66751a 100644 --- a/src/main/java/io/kamax/mxisd/config/MatrixConfig.java +++ b/src/main/java/io/kamax/mxisd/config/MatrixConfig.java @@ -63,7 +63,6 @@ public class MatrixConfig { private String domain; private Identity identity = new Identity(); - private ListenerConfig listener = new ListenerConfig(); public String getDomain() { return domain; @@ -81,14 +80,6 @@ public class MatrixConfig { this.identity = identity; } - public ListenerConfig getListener() { - return listener; - } - - public void setListener(ListenerConfig listener) { - this.listener = listener; - } - public void build() { log.info("--- Matrix config ---"); @@ -99,8 +90,6 @@ public class MatrixConfig { log.info("Domain: {}", getDomain()); log.info("Identity:"); log.info("\tServers: {}", GsonUtil.get().toJson(identity.getServers())); - - listener.build(); } } diff --git a/src/main/java/io/kamax/mxisd/config/MxisdConfig.java b/src/main/java/io/kamax/mxisd/config/MxisdConfig.java index 787171e..4a0dc4b 100644 --- a/src/main/java/io/kamax/mxisd/config/MxisdConfig.java +++ b/src/main/java/io/kamax/mxisd/config/MxisdConfig.java @@ -83,6 +83,7 @@ public class MxisdConfig { } + private AppServiceConfig appsvc = new AppServiceConfig(); private AuthenticationConfig auth = new AuthenticationConfig(); private DirectoryConfig directory = new DirectoryConfig(); private Dns dns = new Dns(); @@ -97,6 +98,7 @@ public class MxisdConfig { private MemoryStoreConfig memory = new MemoryStoreConfig(); private NotificationConfig notification = new NotificationConfig(); private NetIqLdapConfig netiq = new NetIqLdapConfig(); + private RegisterConfig register = new RegisterConfig(); private ServerConfig server = new ServerConfig(); private SessionConfig session = new SessionConfig(); private StorageConfig storage = new StorageConfig(); @@ -107,6 +109,14 @@ public class MxisdConfig { private ViewConfig view = new ViewConfig(); private WordpressConfig wordpress = new WordpressConfig(); + public AppServiceConfig getAppsvc() { + return appsvc; + } + + public void setAppsvc(AppServiceConfig appsvc) { + this.appsvc = appsvc; + } + public AuthenticationConfig getAuth() { return auth; } @@ -219,6 +229,14 @@ public class MxisdConfig { this.netiq = netiq; } + public RegisterConfig getRegister() { + return register; + } + + public void setRegister(RegisterConfig register) { + this.register = register; + } + public ServerConfig getServer() { return server; } @@ -297,6 +315,7 @@ public class MxisdConfig { log.debug("server.name is empty, using matrix.domain"); } + getAppsvc().build(); getAuth().build(); getDirectory().build(); getExec().build(); @@ -310,6 +329,7 @@ public class MxisdConfig { getMemory().build(); getNetiq().build(); getNotification().build(); + getRegister().build(); getRest().build(); getSession().build(); getServer().build(); diff --git a/src/main/java/io/kamax/mxisd/config/RegisterConfig.java b/src/main/java/io/kamax/mxisd/config/RegisterConfig.java new file mode 100644 index 0000000..515aab4 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/config/RegisterConfig.java @@ -0,0 +1,201 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sarl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.config; + +import io.kamax.matrix.ThreePidMedium; +import io.kamax.matrix.json.GsonUtil; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.stream.Collectors; + +public class RegisterConfig { + + private static final Logger log = LoggerFactory.getLogger(RegisterConfig.class); + + public static class ThreepidPolicyPattern { + + private List blacklist = new ArrayList<>(); + private List whitelist = new ArrayList<>(); + + public List getBlacklist() { + return blacklist; + } + + public void setBlacklist(List blacklist) { + this.blacklist = blacklist; + } + + public List getWhitelist() { + return whitelist; + } + + public void setWhitelist(List whitelist) { + this.whitelist = whitelist; + } + + } + + public static class EmailPolicy extends ThreepidPolicy { + + private ThreepidPolicyPattern domain = new ThreepidPolicyPattern(); + + public ThreepidPolicyPattern getDomain() { + return domain; + } + + public void setDomain(ThreepidPolicyPattern domain) { + this.domain = domain; + } + + private List buildPatterns(List domains) { + log.info("Building email policy"); + return domains.stream().map(d -> { + if (StringUtils.startsWith(d, "*")) { + log.info("Found domain and subdomain policy"); + d = "(.*)" + d.substring(1); + } else if (StringUtils.startsWith(d, ".")) { + log.info("Found subdomain-only policy"); + d = "(.*)" + d; + } else { + log.info("Found domain-only policy"); + } + + return "([^@]+)@" + d.replace(".", "\\."); + }).collect(Collectors.toList()); + } + + @Override + public void build() { + if (Objects.isNull(getDomain())) { + return; + } + + if (Objects.nonNull(getDomain().getBlacklist())) { + if (Objects.isNull(getPattern().getBlacklist())) { + getPattern().setBlacklist(new ArrayList<>()); + } + + List domains = buildPatterns(getDomain().getBlacklist()); + getPattern().getBlacklist().addAll(domains); + } + + if (Objects.nonNull(getDomain().getWhitelist())) { + if (Objects.isNull(getPattern().getWhitelist())) { + getPattern().setWhitelist(new ArrayList<>()); + } + + List domains = buildPatterns(getDomain().getWhitelist()); + getPattern().getWhitelist().addAll(domains); + } + + setDomain(null); + } + + } + + public static class ThreepidPolicy { + + private ThreepidPolicyPattern pattern = new ThreepidPolicyPattern(); + + public ThreepidPolicyPattern getPattern() { + return pattern; + } + + public void setPattern(ThreepidPolicyPattern pattern) { + this.pattern = pattern; + } + + public void build() { + // no-op + } + + } + + public static class Policy { + + private boolean allowed; + private boolean invite = true; + private Map threepid = new HashMap<>(); + + public boolean isAllowed() { + return allowed; + } + + public void setAllowed(boolean allowed) { + this.allowed = allowed; + } + + public boolean forInvite() { + return invite; + } + + public void setInvite(boolean invite) { + this.invite = invite; + } + + public Map getThreepid() { + return threepid; + } + + public void setThreepid(Map threepid) { + this.threepid = threepid; + } + + } + + private Policy policy = new Policy(); + + public Policy getPolicy() { + return policy; + } + + public void setPolicy(Policy policy) { + this.policy = policy; + } + + public void build() { + log.info("--- Registration config ---"); + + log.info("Before Build"); + log.info(GsonUtil.getPrettyForLog(this)); + + new HashMap<>(getPolicy().getThreepid()).forEach((medium, policy) -> { + if (ThreePidMedium.Email.is(medium)) { + EmailPolicy pPolicy = GsonUtil.get().fromJson(GsonUtil.get().toJson(policy), EmailPolicy.class); + pPolicy.build(); + policy = GsonUtil.makeObj(pPolicy); + } else { + ThreepidPolicy pPolicy = GsonUtil.get().fromJson(GsonUtil.get().toJson(policy), ThreepidPolicy.class); + pPolicy.build(); + policy = GsonUtil.makeObj(pPolicy); + } + + getPolicy().getThreepid().put(medium, policy); + }); + + log.info("After Build"); + log.info(GsonUtil.getPrettyForLog(this)); + } + +} diff --git a/src/main/java/io/kamax/mxisd/config/sql/SqlConfig.java b/src/main/java/io/kamax/mxisd/config/sql/SqlConfig.java index e30f5ff..6eb5ed3 100644 --- a/src/main/java/io/kamax/mxisd/config/sql/SqlConfig.java +++ b/src/main/java/io/kamax/mxisd/config/sql/SqlConfig.java @@ -193,11 +193,35 @@ public abstract class SqlConfig { } + public static class ProfileRoles { + + private String type; + private String query; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getQuery() { + return query; + } + + public void setQuery(String query) { + this.query = query; + } + + } + public static class Profile { private Boolean enabled; private ProfileDisplayName displayName = new ProfileDisplayName(); private ProfileThreepids threepid = new ProfileThreepids(); + private ProfileRoles role = new ProfileRoles(); public Boolean isEnabled() { return enabled; @@ -223,6 +247,14 @@ public abstract class SqlConfig { this.threepid = threepid; } + public ProfileRoles getRole() { + return role; + } + + public void setRole(ProfileRoles role) { + this.role = role; + } + } private boolean enabled; @@ -323,10 +355,11 @@ public abstract class SqlConfig { log.info("3PID mapping query: {}", getIdentity().getQuery()); log.info("Identity medium queries: {}", GsonUtil.build().toJson(getIdentity().getMedium())); log.info("Profile:"); - log.info("\tEnabled: {}", getProfile().isEnabled()); + log.info(" Enabled: {}", getProfile().isEnabled()); if (getProfile().isEnabled()) { - log.info("\tDisplay name query: {}", getProfile().getDisplayName().getQuery()); - log.info("\tProfile 3PID query: {}", getProfile().getThreepid().getQuery()); + log.info(" Display name query: {}", getProfile().getDisplayName().getQuery()); + log.info(" Profile 3PID query: {}", getProfile().getThreepid().getQuery()); + log.info(" Role query: {}", getProfile().getRole().getQuery()); } } } diff --git a/src/main/java/io/kamax/mxisd/config/sql/synapse/SynapseSqlProviderConfig.java b/src/main/java/io/kamax/mxisd/config/sql/synapse/SynapseSqlProviderConfig.java index c6f3281..747a18e 100644 --- a/src/main/java/io/kamax/mxisd/config/sql/synapse/SynapseSqlProviderConfig.java +++ b/src/main/java/io/kamax/mxisd/config/sql/synapse/SynapseSqlProviderConfig.java @@ -20,6 +20,7 @@ package io.kamax.mxisd.config.sql.synapse; +import io.kamax.mxisd.UserIdType; import io.kamax.mxisd.backend.sql.synapse.SynapseQueries; import io.kamax.mxisd.config.sql.SqlConfig; import org.apache.commons.lang.StringUtils; @@ -48,9 +49,17 @@ public class SynapseSqlProviderConfig extends SqlConfig { if (StringUtils.isBlank(getProfile().getDisplayName().getQuery())) { getProfile().getDisplayName().setQuery(SynapseQueries.getDisplayName()); } + if (StringUtils.isBlank(getProfile().getThreepid().getQuery())) { getProfile().getThreepid().setQuery(SynapseQueries.getThreepids()); } + + if (StringUtils.isBlank(getProfile().getRole().getType())) { + getProfile().getRole().setType(UserIdType.MatrixID.getId()); + } + if (StringUtils.isBlank(getProfile().getRole().getQuery())) { + getProfile().getRole().setQuery(SynapseQueries.getRoles()); + } } printConfig(); diff --git a/src/main/java/io/kamax/mxisd/crypto/CryptoFactory.java b/src/main/java/io/kamax/mxisd/crypto/CryptoFactory.java index d52c233..253e978 100644 --- a/src/main/java/io/kamax/mxisd/crypto/CryptoFactory.java +++ b/src/main/java/io/kamax/mxisd/crypto/CryptoFactory.java @@ -20,9 +20,12 @@ package io.kamax.mxisd.crypto; -import io.kamax.matrix.crypto.*; import io.kamax.mxisd.config.KeyConfig; -import io.kamax.mxisd.config.ServerConfig; +import io.kamax.mxisd.crypto.ed25519.Ed25519KeyManager; +import io.kamax.mxisd.crypto.ed25519.Ed25519SignatureManager; +import io.kamax.mxisd.storage.crypto.FileKeyStore; +import io.kamax.mxisd.storage.crypto.KeyStore; +import io.kamax.mxisd.storage.crypto.MemoryKeyStore; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; @@ -31,10 +34,10 @@ import java.io.IOException; public class CryptoFactory { - public static KeyManager getKeyManager(KeyConfig keyCfg) { - _KeyStore store; + public static Ed25519KeyManager getKeyManager(KeyConfig keyCfg) { + KeyStore store; if (StringUtils.equals(":memory:", keyCfg.getPath())) { - store = new KeyMemoryStore(); + store = new MemoryKeyStore(); } else { File keyStore = new File(keyCfg.getPath()); if (!keyStore.exists()) { @@ -45,14 +48,14 @@ public class CryptoFactory { } } - store = new KeyFileStore(keyCfg.getPath()); + store = new FileKeyStore(keyCfg.getPath()); } - return new KeyManager(store); + return new Ed25519KeyManager(store); } - public static SignatureManager getSignatureManager(KeyManager keyMgr, ServerConfig cfg) { - return new SignatureManager(keyMgr, cfg.getName()); + public static SignatureManager getSignatureManager(Ed25519KeyManager keyMgr) { + return new Ed25519SignatureManager(keyMgr); } } diff --git a/src/main/java/io/kamax/mxisd/crypto/GenericKey.java b/src/main/java/io/kamax/mxisd/crypto/GenericKey.java new file mode 100644 index 0000000..685ac2d --- /dev/null +++ b/src/main/java/io/kamax/mxisd/crypto/GenericKey.java @@ -0,0 +1,51 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.crypto; + +public class GenericKey implements Key { + + private final KeyIdentifier id; + private final boolean isValid; + private final String privKey; + + public GenericKey(KeyIdentifier id, boolean isValid, String privKey) { + this.id = new GenericKeyIdentifier(id); + this.isValid = isValid; + this.privKey = privKey; + } + + + @Override + public KeyIdentifier getId() { + return id; + } + + @Override + public boolean isValid() { + return isValid; + } + + @Override + public String getPrivateKeyBase64() { + return privKey; + } + +} diff --git a/src/main/java/io/kamax/mxisd/crypto/GenericKeyIdentifier.java b/src/main/java/io/kamax/mxisd/crypto/GenericKeyIdentifier.java new file mode 100644 index 0000000..cc9c0f0 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/crypto/GenericKeyIdentifier.java @@ -0,0 +1,76 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.crypto; + +import org.apache.commons.lang3.StringUtils; + +import java.util.Objects; + +public class GenericKeyIdentifier implements KeyIdentifier { + + private final KeyType type; + private final String algo; + private final String serial; + + public GenericKeyIdentifier(KeyIdentifier id) { + this(id.getType(), id.getAlgorithm(), id.getSerial()); + } + + public GenericKeyIdentifier(KeyType type, String algo, String serial) { + if (StringUtils.isAnyBlank(algo, serial)) { + throw new IllegalArgumentException("Algorithm and/or Serial cannot be blank"); + } + + this.type = Objects.requireNonNull(type); + this.algo = algo; + this.serial = serial; + } + + @Override + public KeyType getType() { + return type; + } + + @Override + public String getAlgorithm() { + return algo; + } + + @Override + public String getSerial() { + return serial; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof GenericKeyIdentifier)) return false; + GenericKeyIdentifier that = (GenericKeyIdentifier) o; + return type == that.type && + algo.equals(that.algo) && + serial.equals(that.serial); + } + + @Override + public int hashCode() { + return Objects.hash(type, algo, serial); + } +} diff --git a/src/main/java/io/kamax/mxisd/crypto/Key.java b/src/main/java/io/kamax/mxisd/crypto/Key.java new file mode 100644 index 0000000..628e237 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/crypto/Key.java @@ -0,0 +1,44 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.crypto; + +/** + * A signing key + */ +public interface Key { + + KeyIdentifier getId(); + + /** + * If the key is currently valid + * + * @return true if the key is valid, false if not + */ + boolean isValid(); + + /** + * Get the private key + * + * @return the private key encoded as Base64 + */ + String getPrivateKeyBase64(); + +} diff --git a/src/main/java/io/kamax/mxisd/crypto/KeyAlgorithm.java b/src/main/java/io/kamax/mxisd/crypto/KeyAlgorithm.java new file mode 100644 index 0000000..4e63d35 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/crypto/KeyAlgorithm.java @@ -0,0 +1,27 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.crypto; + +public interface KeyAlgorithm { + + String Ed25519 = "ed25519"; + +} diff --git a/src/main/java/io/kamax/mxisd/crypto/KeyIdentifier.java b/src/main/java/io/kamax/mxisd/crypto/KeyIdentifier.java new file mode 100644 index 0000000..1954d06 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/crypto/KeyIdentifier.java @@ -0,0 +1,54 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.crypto; + +/** + * Identifying data for a given Key. + */ +public interface KeyIdentifier { + + /** + * Type of key. + * + * @return The type of the key + */ + KeyType getType(); + + /** + * Algorithm of the key. Typically ed25519. + * + * @return The algorithm of the key + */ + String getAlgorithm(); + + /** + * Serial of the key, unique for the algorithm. + * It is typically made of random alphanumerical characters. + * + * @return The serial of the key + */ + String getSerial(); + + default String getId() { + return getAlgorithm().toLowerCase() + ":" + getSerial(); + } + +} diff --git a/src/main/java/io/kamax/mxisd/crypto/KeyManager.java b/src/main/java/io/kamax/mxisd/crypto/KeyManager.java new file mode 100644 index 0000000..a36f70b --- /dev/null +++ b/src/main/java/io/kamax/mxisd/crypto/KeyManager.java @@ -0,0 +1,41 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.crypto; + +import java.util.List; + +public interface KeyManager { + + KeyIdentifier generateKey(KeyType type); + + List getKeys(KeyType type); + + Key getServerSigningKey(); + + Key getKey(KeyIdentifier id); + + void disableKey(KeyIdentifier id); + + String getPublicKeyBase64(KeyIdentifier id); + + boolean isValid(KeyType type, String publicKeyBase64); + +} diff --git a/src/main/java/io/kamax/mxisd/crypto/KeyType.java b/src/main/java/io/kamax/mxisd/crypto/KeyType.java new file mode 100644 index 0000000..2b63ba2 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/crypto/KeyType.java @@ -0,0 +1,39 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.crypto; + +/** + * Types of keys used by an Identity server. + * See https://matrix.org/docs/spec/identity_service/r0.1.0.html#key-management + */ +public enum KeyType { + + /** + * Ephemeral keys are related to 3PID invites and are only valid while the invite is pending. + */ + Ephemeral, + + /** + * Regular keys are used by the Identity Server itself to sign requests/responses + */ + Regular + +} diff --git a/src/main/java/io/kamax/mxisd/crypto/RegularKeyIdentifier.java b/src/main/java/io/kamax/mxisd/crypto/RegularKeyIdentifier.java new file mode 100644 index 0000000..3990587 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/crypto/RegularKeyIdentifier.java @@ -0,0 +1,29 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.crypto; + +public class RegularKeyIdentifier extends GenericKeyIdentifier { + + public RegularKeyIdentifier(String algo, String serial) { + super(KeyType.Regular, algo, serial); + } + +} diff --git a/src/main/java/io/kamax/mxisd/crypto/Signature.java b/src/main/java/io/kamax/mxisd/crypto/Signature.java new file mode 100644 index 0000000..08d5887 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/crypto/Signature.java @@ -0,0 +1,29 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.crypto; + +public interface Signature { + + KeyIdentifier getKey(); + + String getSignature(); + +} diff --git a/src/main/java/io/kamax/mxisd/crypto/SignatureManager.java b/src/main/java/io/kamax/mxisd/crypto/SignatureManager.java new file mode 100644 index 0000000..bcfdcfa --- /dev/null +++ b/src/main/java/io/kamax/mxisd/crypto/SignatureManager.java @@ -0,0 +1,64 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.crypto; + +import com.google.gson.JsonObject; + +import java.nio.charset.StandardCharsets; + +public interface SignatureManager { + + /** + * Sign the message and produce a signatures object that can directly be added to the object being signed. + * + * @param domain The domain under which the signature should be added + * @param message The message to sign + * @return The signatures object + */ + JsonObject signMessageGson(String domain, String message); + + /** + * Sign the canonical form of a JSON object. + * + * @param obj The JSON object to canonicalize and sign + * @return The signature + */ + Signature sign(JsonObject obj); + + /** + * Sign the message, using UTF-8 as decoding character set. + * + * @param message The UTF-8 encoded message + * @return The signature + */ + default Signature sign(String message) { + return sign(message.getBytes(StandardCharsets.UTF_8)); + } + + /** + * Sign the data. + * + * @param data The data to sign + * @return The signature + */ + Signature sign(byte[] data); + +} diff --git a/src/main/java/io/kamax/mxisd/crypto/ed25519/Ed25519Key.java b/src/main/java/io/kamax/mxisd/crypto/ed25519/Ed25519Key.java new file mode 100644 index 0000000..1416101 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/crypto/ed25519/Ed25519Key.java @@ -0,0 +1,58 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.crypto.ed25519; + +import io.kamax.mxisd.crypto.GenericKeyIdentifier; +import io.kamax.mxisd.crypto.Key; +import io.kamax.mxisd.crypto.KeyAlgorithm; +import io.kamax.mxisd.crypto.KeyIdentifier; + +public class Ed25519Key implements Key { + + private KeyIdentifier id; + private String privKey; + + public Ed25519Key(KeyIdentifier id, String privKey) { + if (!KeyAlgorithm.Ed25519.equals(id.getAlgorithm())) { + throw new IllegalArgumentException(); + } + + this.id = new GenericKeyIdentifier(id); + this.privKey = privKey; + } + + + @Override + public KeyIdentifier getId() { + return id; + } + + @Override + public boolean isValid() { + return true; + } + + @Override + public String getPrivateKeyBase64() { + return privKey; + } + +} diff --git a/src/main/java/io/kamax/mxisd/crypto/ed25519/Ed25519KeyManager.java b/src/main/java/io/kamax/mxisd/crypto/ed25519/Ed25519KeyManager.java new file mode 100644 index 0000000..ac64475 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/crypto/ed25519/Ed25519KeyManager.java @@ -0,0 +1,148 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.crypto.ed25519; + +import io.kamax.matrix.codec.MxBase64; +import io.kamax.mxisd.crypto.*; +import io.kamax.mxisd.storage.crypto.KeyStore; +import net.i2p.crypto.eddsa.EdDSAPrivateKey; +import net.i2p.crypto.eddsa.EdDSAPublicKey; +import net.i2p.crypto.eddsa.KeyPairGenerator; +import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable; +import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec; +import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec; +import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.security.KeyPair; +import java.time.Instant; +import java.util.List; +import java.util.stream.Collectors; + +public class Ed25519KeyManager implements KeyManager { + + private static final Logger log = LoggerFactory.getLogger(Ed25519KeyManager.class); + + private final EdDSAParameterSpec keySpecs; + private final KeyStore store; + + public Ed25519KeyManager(KeyStore store) { + this.keySpecs = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.CURVE_ED25519_SHA512); + this.store = store; + + if (!store.getCurrentKey().isPresent()) { + List keys = store.list(KeyType.Regular).stream() + .map(this::getKey) + .filter(Key::isValid) + .map(Key::getId) + .collect(Collectors.toList()); + + if (keys.isEmpty()) { + keys.add(generateKey(KeyType.Regular)); + } + + store.setCurrentKey(keys.get(0)); + } + } + + private String generateId() { + ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES); + buffer.putLong(Instant.now().toEpochMilli() - 1546297200000L); // TS since 2019-01-01T00:00:00Z to keep IDs short + return Base64.encodeBase64URLSafeString(buffer.array()) + RandomStringUtils.randomAlphanumeric(1); + } + + private String getPrivateKeyBase64(EdDSAPrivateKey key) { + return MxBase64.encode(key.getSeed()); + } + + EdDSAParameterSpec getKeySpecs() { + return keySpecs; + } + + @Override + public KeyIdentifier generateKey(KeyType type) { + KeyIdentifier id; + do { + id = new GenericKeyIdentifier(type, KeyAlgorithm.Ed25519, generateId()); + } while (store.has(id)); + + KeyPair pair = (new KeyPairGenerator()).generateKeyPair(); + String keyEncoded = getPrivateKeyBase64((EdDSAPrivateKey) pair.getPrivate()); + + Key key = new GenericKey(id, true, keyEncoded); + store.add(key); + + return id; + } + + @Override + public List getKeys(KeyType type) { + return store.list(type); + } + + @Override + public Key getServerSigningKey() { + return store.get(store.getCurrentKey().orElseThrow(IllegalStateException::new)); + } + + @Override + public Key getKey(KeyIdentifier id) { + return store.get(id); + } + + private EdDSAPrivateKeySpec getPrivateKeySpecs(KeyIdentifier id) { + return new EdDSAPrivateKeySpec(Base64.decodeBase64(getKey(id).getPrivateKeyBase64()), keySpecs); + } + + EdDSAPrivateKey getPrivateKey(KeyIdentifier id) { + return new EdDSAPrivateKey(getPrivateKeySpecs(id)); + } + + private EdDSAPublicKey getPublicKey(KeyIdentifier id) { + EdDSAPrivateKeySpec privKeySpec = getPrivateKeySpecs(id); + EdDSAPublicKeySpec pubKeySpec = new EdDSAPublicKeySpec(privKeySpec.getA(), keySpecs); + return new EdDSAPublicKey(pubKeySpec); + } + + @Override + public void disableKey(KeyIdentifier id) { + Key key = store.get(id); + key = new GenericKey(id, false, key.getPrivateKeyBase64()); + store.update(key); + } + + @Override + public String getPublicKeyBase64(KeyIdentifier id) { + return MxBase64.encode(getPublicKey(id).getAbyte()); + } + + @Override + public boolean isValid(KeyType type, String publicKeyBase64) { + // TODO caching? + return getKeys(type).stream().anyMatch(id -> StringUtils.equals(getPublicKeyBase64(id), publicKeyBase64)); + } + +} diff --git a/src/main/java/io/kamax/mxisd/crypto/ed25519/Ed25519RegularKeyIdentifier.java b/src/main/java/io/kamax/mxisd/crypto/ed25519/Ed25519RegularKeyIdentifier.java new file mode 100644 index 0000000..e0d5856 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/crypto/ed25519/Ed25519RegularKeyIdentifier.java @@ -0,0 +1,32 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.crypto.ed25519; + +import io.kamax.mxisd.crypto.KeyAlgorithm; +import io.kamax.mxisd.crypto.RegularKeyIdentifier; + +public class Ed25519RegularKeyIdentifier extends RegularKeyIdentifier { + + public Ed25519RegularKeyIdentifier(String serial) { + super(KeyAlgorithm.Ed25519, serial); + } + +} diff --git a/src/main/java/io/kamax/mxisd/crypto/ed25519/Ed25519SignatureManager.java b/src/main/java/io/kamax/mxisd/crypto/ed25519/Ed25519SignatureManager.java new file mode 100644 index 0000000..dab5a99 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/crypto/ed25519/Ed25519SignatureManager.java @@ -0,0 +1,86 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.crypto.ed25519; + +import com.google.gson.JsonObject; +import io.kamax.matrix.codec.MxBase64; +import io.kamax.matrix.json.MatrixJson; +import io.kamax.mxisd.crypto.KeyIdentifier; +import io.kamax.mxisd.crypto.Signature; +import io.kamax.mxisd.crypto.SignatureManager; +import net.i2p.crypto.eddsa.EdDSAEngine; + +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SignatureException; + +public class Ed25519SignatureManager implements SignatureManager { + + private final Ed25519KeyManager keyMgr; + + public Ed25519SignatureManager(Ed25519KeyManager keyMgr) { + this.keyMgr = keyMgr; + } + + @Override + public JsonObject signMessageGson(String domain, String message) { + Signature sign = sign(message); + + JsonObject keySignature = new JsonObject(); + keySignature.addProperty(sign.getKey().getAlgorithm() + ":" + sign.getKey().getSerial(), sign.getSignature()); + JsonObject signature = new JsonObject(); + signature.add(domain, keySignature); + + return signature; + } + + @Override + public Signature sign(JsonObject obj) { + return sign(MatrixJson.encodeCanonical(obj)); + } + + @Override + public Signature sign(byte[] data) { + try { + KeyIdentifier signingKeyId = keyMgr.getServerSigningKey().getId(); + EdDSAEngine signEngine = new EdDSAEngine(MessageDigest.getInstance(keyMgr.getKeySpecs().getHashAlgorithm())); + signEngine.initSign(keyMgr.getPrivateKey(signingKeyId)); + byte[] signRaw = signEngine.signOneShot(data); + String sign = MxBase64.encode(signRaw); + + return new Signature() { + @Override + public KeyIdentifier getKey() { + return signingKeyId; + } + + @Override + public String getSignature() { + return sign; + } + }; + } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/main/java/io/kamax/mxisd/exception/ObjectNotFoundException.java b/src/main/java/io/kamax/mxisd/exception/ObjectNotFoundException.java index 81f79f0..3111470 100644 --- a/src/main/java/io/kamax/mxisd/exception/ObjectNotFoundException.java +++ b/src/main/java/io/kamax/mxisd/exception/ObjectNotFoundException.java @@ -22,8 +22,12 @@ package io.kamax.mxisd.exception; public class ObjectNotFoundException extends RuntimeException { + public ObjectNotFoundException(String message) { + super(message); + } + public ObjectNotFoundException(String type, String id) { - super(type + " with ID " + id + " does not exist"); + this(type + " with ID " + id + " does not exist"); } } diff --git a/src/main/java/io/kamax/mxisd/http/io/identity/SingeLookupReplyJson.java b/src/main/java/io/kamax/mxisd/http/io/identity/SingeLookupReplyJson.java index 32c1d91..e1e662c 100644 --- a/src/main/java/io/kamax/mxisd/http/io/identity/SingeLookupReplyJson.java +++ b/src/main/java/io/kamax/mxisd/http/io/identity/SingeLookupReplyJson.java @@ -22,6 +22,9 @@ package io.kamax.mxisd.http.io.identity; import io.kamax.mxisd.lookup.SingleLookupReply; +import java.util.HashMap; +import java.util.Map; + public class SingeLookupReplyJson { private String address; @@ -30,6 +33,7 @@ public class SingeLookupReplyJson { private long not_after; private long not_before; private long ts; + private Map> signatures = new HashMap<>(); public SingeLookupReplyJson(SingleLookupReply reply) { this.address = reply.getRequest().getThreePid(); @@ -64,4 +68,8 @@ public class SingeLookupReplyJson { return ts; } + public Map> getSignatures() { + return signatures; + } + } diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/BasicHttpHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/BasicHttpHandler.java index 0b21a19..fa059f8 100644 --- a/src/main/java/io/kamax/mxisd/http/undertow/handler/BasicHttpHandler.java +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/BasicHttpHandler.java @@ -23,20 +23,29 @@ package io.kamax.mxisd.http.undertow.handler; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import io.kamax.matrix.json.GsonUtil; +import io.kamax.mxisd.dns.ClientDnsOverwrite; +import io.kamax.mxisd.exception.AccessTokenNotFoundException; import io.kamax.mxisd.exception.HttpMatrixException; import io.kamax.mxisd.exception.InternalServerError; import io.kamax.mxisd.proxy.Response; +import io.kamax.mxisd.util.RestClientUtils; import io.undertow.server.HttpHandler; import io.undertow.server.HttpServerExchange; import io.undertow.util.HttpString; import org.apache.commons.io.IOUtils; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.Header; +import org.apache.http.HeaderElement; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.InetSocketAddress; +import java.net.URI; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.Deque; @@ -46,7 +55,19 @@ import java.util.Optional; public abstract class BasicHttpHandler implements HttpHandler { - private transient final Logger log = LoggerFactory.getLogger(BasicHttpHandler.class); + private static final Logger log = LoggerFactory.getLogger(BasicHttpHandler.class); + + protected String getAccessToken(HttpServerExchange exchange) { + return Optional.ofNullable(exchange.getRequestHeaders().getFirst("Authorization")) + .flatMap(v -> { + if (!v.startsWith("Bearer ")) { + return Optional.empty(); + } + + return Optional.of(v.substring("Bearer ".length())); + }).filter(StringUtils::isNotEmpty) + .orElseThrow(AccessTokenNotFoundException::new); + } protected String getRemoteHostAddress(HttpServerExchange exchange) { return ((InetSocketAddress) exchange.getConnection().getPeerAddress()).getAddress().getHostAddress(); @@ -149,4 +170,34 @@ public abstract class BasicHttpHandler implements HttpHandler { upstream.getHeaders().forEach((key, value) -> exchange.getResponseHeaders().addAll(HttpString.tryFromString(key), value)); writeBodyAsUtf8(exchange, upstream.getBody()); } + + protected void proxyPost(HttpServerExchange exchange, JsonObject body, CloseableHttpClient client, ClientDnsOverwrite dns) { + String target = dns.transform(URI.create(exchange.getRequestURL())).toString(); + log.info("Requesting remote: {}", target); + HttpPost req = RestClientUtils.post(target, GsonUtil.get(), body); + + exchange.getRequestHeaders().forEach(header -> { + header.forEach(v -> { + String name = header.getHeaderName().toString(); + if (!StringUtils.startsWithIgnoreCase(name, "content-")) { + req.addHeader(name, v); + } + }); + }); + + try (CloseableHttpResponse res = client.execute(req)) { + exchange.setStatusCode(res.getStatusLine().getStatusCode()); + for (Header h : res.getAllHeaders()) { + for (HeaderElement el : h.getElements()) { + exchange.getResponseHeaders().add(HttpString.tryFromString(h.getName()), el.getValue()); + } + } + res.getEntity().writeTo(exchange.getOutputStream()); + exchange.endExchange(); + } catch (IOException e) { + log.warn("Unable to make proxy call: {}", e.getMessage(), e); + throw new InternalServerError(e); + } + } + } diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/InternalInfoHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/InternalInfoHandler.java new file mode 100644 index 0000000..973e1f3 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/InternalInfoHandler.java @@ -0,0 +1,50 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sarl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.http.undertow.handler; + +import io.undertow.server.HttpServerExchange; + +import java.util.concurrent.ThreadLocalRandom; + +public class InternalInfoHandler extends BasicHttpHandler { + + /* + * This endpoint should never be called as being entierly custom as per instructions of New Vector, + * the author of that endpoint. + * + * Used for the first time at https://github.com/matrix-org/synapse/pull/4681/files#diff-a73c645c44a17da6ab70f256da6b60afR41 + * + * Full context: https://matrix.to/#/!YkZelGRiqijtzXZODa:matrix.org/$15510967621328WMKVu:kamax.io?via=matrix.org + * Room name: #matrix-spec + * Room alias: #matrix-spec:matrix.org + */ + public static final String Path = "/_matrix/identity/api/{version}/internal-info"; + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + // We will return a random status code in all possible error codes + int type = ThreadLocalRandom.current().nextInt(4, 6) * 100; // Random 4 or 5, times 100 + int status = type + ThreadLocalRandom.current().nextInt(0, 100); // Random 0 to 99 + + respond(exchange, status, "M_FORBIDDEN", "This endpoint is under quarantine and possibly wrongfully labeled stable."); + } + +} diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/as/v1/AsUserHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/as/v1/AsUserHandler.java new file mode 100644 index 0000000..a37af1f --- /dev/null +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/as/v1/AsUserHandler.java @@ -0,0 +1,46 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sarl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.http.undertow.handler.as.v1; + +import io.kamax.mxisd.as.AppSvcManager; +import io.undertow.server.HttpServerExchange; + +import java.util.LinkedList; + +public class AsUserHandler extends ApplicationServiceHandler { + + public static final String ID = "userId"; + public static final String Path = "/_matrix/app/v1/users/{" + ID + "}"; + + private final AppSvcManager app; + + public AsUserHandler(AppSvcManager app) { + this.app = app; + } + + @Override + public void handleRequest(HttpServerExchange exchange) { + String userId = exchange.getQueryParameters().getOrDefault(ID, new LinkedList<>()).peekFirst(); + app.withToken(getToken(exchange)).processUser(userId); + respondJson(exchange, "{}"); + } + +} diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/EphemeralKeyIsValidHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/EphemeralKeyIsValidHandler.java index eaa802c..0f70013 100644 --- a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/EphemeralKeyIsValidHandler.java +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/EphemeralKeyIsValidHandler.java @@ -20,6 +20,8 @@ package io.kamax.mxisd.http.undertow.handler.identity.v1; +import io.kamax.mxisd.crypto.KeyManager; +import io.kamax.mxisd.crypto.KeyType; import io.kamax.mxisd.http.IsAPIv1; import io.undertow.server.HttpServerExchange; import org.slf4j.Logger; @@ -31,11 +33,19 @@ public class EphemeralKeyIsValidHandler extends KeyIsValidHandler { private transient final Logger log = LoggerFactory.getLogger(EphemeralKeyIsValidHandler.class); + private KeyManager mgr; + + public EphemeralKeyIsValidHandler(KeyManager mgr) { + this.mgr = mgr; + } + @Override public void handleRequest(HttpServerExchange exchange) { - log.warn("Ephemeral key was requested but no ephemeral key are generated, replying not valid"); + // FIXME process + correctly in query parameter handling + String pubKey = getQueryParameter(exchange, "public_key").replace(" ", "+"); + log.info("Validating ephemeral public key {}", pubKey); - respondJson(exchange, invalidKey); + respondJson(exchange, mgr.isValid(KeyType.Ephemeral, pubKey) ? validKey : invalidKey); } } diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/KeyGetHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/KeyGetHandler.java index 1dc8228..8b1de81 100644 --- a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/KeyGetHandler.java +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/KeyGetHandler.java @@ -21,18 +21,20 @@ package io.kamax.mxisd.http.undertow.handler.identity.v1; import com.google.gson.JsonObject; -import io.kamax.matrix.crypto.KeyManager; -import io.kamax.mxisd.exception.BadRequestException; +import io.kamax.mxisd.crypto.GenericKeyIdentifier; +import io.kamax.mxisd.crypto.KeyManager; +import io.kamax.mxisd.crypto.KeyType; import io.kamax.mxisd.http.IsAPIv1; import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; import io.undertow.server.HttpServerExchange; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class KeyGetHandler extends BasicHttpHandler { public static final String Key = "key"; - public static final String Path = IsAPIv1.Base + "/pubkey/{key}"; + public static final String Path = IsAPIv1.Base + "/pubkey/{" + Key + "}"; private transient final Logger log = LoggerFactory.getLogger(KeyGetHandler.class); @@ -45,17 +47,17 @@ public class KeyGetHandler extends BasicHttpHandler { @Override public void handleRequest(HttpServerExchange exchange) { String key = getQueryParameter(exchange, Key); - String[] v = key.split(":", 2); - String keyType = v[0]; - int keyId = Integer.parseInt(v[1]); - - if (!"ed25519".contentEquals(keyType)) { - throw new BadRequestException("Invalid algorithm: " + keyType); + if (StringUtils.isBlank(key)) { + throw new IllegalArgumentException("Key ID cannot be empty or blank"); } - log.info("Key {}:{} was requested", keyType, keyId); + String[] v = key.split(":", 2); // Maybe use regex? + String keyAlgo = v[0]; + String keyId = v[1]; + + log.info("Key {}:{} was requested", keyAlgo, keyId); JsonObject obj = new JsonObject(); - obj.addProperty("public_key", mgr.getPublicKeyBase64(keyId)); + obj.addProperty("public_key", mgr.getPublicKeyBase64(new GenericKeyIdentifier(KeyType.Regular, keyAlgo, keyId))); respond(exchange, obj); } diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/RegularKeyIsValidHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/RegularKeyIsValidHandler.java index 171c2b0..6b5167c 100644 --- a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/RegularKeyIsValidHandler.java +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/RegularKeyIsValidHandler.java @@ -20,10 +20,10 @@ package io.kamax.mxisd.http.undertow.handler.identity.v1; -import io.kamax.matrix.crypto.KeyManager; +import io.kamax.mxisd.crypto.KeyManager; +import io.kamax.mxisd.crypto.KeyType; import io.kamax.mxisd.http.IsAPIv1; import io.undertow.server.HttpServerExchange; -import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,12 +41,11 @@ public class RegularKeyIsValidHandler extends KeyIsValidHandler { @Override public void handleRequest(HttpServerExchange exchange) { - String pubKey = getQueryParameter(exchange, "public_key"); + // FIXME process + correctly in query parameter handling + String pubKey = getQueryParameter(exchange, "public_key").replace(" ", "+"); log.info("Validating public key {}", pubKey); - // TODO do in manager - boolean valid = StringUtils.equals(pubKey, mgr.getPublicKeyBase64(mgr.getCurrentIndex())); - respondJson(exchange, valid ? validKey : invalidKey); + respondJson(exchange, mgr.isValid(KeyType.Regular, pubKey) ? validKey : invalidKey); } } diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/SignEd25519Handler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/SignEd25519Handler.java new file mode 100644 index 0000000..8d6dc7e --- /dev/null +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/SignEd25519Handler.java @@ -0,0 +1,75 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sarl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.http.undertow.handler.identity.v1; + +import com.google.gson.JsonObject; +import io.kamax.matrix.MatrixID; +import io.kamax.matrix._MatrixID; +import io.kamax.matrix.json.GsonUtil; +import io.kamax.matrix.json.MatrixJson; +import io.kamax.mxisd.config.MxisdConfig; +import io.kamax.mxisd.crypto.SignatureManager; +import io.kamax.mxisd.http.IsAPIv1; +import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; +import io.kamax.mxisd.invitation.IThreePidInviteReply; +import io.kamax.mxisd.invitation.InvitationManager; +import io.undertow.server.HttpServerExchange; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SignEd25519Handler extends BasicHttpHandler { + + public static final String Path = IsAPIv1.Base + "/sign-ed25519"; + + private static final Logger log = LoggerFactory.getLogger(SignEd25519Handler.class); + + private final MxisdConfig cfg; + private final InvitationManager invMgr; + private final SignatureManager signMgr; + + public SignEd25519Handler(MxisdConfig cfg, InvitationManager invMgr, SignatureManager signMgr) { + this.cfg = cfg; + this.invMgr = invMgr; + this.signMgr = signMgr; + } + + @Override + public void handleRequest(HttpServerExchange exchange) { + JsonObject body = parseJsonObject(exchange); + + _MatrixID mxid = MatrixID.asAcceptable(GsonUtil.getStringOrThrow(body, "mxid")); + String token = GsonUtil.getStringOrThrow(body, "token"); + String privKey = GsonUtil.getStringOrThrow(body, "private_key"); + + IThreePidInviteReply reply = invMgr.getInvite(token, privKey); + _MatrixID sender = reply.getInvite().getSender(); + + JsonObject res = new JsonObject(); + res.addProperty("token", token); + res.addProperty("sender", sender.getId()); + res.addProperty("mxid", mxid.getId()); + res.add("signatures", signMgr.signMessageGson(cfg.getServer().getName(), MatrixJson.encodeCanonical(res))); + + log.info("Signed data for invite using token {}", token); + respondJson(exchange, res); + } + +} diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/SingleLookupHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/SingleLookupHandler.java index e341057..d81d38c 100644 --- a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/SingleLookupHandler.java +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/SingleLookupHandler.java @@ -21,10 +21,12 @@ package io.kamax.mxisd.http.undertow.handler.identity.v1; import com.google.gson.JsonObject; -import io.kamax.matrix.crypto.SignatureManager; import io.kamax.matrix.event.EventKey; import io.kamax.matrix.json.GsonUtil; import io.kamax.matrix.json.MatrixJson; +import io.kamax.mxisd.config.MxisdConfig; +import io.kamax.mxisd.config.ServerConfig; +import io.kamax.mxisd.crypto.SignatureManager; import io.kamax.mxisd.http.IsAPIv1; import io.kamax.mxisd.http.io.identity.SingeLookupReplyJson; import io.kamax.mxisd.lookup.SingleLookupReply; @@ -42,10 +44,12 @@ public class SingleLookupHandler extends LookupHandler { private transient final Logger log = LoggerFactory.getLogger(SingleLookupHandler.class); + private ServerConfig cfg; private LookupStrategy strategy; private SignatureManager signMgr; - public SingleLookupHandler(LookupStrategy strategy, SignatureManager signMgr) { + public SingleLookupHandler(MxisdConfig cfg, LookupStrategy strategy, SignatureManager signMgr) { + this.cfg = cfg.getServer(); this.strategy = strategy; this.signMgr = signMgr; } @@ -72,7 +76,7 @@ public class SingleLookupHandler extends LookupHandler { // FIXME signing should be done in the business model, not in the controller JsonObject obj = GsonUtil.makeObj(new SingeLookupReplyJson(lookup)); - obj.add(EventKey.Signatures.get(), signMgr.signMessageGson(MatrixJson.encodeCanonical(obj))); + obj.add(EventKey.Signatures.get(), signMgr.signMessageGson(cfg.getName(), MatrixJson.encodeCanonical(obj))); respondJson(exchange, obj); } diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/StoreInviteHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/StoreInviteHandler.java index 7131a19..11996a0 100644 --- a/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/StoreInviteHandler.java +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/identity/v1/StoreInviteHandler.java @@ -24,9 +24,9 @@ import com.google.gson.JsonObject; import com.google.gson.reflect.TypeToken; import io.kamax.matrix.MatrixID; import io.kamax.matrix._MatrixID; -import io.kamax.matrix.crypto.KeyManager; import io.kamax.matrix.json.GsonUtil; import io.kamax.mxisd.config.ServerConfig; +import io.kamax.mxisd.crypto.KeyManager; import io.kamax.mxisd.exception.BadRequestException; import io.kamax.mxisd.http.IsAPIv1; import io.kamax.mxisd.http.io.identity.StoreInviteRequest; @@ -96,7 +96,8 @@ public class StoreInviteHandler extends BasicHttpHandler { IThreePidInvite invite = new ThreePidInvite(sender, inv.getMedium(), inv.getAddress(), inv.getRoomId(), parameters); IThreePidInviteReply reply = invMgr.storeInvite(invite); - respondJson(exchange, new ThreePidInviteReplyIO(reply, keyMgr.getPublicKeyBase64(keyMgr.getCurrentIndex()), cfg.getPublicUrl())); + // FIXME the key info must be set by the invitation manager in the reply object! + respondJson(exchange, new ThreePidInviteReplyIO(reply, keyMgr.getPublicKeyBase64(keyMgr.getServerSigningKey().getId()), cfg.getPublicUrl())); } } diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/invite/v1/RoomInviteHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/invite/v1/RoomInviteHandler.java new file mode 100644 index 0000000..20e3420 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/invite/v1/RoomInviteHandler.java @@ -0,0 +1,103 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sarl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.http.undertow.handler.invite.v1; + +import com.google.gson.JsonObject; +import io.kamax.matrix.MatrixID; +import io.kamax.matrix._MatrixID; +import io.kamax.matrix.json.GsonUtil; +import io.kamax.mxisd.dns.ClientDnsOverwrite; +import io.kamax.mxisd.exception.InternalServerError; +import io.kamax.mxisd.exception.NotAllowedException; +import io.kamax.mxisd.exception.RemoteHomeServerException; +import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; +import io.kamax.mxisd.invitation.InvitationManager; +import io.undertow.server.HttpServerExchange; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; +import java.util.Optional; + +public class RoomInviteHandler extends BasicHttpHandler { + + public static final String Path = "/_matrix/client/r0/rooms/{roomId}/invite"; + + private static final Logger log = LoggerFactory.getLogger(RoomInviteHandler.class); + + private final CloseableHttpClient client; + private final ClientDnsOverwrite dns; + private final InvitationManager invMgr; + + public RoomInviteHandler(CloseableHttpClient client, ClientDnsOverwrite dns, InvitationManager invMgr) { + this.client = client; + this.dns = dns; + this.invMgr = invMgr; + } + + @Override + public void handleRequest(HttpServerExchange exchange) { + String accessToken = getAccessToken(exchange); + + String whoamiUri = dns.transform(URI.create(exchange.getRequestURL()).resolve(URI.create("/_matrix/client/r0/account/whoami"))).toString(); + log.info("Who Am I URL: {}", whoamiUri); + HttpGet whoAmIReq = new HttpGet(whoamiUri); + whoAmIReq.addHeader("Authorization", "Bearer " + accessToken); + _MatrixID uId; + try (CloseableHttpResponse whoAmIRes = client.execute(whoAmIReq)) { + int sc = whoAmIRes.getStatusLine().getStatusCode(); + String body = EntityUtils.toString(whoAmIRes.getEntity()); + + if (sc != 200) { + log.warn("Unable to get caller identity from Homeserver - Status code: {}", sc); + log.debug("Body: {}", body); + throw new RemoteHomeServerException(body); + } + + JsonObject json = GsonUtil.parseObj(body); + Optional uIdRaw = GsonUtil.findString(json, "user_id"); + if (!uIdRaw.isPresent()) { + throw new RemoteHomeServerException("No User ID provided when checking identity"); + } + + uId = MatrixID.asAcceptable(uIdRaw.get()); + } catch (IOException e) { + InternalServerError ex = new InternalServerError(e); + log.error("Ref {}: Unable to fetch caller identity from Homeserver", ex.getReference()); + throw ex; + } + + log.info("Processing room invite from {}", uId.getId()); + JsonObject reqBody = parseJsonObject(exchange); + if (!invMgr.canInvite(uId, reqBody)) { + throw new NotAllowedException("Your account is not allowed to invite that address"); + } + + log.info("Invite was allowing, relaying to the Homeserver"); + proxyPost(exchange, reqBody, client, dns); + } + +} diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/register/v1/Register3pidRequestTokenHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/register/v1/Register3pidRequestTokenHandler.java new file mode 100644 index 0000000..ed9919c --- /dev/null +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/register/v1/Register3pidRequestTokenHandler.java @@ -0,0 +1,77 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sarl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.http.undertow.handler.register.v1; + +import com.google.gson.JsonObject; +import io.kamax.matrix.ThreePid; +import io.kamax.matrix.ThreePidMedium; +import io.kamax.matrix.json.GsonUtil; +import io.kamax.mxisd.dns.ClientDnsOverwrite; +import io.kamax.mxisd.exception.NotAllowedException; +import io.kamax.mxisd.http.io.identity.SessionEmailTokenRequestJson; +import io.kamax.mxisd.http.io.identity.SessionPhoneTokenRequestJson; +import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; +import io.kamax.mxisd.registration.RegistrationManager; +import io.undertow.server.HttpServerExchange; +import org.apache.http.impl.client.CloseableHttpClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Register3pidRequestTokenHandler extends BasicHttpHandler { + + public static final String Key = "medium"; + public static final String Path = "/_matrix/client/r0/register/{" + Key + "}/requestToken"; + + private static final Logger log = LoggerFactory.getLogger(Register3pidRequestTokenHandler.class); + + private final RegistrationManager mgr; + private final ClientDnsOverwrite dns; + private final CloseableHttpClient client; + + public Register3pidRequestTokenHandler(RegistrationManager mgr, ClientDnsOverwrite dns, CloseableHttpClient client) { + this.mgr = mgr; + this.dns = dns; // FIXME this shouldn't be in here but in the manager + this.client = client; // FIXME this shouldn't be in here but in the manager + } + + @Override + public void handleRequest(HttpServerExchange exchange) { + JsonObject body = parseJsonObject(exchange); + + String medium = getPathVariable(exchange, Key); + String address = GsonUtil.findString(body, "address").orElse(""); + if (ThreePidMedium.Email.is(medium)) { + address = GsonUtil.get().fromJson(body, SessionEmailTokenRequestJson.class).getValue(); + } else if (ThreePidMedium.PhoneNumber.is(medium)) { + address = GsonUtil.get().fromJson(body, SessionPhoneTokenRequestJson.class).getValue(); + } else { + log.warn("Unsupported 3PID medium. We attempted to extract the address but the call might fail"); + } + + ThreePid tpid = new ThreePid(medium, address); + if (!mgr.isAllowed(tpid)) { + throw new NotAllowedException("Your " + medium + " address cannot be used for registration"); + } + + proxyPost(exchange, body, client, dns); + } + +} diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/status/StatusHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/status/StatusHandler.java index a5dfa0e..4183081 100644 --- a/src/main/java/io/kamax/mxisd/http/undertow/handler/status/StatusHandler.java +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/status/StatusHandler.java @@ -26,7 +26,7 @@ import io.undertow.server.HttpServerExchange; public class StatusHandler extends BasicHttpHandler { - public static final String Path = "/_matrix/identity/status"; + public static final String Path = "/status"; @Override public void handleRequest(HttpServerExchange exchange) { diff --git a/src/main/java/io/kamax/mxisd/http/undertow/handler/status/VersionHandler.java b/src/main/java/io/kamax/mxisd/http/undertow/handler/status/VersionHandler.java new file mode 100644 index 0000000..ab4cf6a --- /dev/null +++ b/src/main/java/io/kamax/mxisd/http/undertow/handler/status/VersionHandler.java @@ -0,0 +1,48 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sarl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.http.undertow.handler.status; + +import com.google.gson.JsonObject; +import io.kamax.matrix.json.GsonUtil; +import io.kamax.mxisd.Mxisd; +import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; +import io.undertow.server.HttpServerExchange; + +public class VersionHandler extends BasicHttpHandler { + + public static final String Path = "/version"; + + private final String body; + + public VersionHandler() { + JsonObject server = new JsonObject(); + server.addProperty("name", Mxisd.Name); + server.addProperty("version", Mxisd.Version); + + body = GsonUtil.getPrettyForLog(GsonUtil.makeObj("server", server)); + } + + @Override + public void handleRequest(HttpServerExchange exchange) { + respondJson(exchange, body); + } + +} diff --git a/src/main/java/io/kamax/mxisd/as/IMatrixIdInvite.java b/src/main/java/io/kamax/mxisd/invitation/IMatrixIdInvite.java similarity index 92% rename from src/main/java/io/kamax/mxisd/as/IMatrixIdInvite.java rename to src/main/java/io/kamax/mxisd/invitation/IMatrixIdInvite.java index 626025d..91e19aa 100644 --- a/src/main/java/io/kamax/mxisd/as/IMatrixIdInvite.java +++ b/src/main/java/io/kamax/mxisd/invitation/IMatrixIdInvite.java @@ -18,10 +18,9 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.as; +package io.kamax.mxisd.invitation; import io.kamax.matrix._MatrixID; -import io.kamax.mxisd.invitation.IThreePidInvite; public interface IMatrixIdInvite extends IThreePidInvite { diff --git a/src/main/java/io/kamax/mxisd/invitation/IThreePidInviteReply.java b/src/main/java/io/kamax/mxisd/invitation/IThreePidInviteReply.java index b1b561b..00057e6 100644 --- a/src/main/java/io/kamax/mxisd/invitation/IThreePidInviteReply.java +++ b/src/main/java/io/kamax/mxisd/invitation/IThreePidInviteReply.java @@ -20,6 +20,8 @@ package io.kamax.mxisd.invitation; +import java.util.List; + public interface IThreePidInviteReply { String getId(); @@ -30,4 +32,6 @@ public interface IThreePidInviteReply { String getDisplayName(); + List getPublicKeys(); + } diff --git a/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java b/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java index 6cc8fb4..13bd6f7 100644 --- a/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java +++ b/src/main/java/io/kamax/mxisd/invitation/InvitationManager.java @@ -23,21 +23,29 @@ package io.kamax.mxisd.invitation; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import io.kamax.matrix.MatrixID; -import io.kamax.matrix.crypto.SignatureManager; +import io.kamax.matrix.ThreePid; +import io.kamax.matrix._MatrixID; import io.kamax.matrix.json.GsonUtil; import io.kamax.mxisd.config.InvitationConfig; +import io.kamax.mxisd.config.MxisdConfig; +import io.kamax.mxisd.config.ServerConfig; +import io.kamax.mxisd.crypto.*; import io.kamax.mxisd.dns.FederationDnsOverwrite; import io.kamax.mxisd.exception.BadRequestException; +import io.kamax.mxisd.exception.ConfigurationException; import io.kamax.mxisd.exception.MappingAlreadyExistsException; +import io.kamax.mxisd.exception.ObjectNotFoundException; import io.kamax.mxisd.lookup.SingleLookupReply; import io.kamax.mxisd.lookup.ThreePidMapping; import io.kamax.mxisd.lookup.strategy.LookupStrategy; import io.kamax.mxisd.notification.NotificationManager; +import io.kamax.mxisd.profile.ProfileManager; import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.ormlite.dao.ThreePidInviteIO; +import org.apache.commons.codec.binary.Base64; import org.apache.commons.io.IOUtils; -import org.apache.commons.lang.RandomStringUtils; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.conn.ssl.NoopHostnameVerifier; @@ -57,6 +65,8 @@ import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.time.DateTimeException; +import java.time.Instant; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ForkJoinPool; @@ -64,14 +74,20 @@ import java.util.concurrent.TimeUnit; public class InvitationManager { - private transient final Logger log = LoggerFactory.getLogger(InvitationManager.class); + private static final Logger log = LoggerFactory.getLogger(InvitationManager.class); + private static final String CreatedAtPropertyKey = "created_at"; + + private final String defaultCreateTs = Long.toString(Instant.now().toEpochMilli()); private InvitationConfig cfg; + private ServerConfig srvCfg; private IStorage storage; private LookupStrategy lookupMgr; + private KeyManager keyMgr; private SignatureManager signMgr; private FederationDnsOverwrite dns; private NotificationManager notifMgr; + private ProfileManager profileMgr; private CloseableHttpClient client; private Timer refreshTimer; @@ -79,23 +95,29 @@ public class InvitationManager { private Map invitations = new ConcurrentHashMap<>(); public InvitationManager( - InvitationConfig cfg, + MxisdConfig mxisdCfg, IStorage storage, LookupStrategy lookupMgr, + KeyManager keyMgr, SignatureManager signMgr, FederationDnsOverwrite dns, - NotificationManager notifMgr + NotificationManager notifMgr, + ProfileManager profileMgr ) { - this.cfg = cfg; + this.cfg = requireValid(mxisdCfg); + this.srvCfg = mxisdCfg.getServer(); this.storage = storage; this.lookupMgr = lookupMgr; + this.keyMgr = keyMgr; this.signMgr = signMgr; this.dns = dns; this.notifMgr = notifMgr; + this.profileMgr = profileMgr; log.info("Loading saved invites"); Collection ioList = storage.getInvites(); ioList.forEach(io -> { + io.getProperties().putIfAbsent(CreatedAtPropertyKey, defaultCreateTs); log.info("Processing invite {}", GsonUtil.get().toJson(io)); ThreePidInvite invite = new ThreePidInvite( MatrixID.asAcceptable(io.getSender()), @@ -105,7 +127,7 @@ public class InvitationManager { io.getProperties() ); - ThreePidInviteReply reply = new ThreePidInviteReply(getId(invite), invite, io.getToken(), ""); + ThreePidInviteReply reply = new ThreePidInviteReply(io.getId(), invite, io.getToken(), "", Collections.emptyList()); invitations.put(reply.getId(), reply); }); @@ -122,25 +144,63 @@ public class InvitationManager { log.info("Setting up invitation mapping refresh timer"); refreshTimer = new Timer(); - refreshTimer.scheduleAtFixedRate(new TimerTask() { - @Override - public void run() { - try { - lookupMappingsForInvites(); - } catch (Throwable t) { - log.error("Error when running background mapping refresh", t); - } - } - }, 5000L, TimeUnit.MILLISECONDS.convert(cfg.getResolution().getTimer(), TimeUnit.MINUTES)); + // We add a shutdown hook to cancel the hook and wait for pending resolutions Runtime.getRuntime().addShutdownHook(new Thread(() -> { refreshTimer.cancel(); ForkJoinPool.commonPool().awaitQuiescence(1, TimeUnit.MINUTES); })); + + // We set the refresh timer for background tasks + refreshTimer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + try { + doMaintenance(); + } catch (Throwable t) { + log.error("Error when running background maintenance", t); + } + } + }, 5000L, TimeUnit.MILLISECONDS.convert(cfg.getResolution().getTimer(), TimeUnit.MINUTES)); } - private String getId(IThreePidInvite invite) { - return invite.getSender().getDomain().toLowerCase() + invite.getMedium().toLowerCase() + invite.getAddress().toLowerCase(); + private InvitationConfig requireValid(MxisdConfig cfg) { + // This is not configured, we'll apply a default configuration + if (Objects.isNull(cfg.getInvite().getExpiration().isEnabled())) { + // We compute our own user, so it can be used if we bridge as well + String mxId = MatrixID.asAcceptable("_mxisd-expired_invite", cfg.getMatrix().getDomain()).getId(); + + // Enabled by default + cfg.getInvite().getExpiration().setEnabled(true); + } + + if (cfg.getInvite().getExpiration().isEnabled()) { + if (cfg.getInvite().getExpiration().getAfter() < 1) { + throw new ConfigurationException("Invitation expiration delay must be greater or equal to 1"); + } + + if (StringUtils.isBlank(cfg.getInvite().getExpiration().getResolveTo())) { + String localpart = cfg.getAppsvc().getUser().getInviteExpired(); + if (StringUtils.isBlank(localpart)) { + throw new ConfigurationException("Could not compute the Invitation expiration resolution target from App service user: not set"); + } + + cfg.getInvite().getExpiration().setResolveTo(MatrixID.asAcceptable(localpart, cfg.getMatrix().getDomain()).getId()); + } + + try { + MatrixID.asAcceptable(cfg.getInvite().getExpiration().getResolveTo()); + } catch (IllegalArgumentException e) { + throw new ConfigurationException("Invitation expiration resolution target is not a valid Matrix ID: " + e.getMessage()); + } + } + + return cfg.getInvite(); + } + + private String computeId(IThreePidInvite invite) { + String rawId = invite.getSender().getDomain().toLowerCase() + invite.getMedium().toLowerCase() + invite.getAddress().toLowerCase(); + return Base64.encodeBase64URLSafeString(rawId.getBytes(StandardCharsets.UTF_8)); } private String getIdForLog(IThreePidInviteReply reply) { @@ -205,19 +265,56 @@ public class InvitationManager { return lookupMgr.find(medium, address, cfg.getResolution().isRecursive()); } + public List listInvites() { + return new ArrayList<>(invitations.values()); + } + + public IThreePidInviteReply getInvite(String id) { + IThreePidInviteReply v = invitations.get(id); + if (Objects.isNull(v)) { + throw new ObjectNotFoundException("Invite", id); + } + + return v; + } + + public boolean canInvite(_MatrixID sender, JsonObject request) { + if (!request.has("medium")) { + log.info("Not a 3PID invite, allowing"); + return true; + } + log.info("3PID invite detected, checking policies..."); + + List allowedRoles = cfg.getPolicy().getIfSender().getHasRole(); + if (Objects.isNull(allowedRoles)) { + log.info("No allowed role configured for sender, allowing"); + return true; + } + + List userRoles = profileMgr.getRoles(sender); + if (Collections.disjoint(userRoles, allowedRoles)) { + log.info("Sender does not have any of the required roles, denying"); + return false; + } + log.info("Sender has at least one of the required roles"); + + log.info("Sender pass all policies to invite, allowing"); + return true; + } + public synchronized IThreePidInviteReply storeInvite(IThreePidInvite invitation) { // TODO better sync if (!notifMgr.isMediumSupported(invitation.getMedium())) { throw new BadRequestException("Medium type " + invitation.getMedium() + " is not supported"); } - String invId = getId(invitation); + String invId = computeId(invitation); log.info("Handling invite for {}:{} from {} in room {}", invitation.getMedium(), invitation.getAddress(), invitation.getSender(), invitation.getRoomId()); IThreePidInviteReply reply = invitations.get(invId); if (reply != null) { log.info("Invite is already pending for {}:{}, returning data", invitation.getMedium(), invitation.getAddress()); if (!StringUtils.equals(invitation.getRoomId(), reply.getInvite().getRoomId())) { log.info("Sending new notification as new invite room {} is different from the original {}", invitation.getRoomId(), reply.getInvite().getRoomId()); - notifMgr.sendForReply(new ThreePidInviteReply(reply.getId(), invitation, reply.getToken(), reply.getDisplayName())); + notifMgr.sendForReply(new ThreePidInviteReply(reply.getId(), invitation, reply.getToken(), reply.getDisplayName(), reply.getPublicKeys())); } else { // FIXME we should check attempt and send if bigger } @@ -232,8 +329,21 @@ public class InvitationManager { String token = RandomStringUtils.randomAlphanumeric(64); String displayName = invitation.getAddress().substring(0, 3) + "..."; + KeyIdentifier pKeyId = keyMgr.getServerSigningKey().getId(); + KeyIdentifier eKeyId = keyMgr.generateKey(KeyType.Ephemeral); - reply = new ThreePidInviteReply(invId, invitation, token, displayName); + String pPubKey = keyMgr.getPublicKeyBase64(pKeyId); + String ePubKey = keyMgr.getPublicKeyBase64(eKeyId); + + invitation.getProperties().put(CreatedAtPropertyKey, Long.toString(Instant.now().toEpochMilli())); + invitation.getProperties().put("p_key_algo", pKeyId.getAlgorithm()); + invitation.getProperties().put("p_key_serial", pKeyId.getSerial()); + invitation.getProperties().put("p_key_public", pPubKey); + invitation.getProperties().put("e_key_algo", eKeyId.getAlgorithm()); + invitation.getProperties().put("e_key_serial", eKeyId.getSerial()); + invitation.getProperties().put("e_key_public", ePubKey); + + reply = new ThreePidInviteReply(invId, invitation, token, displayName, Arrays.asList(pPubKey, ePubKey)); log.info("Performing invite to {}:{}", invitation.getMedium(), invitation.getAddress()); notifMgr.sendForReply(reply); @@ -246,6 +356,78 @@ public class InvitationManager { return reply; } + public boolean hasInvite(ThreePid tpid) { + for (IThreePidInviteReply reply : invitations.values()) { + if (!StringUtils.equals(tpid.getMedium(), reply.getInvite().getMedium())) { + continue; + } + + if (!StringUtils.equals(tpid.getAddress(), reply.getInvite().getAddress())) { + continue; + } + + return true; + } + + return false; + } + + private void removeInvite(IThreePidInviteReply reply) { + invitations.remove(reply.getId()); + storage.deleteInvite(reply.getId()); + } + + /** + * Trigger the periodic maintenance tasks + */ + public void doMaintenance() { + lookupMappingsForInvites(); + expireInvites(); + } + + public void expireInvites() { + log.debug("Invite expiration: started"); + + if (!cfg.getExpiration().isEnabled()) { + log.debug("Invite expiration is disabled, skipping"); + return; + } + + if (invitations.isEmpty()) { + log.debug("No invite to expired, skipping"); + return; + } + + String targetMxid = cfg.getExpiration().getResolveTo(); + for (IThreePidInviteReply reply : invitations.values()) { + log.debug("Processing invite {}", reply.getId()); + + String tsRaw = reply.getInvite().getProperties().computeIfAbsent(CreatedAtPropertyKey, k -> defaultCreateTs); + try { + Instant ts = Instant.ofEpochMilli(Long.parseLong(tsRaw)); + Instant targetTs = ts.plusSeconds(cfg.getExpiration().getAfter() * 60); + Instant now = Instant.now(); + log.debug("Invite {} - Created at {} - Expires at {} - Current time is {}", reply.getId(), ts, targetTs, now); + if (targetTs.isAfter(now)) { + log.debug("Invite {} has not expired yet, skipping", reply.getId()); + continue; + } + + log.info("Invite {} has expired at TS {} - Expiring and resolving to {}", targetTs, targetMxid); + publishMapping(reply, targetMxid); + } catch (NumberFormatException | DateTimeException e) { + log.warn("Invite {} has an invalid creation TS, setting to default value of {}", reply.getId(), defaultCreateTs); + reply.getInvite().getProperties().put(CreatedAtPropertyKey, defaultCreateTs); + } + } + + log.debug("Invite expiration: finished"); + } + + public void expireInvite(String id) { + publishMapping(getInvite(id), cfg.getExpiration().getResolveTo()); + } + public void lookupMappingsForInvites() { if (!invitations.isEmpty()) { log.info("Checking for existing mapping for pending invites"); @@ -266,6 +448,28 @@ public class InvitationManager { } } + public IThreePidInviteReply getInvite(String token, String privKey) { + for (IThreePidInviteReply reply : invitations.values()) { + if (StringUtils.equals(reply.getToken(), token)) { + String algo = reply.getInvite().getProperties().get("e_key_algo"); + String serial = reply.getInvite().getProperties().get("e_key_serial"); + + if (StringUtils.isAnyBlank(algo, serial)) { + continue; + } + + String storedPrivKey = keyMgr.getKey(new GenericKeyIdentifier(KeyType.Ephemeral, algo, serial)).getPrivateKeyBase64(); + if (!StringUtils.equals(storedPrivKey, privKey)) { + continue; + } + + return reply; + } + } + + throw new ObjectNotFoundException("No invite with such token and/or private key"); + } + private void publishMapping(IThreePidInviteReply reply, String mxid) { String medium = reply.getInvite().getMedium(); String address = reply.getInvite().getAddress(); @@ -280,7 +484,7 @@ public class InvitationManager { JsonObject obj = new JsonObject(); obj.addProperty("mxid", mxid); obj.addProperty("token", reply.getToken()); - obj.add("signatures", signMgr.signMessageGson(obj.toString())); + obj.add("signatures", signMgr.signMessageGson(srvCfg.getName(), obj.toString())); JsonObject objUp = new JsonObject(); objUp.addProperty("mxid", mxid); @@ -298,30 +502,45 @@ public class InvitationManager { content.addProperty("address", address); content.addProperty("mxid", mxid); - content.add("signatures", signMgr.signMessageGson(content.toString())); + content.add("signatures", signMgr.signMessageGson(srvCfg.getName(), content.toString())); StringEntity entity = new StringEntity(content.toString(), StandardCharsets.UTF_8); entity.setContentType("application/json"); req.setEntity(entity); + + Instant resolvedAt = Instant.now(); + boolean couldPublish = false; + boolean shouldArchive = true; try { log.info("Posting onBind event to {}", req.getURI()); CloseableHttpResponse response = client.execute(req); int statusCode = response.getStatusLine().getStatusCode(); log.info("Answer code: {}", statusCode); if (statusCode >= 300 && statusCode != 403) { - log.warn("Answer body: {}", IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8)); - } else { - if (statusCode == 403) { - log.info("Invite was obsolete"); - } + log.info("Answer body: {}", IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8)); + log.warn("HS returned an error."); - invitations.remove(getId(reply.getInvite())); - storage.deleteInvite(reply.getId()); - log.info("Removed invite from internal store"); + shouldArchive = statusCode != 502; + if (shouldArchive) { + log.info("Invite can be found in historical storage for manual re-processing"); + } + } else { + couldPublish = true; + if (statusCode == 403) { + log.info("Invite is obsolete or no longer under our control"); + } } response.close(); } catch (IOException e) { log.warn("Unable to tell HS {} about invite being mapped", domain, e); + } finally { + if (shouldArchive) { + synchronized (this) { + storage.insertHistoricalInvite(reply, mxid, resolvedAt, couldPublish); + removeInvite(reply); + log.info("Moved invite {} to historical table", reply.getId()); + } + } } }).start(); } @@ -337,7 +556,7 @@ public class InvitationManager { @Override public void run() { try { - log.info("Searching for mapping created since invite {} was created", getIdForLog(reply)); + log.info("Searching for mapping created after invite {} was created", getIdForLog(reply)); Optional result = lookup3pid(reply.getInvite().getMedium(), reply.getInvite().getAddress()); if (result.isPresent()) { SingleLookupReply lookup = result.get(); diff --git a/src/main/java/io/kamax/mxisd/as/MatrixIdInvite.java b/src/main/java/io/kamax/mxisd/invitation/MatrixIdInvite.java similarity index 98% rename from src/main/java/io/kamax/mxisd/as/MatrixIdInvite.java rename to src/main/java/io/kamax/mxisd/invitation/MatrixIdInvite.java index 1c9d630..1e36ea7 100644 --- a/src/main/java/io/kamax/mxisd/as/MatrixIdInvite.java +++ b/src/main/java/io/kamax/mxisd/invitation/MatrixIdInvite.java @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.as; +package io.kamax.mxisd.invitation; import io.kamax.matrix._MatrixID; diff --git a/src/main/java/io/kamax/mxisd/invitation/ThreePidInviteReply.java b/src/main/java/io/kamax/mxisd/invitation/ThreePidInviteReply.java index 04ce9e6..4def1ca 100644 --- a/src/main/java/io/kamax/mxisd/invitation/ThreePidInviteReply.java +++ b/src/main/java/io/kamax/mxisd/invitation/ThreePidInviteReply.java @@ -20,18 +20,24 @@ package io.kamax.mxisd.invitation; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + public class ThreePidInviteReply implements IThreePidInviteReply { private String id; private IThreePidInvite invite; private String token; private String displayName; + private List publicKeys; - public ThreePidInviteReply(String id, IThreePidInvite invite, String token, String displayName) { + public ThreePidInviteReply(String id, IThreePidInvite invite, String token, String displayName, List publicKeys) { this.id = id; this.invite = invite; this.token = token; this.displayName = displayName; + this.publicKeys = Collections.unmodifiableList(new ArrayList<>(publicKeys)); } @Override @@ -54,4 +60,9 @@ public class ThreePidInviteReply implements IThreePidInviteReply { return displayName; } + @Override + public List getPublicKeys() { + return publicKeys; + } + } diff --git a/src/main/java/io/kamax/mxisd/lookup/SingleLookupReply.java b/src/main/java/io/kamax/mxisd/lookup/SingleLookupReply.java index 2d7bcb3..de291d9 100644 --- a/src/main/java/io/kamax/mxisd/lookup/SingleLookupReply.java +++ b/src/main/java/io/kamax/mxisd/lookup/SingleLookupReply.java @@ -27,6 +27,8 @@ import io.kamax.matrix._MatrixID; import io.kamax.mxisd.http.io.identity.SingeLookupReplyJson; import java.time.Instant; +import java.util.HashMap; +import java.util.Map; public class SingleLookupReply { @@ -39,6 +41,7 @@ public class SingleLookupReply { private Instant notBefore; private Instant notAfter; private Instant timestamp; + private Map> signatures = new HashMap<>(); public static SingleLookupReply fromRecursive(SingleLookupRequest request, String body) { SingleLookupReply reply = new SingleLookupReply(); @@ -52,6 +55,7 @@ public class SingleLookupReply { reply.notAfter = Instant.ofEpochMilli(json.getNot_after()); reply.notBefore = Instant.ofEpochMilli(json.getNot_before()); reply.timestamp = Instant.ofEpochMilli(json.getTs()); + reply.signatures = new HashMap<>(json.getSignatures()); } catch (JsonSyntaxException e) { // stub - we only want to try, nothing more } @@ -107,4 +111,12 @@ public class SingleLookupReply { return timestamp; } + public Map> getSignatures() { + return signatures; + } + + public Map getSignature(String host) { + return signatures.computeIfAbsent(host, k -> new HashMap<>()); + } + } diff --git a/src/main/java/io/kamax/mxisd/notification/NotificationHandler.java b/src/main/java/io/kamax/mxisd/notification/NotificationHandler.java index 07056e5..b6c5ba2 100644 --- a/src/main/java/io/kamax/mxisd/notification/NotificationHandler.java +++ b/src/main/java/io/kamax/mxisd/notification/NotificationHandler.java @@ -21,7 +21,7 @@ package io.kamax.mxisd.notification; import io.kamax.matrix.ThreePid; -import io.kamax.mxisd.as.IMatrixIdInvite; +import io.kamax.mxisd.invitation.IMatrixIdInvite; import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.threepid.session.IThreePidSession; diff --git a/src/main/java/io/kamax/mxisd/notification/NotificationManager.java b/src/main/java/io/kamax/mxisd/notification/NotificationManager.java index 22a8e57..33eea8c 100644 --- a/src/main/java/io/kamax/mxisd/notification/NotificationManager.java +++ b/src/main/java/io/kamax/mxisd/notification/NotificationManager.java @@ -21,9 +21,9 @@ package io.kamax.mxisd.notification; import io.kamax.matrix.ThreePid; -import io.kamax.mxisd.as.IMatrixIdInvite; import io.kamax.mxisd.config.threepid.notification.NotificationConfig; import io.kamax.mxisd.exception.NotImplementedException; +import io.kamax.mxisd.invitation.IMatrixIdInvite; import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.threepid.session.IThreePidSession; import org.apache.commons.lang.StringUtils; diff --git a/src/main/java/io/kamax/mxisd/profile/ProfileManager.java b/src/main/java/io/kamax/mxisd/profile/ProfileManager.java index c803626..f8c143a 100644 --- a/src/main/java/io/kamax/mxisd/profile/ProfileManager.java +++ b/src/main/java/io/kamax/mxisd/profile/ProfileManager.java @@ -37,10 +37,7 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @@ -113,4 +110,8 @@ public class ProfileManager { } } + public boolean hasAnyRole(_MatrixID user, List requiredRoles) { + return !requiredRoles.isEmpty() || Collections.disjoint(getRoles(user), requiredRoles); + } + } diff --git a/src/main/java/io/kamax/mxisd/registration/RegistrationManager.java b/src/main/java/io/kamax/mxisd/registration/RegistrationManager.java new file mode 100644 index 0000000..f98f80d --- /dev/null +++ b/src/main/java/io/kamax/mxisd/registration/RegistrationManager.java @@ -0,0 +1,142 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sarl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.registration; + +import com.google.gson.JsonObject; +import io.kamax.matrix.ThreePid; +import io.kamax.matrix.json.GsonUtil; +import io.kamax.mxisd.config.RegisterConfig; +import io.kamax.mxisd.dns.ClientDnsOverwrite; +import io.kamax.mxisd.exception.NotImplementedException; +import io.kamax.mxisd.exception.RemoteHomeServerException; +import io.kamax.mxisd.invitation.InvitationManager; +import io.kamax.mxisd.util.RestClientUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class RegistrationManager { + + private static final Logger log = LoggerFactory.getLogger(RegistrationManager.class); + + private final RegisterConfig cfg; + private final CloseableHttpClient client; + private final ClientDnsOverwrite dns; + private final InvitationManager invMgr; + + public RegistrationManager(RegisterConfig cfg, CloseableHttpClient client, ClientDnsOverwrite dns, InvitationManager invMgr) { + this.cfg = cfg; + this.client = client; + this.dns = dns; + this.invMgr = invMgr; + } + + private String resolveProxyUrl(URI target) { + URIBuilder builder = dns.transform(target); + String urlToLogin = builder.toString(); + log.info("Proxy resolution: {} to {}", target.toString(), urlToLogin); + return urlToLogin; + } + + public RegistrationReply execute(URI target, JsonObject request) { + HttpPost registerProxyRq = RestClientUtils.post(resolveProxyUrl(target), GsonUtil.get(), request); + try (CloseableHttpResponse response = client.execute(registerProxyRq)) { + int status = response.getStatusLine().getStatusCode(); + if (status == 200) { + // The user managed to register. We check if it had a session + String sessionId = GsonUtil.findObj(request, "auth").flatMap(auth -> GsonUtil.findString(auth, "session")).orElse(""); + if (StringUtils.isEmpty(sessionId)) { + // No session ID was provided. This is an edge case we do not support for now as investigation is needed + // to ensure how and when this happens. + + HttpPost newSessReq = RestClientUtils.post(resolveProxyUrl(target), GsonUtil.get(), new JsonObject()); + try (CloseableHttpResponse newSessRes = client.execute(newSessReq)) { + RegistrationReply reply = new RegistrationReply(); + reply.setStatus(newSessRes.getStatusLine().getStatusCode()); + reply.setBody(GsonUtil.parseObj(EntityUtils.toString(newSessRes.getEntity()))); + return reply; + } + } + } + + throw new NotImplementedException("Registration"); + } catch (IOException e) { + throw new RemoteHomeServerException(e.getMessage()); + } + } + + public boolean isAllowed(ThreePid tpid) { + // We check if the policy allows registration for invites, and if there is an invite for the 3PID + if (cfg.getPolicy().forInvite() && invMgr.hasInvite(tpid)) { + log.info("Registration allowed for pending invite"); + return true; + } + + // The following section deals with patterns which can either be built at startup time, or for each invite at runtime. + // Registration is a very rare occurrence relatively speaking, so we make the choice to build the patterns each time + // at runtime to save on RAM. + + Object policy = cfg.getPolicy().getThreepid().get(tpid.getMedium()); + if (Objects.nonNull(policy)) { + RegisterConfig.ThreepidPolicy tpidPolicy = GsonUtil.get().fromJson(GsonUtil.get().toJson(policy), RegisterConfig.ThreepidPolicy.class); + log.info("Found registration policy for {}", tpid.getMedium()); + + log.info("Processing pattern blacklist"); + for (String pattern : tpidPolicy.getPattern().getBlacklist()) { + log.info("Processing pattern {}", pattern); + + // We compile the pattern + Matcher m = Pattern.compile(pattern).matcher(tpid.getAddress()); + if (m.matches()) { // We only care about those who match... + log.info("Found matching blacklist entry, denying registration"); + return false; // ... and get denied as per blacklist + } + } + + log.info("Processing pattern whitelist"); + for (String pattern : tpidPolicy.getPattern().getWhitelist()) { + log.info("Processing pattern {}", pattern); + + // We compile the pattern + Matcher m = Pattern.compile(pattern).matcher(tpid.getAddress()); + if (m.matches()) { // We only care about those who match... + log.info("Found matching whitelist entry, allowing registration"); + return true; // ... and get accepted as per whitelist + } + } + } + + log.info("Returning default registration policy: {}", cfg.getPolicy().isAllowed()); + return cfg.getPolicy().isAllowed(); + } + +} diff --git a/src/main/java/io/kamax/mxisd/registration/RegistrationReply.java b/src/main/java/io/kamax/mxisd/registration/RegistrationReply.java new file mode 100644 index 0000000..b2e2dde --- /dev/null +++ b/src/main/java/io/kamax/mxisd/registration/RegistrationReply.java @@ -0,0 +1,46 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sarl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.registration; + +import com.google.gson.JsonObject; + +public class RegistrationReply { + + private int status; + private JsonObject body; + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public JsonObject getBody() { + return body; + } + + public void setBody(JsonObject body) { + this.body = body; + } + +} diff --git a/src/main/java/io/kamax/mxisd/storage/IStorage.java b/src/main/java/io/kamax/mxisd/storage/IStorage.java index 80d35c4..ff1bbbe 100644 --- a/src/main/java/io/kamax/mxisd/storage/IStorage.java +++ b/src/main/java/io/kamax/mxisd/storage/IStorage.java @@ -38,6 +38,8 @@ public interface IStorage { void deleteInvite(String id); + void insertHistoricalInvite(IThreePidInviteReply data, String resolvedTo, Instant resolvedAt, boolean couldPublish); + Optional getThreePidSession(String sid); Optional findThreePidSession(ThreePid tpid, String secret); diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/FileKeyJson.java b/src/main/java/io/kamax/mxisd/storage/crypto/FileKeyJson.java new file mode 100644 index 0000000..5938a9d --- /dev/null +++ b/src/main/java/io/kamax/mxisd/storage/crypto/FileKeyJson.java @@ -0,0 +1,63 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.storage.crypto; + +import io.kamax.mxisd.crypto.Key; + +public class FileKeyJson { + + public static FileKeyJson get(Key key) { + FileKeyJson json = new FileKeyJson(); + json.setVersion("0"); + json.setKey(key.getPrivateKeyBase64()); + json.setValid(key.isValid()); + return json; + } + + private String version; + private boolean isValid; + private String key; + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public boolean isValid() { + return isValid; + } + + public void setValid(boolean valid) { + isValid = valid; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + +} diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/FileKeyStore.java b/src/main/java/io/kamax/mxisd/storage/crypto/FileKeyStore.java new file mode 100644 index 0000000..7ced08b --- /dev/null +++ b/src/main/java/io/kamax/mxisd/storage/crypto/FileKeyStore.java @@ -0,0 +1,256 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.storage.crypto; + +import com.google.gson.JsonObject; +import io.kamax.matrix.crypto.KeyFileStore; +import io.kamax.matrix.json.GsonUtil; +import io.kamax.mxisd.crypto.*; +import io.kamax.mxisd.exception.ObjectNotFoundException; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class FileKeyStore implements KeyStore { + + private static final Logger log = LoggerFactory.getLogger(FileKeyStore.class); + + private final String currentFilename = "current"; + private final String base; + + public FileKeyStore(String path) { + base = new File(path).getAbsoluteFile().toString(); + File f = new File(base); + + if (!f.exists()) { + try { + FileUtils.forceMkdir(f); + } catch (IOException e) { + throw new RuntimeException("Unable to create key store"); + } + } else { + if (f.isFile()) { + try { + log.info("Found old key store format at {}, migrating...", base); + File oldStorePath = new File(f.toString() + ".backup-before-migration"); + FileUtils.moveFile(f, oldStorePath); + FileUtils.forceMkdir(f); + + + String privKey = new KeyFileStore(oldStorePath.toString()).load().orElse(""); + if (StringUtils.isBlank(privKey)) { + log.info("Empty file, nothing to migrate"); + } else { + // We ensure this is valid Base64 data before migrating + Base64.decodeBase64(privKey); + + // We store the new key + add(new GenericKey(new GenericKeyIdentifier(KeyType.Regular, KeyAlgorithm.Ed25519, "0"), true, privKey)); + + log.info("Store migrated to new directory format"); + } + } catch (IOException e) { + throw new RuntimeException("Unable to migrate store from old single file format to new directory format", e); + } + } else { + log.info("Key store is already in directory format"); + } + } + + if (!f.isDirectory()) { + throw new RuntimeException("Key store path is not a directory: " + f.toString()); + } + } + + private String toDirName(KeyType type) { + return type.name().toLowerCase(); + } + + private Path ensureDirExists(KeyIdentifier id) { + File b = Paths.get(base, toDirName(id.getType()), id.getAlgorithm()).toFile(); + + if (b.exists()) { + if (!b.isDirectory()) { + throw new RuntimeException("Key store path already exists but is not a directory: " + b.toString()); + } + } else { + try { + FileUtils.forceMkdir(b); + } catch (IOException e) { + throw new RuntimeException("Unable to create key store path at " + b.toString(), e); + } + } + + return b.toPath(); + } + + @Override + public boolean has(KeyIdentifier id) { + return Paths.get(base, toDirName(id.getType()), id.getAlgorithm(), id.getSerial()).toFile().isFile(); + } + + @Override + public List list() { + List keyIds = new ArrayList<>(); + + for (KeyType type : KeyType.values()) { + keyIds.addAll(list(type)); + } + + return keyIds; + } + + @Override + public List list(KeyType type) { + List keyIds = new ArrayList<>(); + + File algoDir = Paths.get(base, toDirName(type)).toFile(); + File[] algos = algoDir.listFiles(); + if (Objects.isNull(algos)) { + return keyIds; + } + + for (File algo : algos) { + File[] serials = algo.listFiles(); + if (Objects.isNull(serials)) { + throw new IllegalStateException("Cannot list stored key serials: was expecting " + algo.toString() + " to be a directory"); + } + + for (File serial : serials) { + keyIds.add(new GenericKeyIdentifier(type, algo.getName(), serial.getName())); + } + } + + return keyIds; + } + + @Override + public Key get(KeyIdentifier id) throws ObjectNotFoundException { + File keyFile = ensureDirExists(id).resolve(id.getSerial()).toFile(); + if (!keyFile.exists() || !keyFile.isFile()) { + throw new ObjectNotFoundException("Key", id.getId()); + } + + try (FileInputStream keyIs = new FileInputStream(keyFile)) { + FileKeyJson json = GsonUtil.get().fromJson(IOUtils.toString(keyIs, StandardCharsets.UTF_8), FileKeyJson.class); + return new GenericKey(id, json.isValid(), json.getKey()); + } catch (IOException e) { + throw new RuntimeException("Unable to read key " + id.getId(), e); + } + } + + @Override + public void add(Key key) throws IllegalStateException { + File keyFile = ensureDirExists(key.getId()).resolve(key.getId().getSerial()).toFile(); + if (keyFile.exists()) { + throw new IllegalStateException("Key " + key.getId().getId() + " already exists"); + } + + FileKeyJson json = FileKeyJson.get(key); + try (FileOutputStream keyOs = new FileOutputStream(keyFile, false)) { + IOUtils.write(GsonUtil.get().toJson(json), keyOs, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException("Unable to create key " + key.getId().getId(), e); + } + } + + @Override + public void update(Key key) throws ObjectNotFoundException { + File keyFile = ensureDirExists(key.getId()).resolve(key.getId().getSerial()).toFile(); + if (!keyFile.exists() || !keyFile.isFile()) { + throw new ObjectNotFoundException("Key", key.getId().getId()); + } + + FileKeyJson json = FileKeyJson.get(key); + try (FileOutputStream keyOs = new FileOutputStream(keyFile, false)) { + IOUtils.write(GsonUtil.get().toJson(json), keyOs, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException("Unable to create key " + key.getId().getId(), e); + } + } + + @Override + public void delete(KeyIdentifier id) throws ObjectNotFoundException { + File keyFile = ensureDirExists(id).resolve(id.getSerial()).toFile(); + if (!keyFile.exists() || !keyFile.isFile()) { + throw new ObjectNotFoundException("Key", id.getId()); + } + + if (!keyFile.delete()) { + throw new RuntimeException("Unable to delete key " + id.getId()); + } + } + + @Override + public void setCurrentKey(KeyIdentifier id) throws IllegalArgumentException { + if (!has(id)) { + throw new IllegalArgumentException("Key " + id.getType() + ":" + id.getAlgorithm() + ":" + id.getSerial() + " is not known to the store"); + } + + JsonObject json = new JsonObject(); + json.addProperty("type", id.getType().name()); + json.addProperty("algo", id.getAlgorithm()); + json.addProperty("serial", id.getSerial()); + + File f = Paths.get(base, currentFilename).toFile(); + + try (FileOutputStream keyOs = new FileOutputStream(f, false)) { + IOUtils.write(GsonUtil.get().toJson(json), keyOs, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException("Unable to write to " + f.toString(), e); + } + } + + @Override + public Optional getCurrentKey() { + File f = Paths.get(base, currentFilename).toFile(); + if (!f.exists()) { + return Optional.empty(); + } + + if (!f.isFile()) { + throw new IllegalStateException("Current key file is not a file: " + f.toString()); + } + + try (FileInputStream keyIs = new FileInputStream(f)) { + JsonObject json = GsonUtil.parseObj(IOUtils.toString(keyIs, StandardCharsets.UTF_8)); + return Optional.of(new GenericKeyIdentifier(KeyType.valueOf(GsonUtil.getStringOrThrow(json, "type")), GsonUtil.getStringOrThrow(json, "algo"), GsonUtil.getStringOrThrow(json, "serial"))); + } catch (IOException e) { + throw new RuntimeException("Unable to read " + f.toString(), e); + } + } + +} diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/KeyStore.java b/src/main/java/io/kamax/mxisd/storage/crypto/KeyStore.java new file mode 100644 index 0000000..1dd73b9 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/storage/crypto/KeyStore.java @@ -0,0 +1,107 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.storage.crypto; + +import io.kamax.mxisd.crypto.Key; +import io.kamax.mxisd.crypto.KeyIdentifier; +import io.kamax.mxisd.crypto.KeyType; +import io.kamax.mxisd.exception.ObjectNotFoundException; + +import java.util.List; +import java.util.Optional; + +/** + * Store to persist signing keys and the identifier for the current long-term signing key + */ +public interface KeyStore { + + /** + * If a given key is currently stored + * + * @param id The Identifier elements for the key + * @return true if the key is stored, false if not + */ + boolean has(KeyIdentifier id); + + /** + * List all keys within the store + * + * @return The list of key identifiers + */ + List list(); + + /** + * List all keys of a given type within the store + * + * @param type The type to filter on + * @return The list of keys identifiers matching the given type + */ + List list(KeyType type); + + /** + * Get the key that relates to the given identifier + * + * @param id The identifier of the key to get + * @return The key + * @throws ObjectNotFoundException If no key is found for that identifier + */ + Key get(KeyIdentifier id) throws ObjectNotFoundException; + + /** + * Add a key to the store + * + * @param key The key to store + * @throws IllegalStateException If a key already exist for the given identifier data + */ + void add(Key key) throws IllegalStateException; + + /** + * Update key properties in the store + * + * @param key They key to update. getId() will be used to identify the key to update + * @throws ObjectNotFoundException If no key is found for that identifier + */ + void update(Key key) throws ObjectNotFoundException; + + /** + * Delete a key from the store + * + * @param id The key identifier of the key to delete + * @throws ObjectNotFoundException If no key is found for that identifier + */ + void delete(KeyIdentifier id) throws ObjectNotFoundException; + + /** + * Store the information of which key is the current signing key + * + * @param id The key identifier + * @throws IllegalArgumentException If the key is not known to the store + */ + void setCurrentKey(KeyIdentifier id) throws IllegalArgumentException; + + /** + * Retrieve the previously stored information of which key is the current signing key, if any + * + * @return The optional key identifier that was previously stored + */ + Optional getCurrentKey(); + +} diff --git a/src/main/java/io/kamax/mxisd/storage/crypto/MemoryKeyStore.java b/src/main/java/io/kamax/mxisd/storage/crypto/MemoryKeyStore.java new file mode 100644 index 0000000..f67df68 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/storage/crypto/MemoryKeyStore.java @@ -0,0 +1,113 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.storage.crypto; + +import io.kamax.mxisd.crypto.*; +import io.kamax.mxisd.exception.ObjectNotFoundException; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class MemoryKeyStore implements KeyStore { + + private Map>> keys = new ConcurrentHashMap<>(); + private KeyIdentifier current; + + private Map getMap(KeyType type, String algo) { + return keys.computeIfAbsent(type, k -> new ConcurrentHashMap<>()).computeIfAbsent(algo, k -> new ConcurrentHashMap<>()); + } + + @Override + public boolean has(KeyIdentifier id) { + return getMap(id.getType(), id.getAlgorithm()).containsKey(id.getSerial()); + } + + @Override + public List list() { + List keyIds = new ArrayList<>(); + keys.forEach((key, value) -> value.forEach((key1, value1) -> value1.forEach((key2, value2) -> keyIds.add(new GenericKeyIdentifier(key, key1, key2))))); + return keyIds; + } + + @Override + public List list(KeyType type) { + List keyIds = new ArrayList<>(); + keys.computeIfAbsent(type, t -> new ConcurrentHashMap<>()).forEach((key, value) -> value.forEach((key1, value1) -> keyIds.add(new GenericKeyIdentifier(type, key, key1)))); + return keyIds; + } + + @Override + public Key get(KeyIdentifier id) throws ObjectNotFoundException { + FileKeyJson data = getMap(id.getType(), id.getAlgorithm()).get(id.getSerial()); + if (Objects.isNull(data)) { + throw new ObjectNotFoundException("Key", id.getType() + ":" + id.getAlgorithm() + ":" + id.getSerial()); + } + + return new GenericKey(new GenericKeyIdentifier(id), data.isValid(), data.getKey()); + } + + private void set(Key key) { + FileKeyJson data = FileKeyJson.get(key); + getMap(key.getId().getType(), key.getId().getAlgorithm()).put(key.getId().getSerial(), data); + } + + @Override + public void add(Key key) throws IllegalStateException { + if (has(key.getId())) { + throw new IllegalStateException("Key " + key.getId().getId() + " already exists"); + } + + set(key); + } + + @Override + public void update(Key key) throws ObjectNotFoundException { + if (!has(key.getId())) { + throw new ObjectNotFoundException("Key", key.getId().getType() + ":" + key.getId().getAlgorithm() + ":" + key.getId().getSerial()); + } + + set(key); + } + + @Override + public void delete(KeyIdentifier id) throws ObjectNotFoundException { + if (!has(id)) { + throw new ObjectNotFoundException("Key", id.getType() + ":" + id.getAlgorithm() + ":" + id.getSerial()); + } + + keys.computeIfAbsent(id.getType(), k -> new ConcurrentHashMap<>()).computeIfAbsent(id.getAlgorithm(), k -> new ConcurrentHashMap<>()).remove(id.getSerial()); + } + + @Override + public void setCurrentKey(KeyIdentifier id) throws IllegalArgumentException { + if (!has(id)) { + throw new IllegalArgumentException("Key " + id.getType() + ":" + id.getAlgorithm() + ":" + id.getSerial() + " is not known to the store"); + } + + current = id; + } + + @Override + public Optional getCurrentKey() { + return Optional.ofNullable(current); + } + +} diff --git a/src/main/java/io/kamax/mxisd/storage/ormlite/OrmLiteSqlStorage.java b/src/main/java/io/kamax/mxisd/storage/ormlite/OrmLiteSqlStorage.java index 0149a7b..31a956b 100644 --- a/src/main/java/io/kamax/mxisd/storage/ormlite/OrmLiteSqlStorage.java +++ b/src/main/java/io/kamax/mxisd/storage/ormlite/OrmLiteSqlStorage.java @@ -34,24 +34,18 @@ import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.dao.IThreePidSessionDao; import io.kamax.mxisd.storage.ormlite.dao.ASTransactionDao; +import io.kamax.mxisd.storage.ormlite.dao.HistoricalThreePidInviteIO; import io.kamax.mxisd.storage.ormlite.dao.ThreePidInviteIO; import io.kamax.mxisd.storage.ormlite.dao.ThreePidSessionDao; import org.apache.commons.lang.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.IOException; import java.sql.SQLException; import java.time.Instant; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Optional; +import java.util.*; public class OrmLiteSqlStorage implements IStorage { - private transient final Logger log = LoggerFactory.getLogger(OrmLiteSqlStorage.class); - @FunctionalInterface private interface Getter { @@ -67,6 +61,7 @@ public class OrmLiteSqlStorage implements IStorage { } private Dao invDao; + private Dao expInvDao; private Dao sessionDao; private Dao asTxnDao; @@ -86,6 +81,7 @@ public class OrmLiteSqlStorage implements IStorage { withCatcher(() -> { ConnectionSource connPool = new JdbcConnectionSource("jdbc:" + backend + ":" + path); invDao = createDaoAndTable(connPool, ThreePidInviteIO.class); + expInvDao = createDaoAndTable(connPool, HistoricalThreePidInviteIO.class); sessionDao = createDaoAndTable(connPool, ThreePidSessionDao.class); asTxnDao = createDaoAndTable(connPool, ASTransactionDao.class); }); @@ -150,6 +146,24 @@ public class OrmLiteSqlStorage implements IStorage { }); } + @Override + public void insertHistoricalInvite(IThreePidInviteReply data, String resolvedTo, Instant resolvedAt, boolean couldPublish) { + withCatcher(() -> { + HistoricalThreePidInviteIO io = new HistoricalThreePidInviteIO(data, resolvedTo, resolvedAt, couldPublish); + int updated = expInvDao.create(io); + if (updated != 1) { + throw new RuntimeException("Unexpected row count after DB action: " + updated); + } + + // Ugly, but it avoids touching the structure of the historical parent class + // and avoid any possible regression at this point. + updated = expInvDao.updateId(io, UUID.randomUUID().toString().replace("-", "")); + if (updated != 1) { + throw new RuntimeException("Unexpected row count after DB action: " + updated); + } + }); + } + @Override public Optional getThreePidSession(String sid) { return withCatcher(() -> Optional.ofNullable(sessionDao.queryForId(sid))); diff --git a/src/main/java/io/kamax/mxisd/storage/ormlite/dao/HistoricalThreePidInviteIO.java b/src/main/java/io/kamax/mxisd/storage/ormlite/dao/HistoricalThreePidInviteIO.java new file mode 100644 index 0000000..57935c7 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/storage/ormlite/dao/HistoricalThreePidInviteIO.java @@ -0,0 +1,72 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sarl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.storage.ormlite.dao; + +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; +import io.kamax.mxisd.invitation.IThreePidInviteReply; + +import java.time.Instant; + +@DatabaseTable(tableName = "invite_3pid_history") +public class HistoricalThreePidInviteIO extends ThreePidInviteIO { + + @DatabaseField(canBeNull = false) + private String resolvedTo; + + @DatabaseField(canBeNull = false) + private long resolvedAt; + + @DatabaseField(canBeNull = false) + private boolean couldPublish; + + @DatabaseField(canBeNull = false) + private long publishAttempts = 1; // Placeholder for retry mechanism, if ever implemented + + public HistoricalThreePidInviteIO() { + // Needed for ORMLite + } + + public HistoricalThreePidInviteIO(IThreePidInviteReply data, String resolvedTo, Instant resolvedAt, boolean couldPublish) { + super(data); + + this.resolvedTo = resolvedTo; + this.resolvedAt = resolvedAt.toEpochMilli(); + this.couldPublish = couldPublish; + } + + public String getResolvedTo() { + return resolvedTo; + } + + public Instant getResolvedAt() { + return Instant.ofEpochMilli(resolvedAt); + } + + public boolean isCouldPublish() { + return couldPublish; + } + + public long getPublishAttempts() { + return publishAttempts; + } + +} diff --git a/src/main/java/io/kamax/mxisd/threepid/connector/email/EmailSmtpConnector.java b/src/main/java/io/kamax/mxisd/threepid/connector/email/EmailSmtpConnector.java index 24d840f..af3c3f6 100644 --- a/src/main/java/io/kamax/mxisd/threepid/connector/email/EmailSmtpConnector.java +++ b/src/main/java/io/kamax/mxisd/threepid/connector/email/EmailSmtpConnector.java @@ -22,6 +22,7 @@ package io.kamax.mxisd.threepid.connector.email; import com.sun.mail.smtp.SMTPTransport; import io.kamax.matrix.ThreePidMedium; +import io.kamax.mxisd.Mxisd; import io.kamax.mxisd.config.threepid.connector.EmailSmtpConfig; import io.kamax.mxisd.exception.FeatureNotAvailable; import io.kamax.mxisd.exception.InternalServerError; @@ -92,7 +93,7 @@ public class EmailSmtpConnector implements EmailConnector { try { InternetAddress sender = new InternetAddress(senderAddress, senderName); MimeMessage msg = new MimeMessage(session, IOUtils.toInputStream(content, StandardCharsets.UTF_8)); - msg.setHeader("X-Mailer", "mxisd"); // FIXME set version + msg.setHeader("X-Mailer", Mxisd.Agent); msg.setSentDate(new Date()); msg.setFrom(sender); msg.setRecipients(Message.RecipientType.TO, recipient); diff --git a/src/main/java/io/kamax/mxisd/threepid/generator/GenericTemplateNotificationGenerator.java b/src/main/java/io/kamax/mxisd/threepid/generator/GenericTemplateNotificationGenerator.java index ad35ddd..fb5a363 100644 --- a/src/main/java/io/kamax/mxisd/threepid/generator/GenericTemplateNotificationGenerator.java +++ b/src/main/java/io/kamax/mxisd/threepid/generator/GenericTemplateNotificationGenerator.java @@ -21,11 +21,11 @@ package io.kamax.mxisd.threepid.generator; import io.kamax.matrix.ThreePid; -import io.kamax.mxisd.as.IMatrixIdInvite; import io.kamax.mxisd.config.MatrixConfig; import io.kamax.mxisd.config.ServerConfig; import io.kamax.mxisd.config.threepid.medium.GenericTemplateConfig; import io.kamax.mxisd.exception.InternalServerError; +import io.kamax.mxisd.invitation.IMatrixIdInvite; import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.threepid.session.IThreePidSession; import io.kamax.mxisd.util.FileUtil; diff --git a/src/main/java/io/kamax/mxisd/threepid/generator/NotificationGenerator.java b/src/main/java/io/kamax/mxisd/threepid/generator/NotificationGenerator.java index eb5f49d..aeb85f1 100644 --- a/src/main/java/io/kamax/mxisd/threepid/generator/NotificationGenerator.java +++ b/src/main/java/io/kamax/mxisd/threepid/generator/NotificationGenerator.java @@ -21,7 +21,7 @@ package io.kamax.mxisd.threepid.generator; import io.kamax.matrix.ThreePid; -import io.kamax.mxisd.as.IMatrixIdInvite; +import io.kamax.mxisd.invitation.IMatrixIdInvite; import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.threepid.session.IThreePidSession; diff --git a/src/main/java/io/kamax/mxisd/threepid/generator/PlaceholderNotificationGenerator.java b/src/main/java/io/kamax/mxisd/threepid/generator/PlaceholderNotificationGenerator.java index 34d6e05..c1fccf6 100644 --- a/src/main/java/io/kamax/mxisd/threepid/generator/PlaceholderNotificationGenerator.java +++ b/src/main/java/io/kamax/mxisd/threepid/generator/PlaceholderNotificationGenerator.java @@ -21,10 +21,10 @@ package io.kamax.mxisd.threepid.generator; import io.kamax.matrix.ThreePid; -import io.kamax.mxisd.as.IMatrixIdInvite; import io.kamax.mxisd.config.MatrixConfig; import io.kamax.mxisd.config.ServerConfig; import io.kamax.mxisd.http.IsAPIv1; +import io.kamax.mxisd.invitation.IMatrixIdInvite; import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.threepid.session.IThreePidSession; import org.apache.commons.lang.StringUtils; diff --git a/src/main/java/io/kamax/mxisd/threepid/notification/GenericNotificationHandler.java b/src/main/java/io/kamax/mxisd/threepid/notification/GenericNotificationHandler.java index b6992a0..cd77907 100644 --- a/src/main/java/io/kamax/mxisd/threepid/notification/GenericNotificationHandler.java +++ b/src/main/java/io/kamax/mxisd/threepid/notification/GenericNotificationHandler.java @@ -21,8 +21,8 @@ package io.kamax.mxisd.threepid.notification; import io.kamax.matrix.ThreePid; -import io.kamax.mxisd.as.IMatrixIdInvite; import io.kamax.mxisd.exception.ConfigurationException; +import io.kamax.mxisd.invitation.IMatrixIdInvite; import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.notification.NotificationHandler; import io.kamax.mxisd.threepid.connector.ThreePidConnector; diff --git a/src/main/java/io/kamax/mxisd/threepid/notification/email/EmailSendGridNotificationHandler.java b/src/main/java/io/kamax/mxisd/threepid/notification/email/EmailSendGridNotificationHandler.java index 93b19a3..ce8760c 100644 --- a/src/main/java/io/kamax/mxisd/threepid/notification/email/EmailSendGridNotificationHandler.java +++ b/src/main/java/io/kamax/mxisd/threepid/notification/email/EmailSendGridNotificationHandler.java @@ -24,10 +24,10 @@ import com.sendgrid.SendGrid; import com.sendgrid.SendGridException; import io.kamax.matrix.ThreePid; import io.kamax.matrix.ThreePidMedium; -import io.kamax.mxisd.as.IMatrixIdInvite; import io.kamax.mxisd.config.MxisdConfig; import io.kamax.mxisd.config.threepid.connector.EmailSendGridConfig; import io.kamax.mxisd.exception.FeatureNotAvailable; +import io.kamax.mxisd.invitation.IMatrixIdInvite; import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.notification.NotificationHandler; import io.kamax.mxisd.threepid.generator.PlaceholderNotificationGenerator; diff --git a/src/main/resources/simplelogger.properties b/src/main/resources/simplelogger.properties new file mode 100644 index 0000000..b509437 --- /dev/null +++ b/src/main/resources/simplelogger.properties @@ -0,0 +1,2 @@ +org.slf4j.simpleLogger.logFile=System.out +org.slf4j.simpleLogger.log.org.xnio=warn diff --git a/src/test/java/io/kamax/mxisd/test/crypto/KeyTest.java b/src/test/java/io/kamax/mxisd/test/crypto/KeyTest.java new file mode 100644 index 0000000..aa0fd34 --- /dev/null +++ b/src/test/java/io/kamax/mxisd/test/crypto/KeyTest.java @@ -0,0 +1,31 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.test.crypto; + +public class KeyTest { + + // As per https://matrix.org/docs/spec/appendices.html#signing-key + public static final String Private = "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA1"; + + // The corresponding public key, not being documented in the spec + public static final String Public = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI"; + +} diff --git a/src/test/java/io/kamax/mxisd/test/crypto/SignatureManagerTest.java b/src/test/java/io/kamax/mxisd/test/crypto/SignatureManagerTest.java new file mode 100644 index 0000000..06d2fed --- /dev/null +++ b/src/test/java/io/kamax/mxisd/test/crypto/SignatureManagerTest.java @@ -0,0 +1,109 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.test.crypto; + +import com.google.gson.JsonObject; +import io.kamax.matrix.json.GsonUtil; +import io.kamax.matrix.json.MatrixJson; +import io.kamax.mxisd.crypto.Signature; +import io.kamax.mxisd.crypto.SignatureManager; +import io.kamax.mxisd.crypto.ed25519.Ed25519Key; +import io.kamax.mxisd.crypto.ed25519.Ed25519KeyManager; +import io.kamax.mxisd.crypto.ed25519.Ed25519RegularKeyIdentifier; +import io.kamax.mxisd.crypto.ed25519.Ed25519SignatureManager; +import io.kamax.mxisd.storage.crypto.KeyStore; +import io.kamax.mxisd.storage.crypto.MemoryKeyStore; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertThat; + +public class SignatureManagerTest { + + private static SignatureManager signMgr; + + private static SignatureManager build(String keySeed) { + Ed25519Key key = new Ed25519Key(new Ed25519RegularKeyIdentifier("0"), keySeed); + KeyStore store = new MemoryKeyStore(); + store.add(key); + + return new Ed25519SignatureManager(new Ed25519KeyManager(store)); + } + + @BeforeClass + public static void beforeClass() { + signMgr = build(KeyTest.Private); + } + + private void testSign(String value, String sign) { + assertThat(signMgr.sign(value).getSignature(), is(equalTo(sign))); + } + + // As per https://matrix.org/docs/spec/appendices.html#json-signing + @Test + public void onEmptyObject() { + String value = "{}"; + String sign = "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ"; + + testSign(value, sign); + } + + // As per https://matrix.org/docs/spec/appendices.html#json-signing + @Test + public void onSimpleObject() { + JsonObject data = new JsonObject(); + data.addProperty("one", 1); + data.addProperty("two", "Two"); + + String value = GsonUtil.get().toJson(data); + String sign = "KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw"; + + testSign(value, sign); + } + + @Test + public void onFederationHeader() { + SignatureManager mgr = build("1QblgjFeL3IxoY4DKOR7p5mL5sQTC0ChmeMJlqb4d5M"); + + JsonObject o = new JsonObject(); + o.addProperty("method", "GET"); + o.addProperty("uri", "/_matrix/federation/v1/query/directory?room_alias=%23a%3Amxhsd.local.kamax.io%3A8447"); + o.addProperty("origin", "synapse.local.kamax.io"); + o.addProperty("destination", "mxhsd.local.kamax.io:8447"); + + String signExpected = "SEMGSOJEsoalrBfHqPO2QrSlbLaUYLHLk4e3q4IJ2JbgvCynT1onp7QF1U4Sl3G3NzybrgdnVvpqcaEgV0WPCw"; + Signature signProduced = mgr.sign(o); + assertThat(signProduced.getSignature(), is(equalTo(signExpected))); + } + + @Test + public void onIdentityLookup() { + String value = MatrixJson.encodeCanonical("{\n" + " \"address\": \"mxisd-federation-test@kamax.io\",\n" + + " \"medium\": \"email\",\n" + " \"mxid\": \"@mxisd-lookup-test:kamax.io\",\n" + + " \"not_after\": 253402300799000,\n" + " \"not_before\": 0,\n" + " \"ts\": 1523482030147\n" + "}"); + + String sign = "ObKA4PNQh2g6c7Yo2QcTcuDgIwhknG7ZfqmNYzbhrbLBOqZomU22xX9raufN2Y3ke1FXsDqsGs7WBDodmzZJCg"; + testSign(value, sign); + } + +} diff --git a/src/test/java/io/kamax/mxisd/test/notification/EmailNotificationTest.java b/src/test/java/io/kamax/mxisd/test/notification/EmailNotificationTest.java index 924b010..f4677d9 100644 --- a/src/test/java/io/kamax/mxisd/test/notification/EmailNotificationTest.java +++ b/src/test/java/io/kamax/mxisd/test/notification/EmailNotificationTest.java @@ -28,10 +28,10 @@ import io.kamax.matrix.ThreePidMedium; import io.kamax.matrix._MatrixID; import io.kamax.matrix.json.GsonUtil; import io.kamax.mxisd.Mxisd; -import io.kamax.mxisd.as.MatrixIdInvite; import io.kamax.mxisd.config.MxisdConfig; import io.kamax.mxisd.config.threepid.connector.EmailSmtpConfig; import io.kamax.mxisd.config.threepid.medium.EmailConfig; +import io.kamax.mxisd.invitation.MatrixIdInvite; import io.kamax.mxisd.threepid.connector.email.EmailSmtpConnector; import io.kamax.mxisd.threepid.session.ThreePidSession; import org.apache.commons.lang.RandomStringUtils; diff --git a/src/test/java/io/kamax/mxisd/test/storage/crypto/FileKeyStoreTest.java b/src/test/java/io/kamax/mxisd/test/storage/crypto/FileKeyStoreTest.java new file mode 100644 index 0000000..fdfed23 --- /dev/null +++ b/src/test/java/io/kamax/mxisd/test/storage/crypto/FileKeyStoreTest.java @@ -0,0 +1,42 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.test.storage.crypto; + +import io.kamax.mxisd.storage.crypto.FileKeyStore; +import io.kamax.mxisd.storage.crypto.KeyStore; +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.util.UUID; + +public class FileKeyStoreTest extends KeyStoreTest { + + @Override + public KeyStore create() throws IOException { + String path = FileUtils.getTempDirectoryPath() + + "/mxisd-test-key-store-" + + UUID.randomUUID().toString().replace("-", ""); + FileUtils.forceDeleteOnExit(new File(path)); + return new FileKeyStore(path); + } + +} diff --git a/src/test/java/io/kamax/mxisd/test/storage/crypto/KeyStoreTest.java b/src/test/java/io/kamax/mxisd/test/storage/crypto/KeyStoreTest.java new file mode 100644 index 0000000..dd64dcf --- /dev/null +++ b/src/test/java/io/kamax/mxisd/test/storage/crypto/KeyStoreTest.java @@ -0,0 +1,128 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.test.storage.crypto; + +import io.kamax.mxisd.crypto.*; +import io.kamax.mxisd.exception.ObjectNotFoundException; +import io.kamax.mxisd.storage.crypto.KeyStore; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.Before; +import org.junit.Test; + +import java.util.Optional; + +import static org.junit.Assert.*; + +public abstract class KeyStoreTest { + + private KeyStore store; + + public abstract KeyStore create() throws Exception; + + private Key generateRandomKey() { + KeyIdentifier keyId = new GenericKeyIdentifier(KeyType.Regular, "algo", RandomStringUtils.randomAlphanumeric(6)); + return new GenericKey(keyId, true, RandomStringUtils.randomAlphanumeric(48)); + } + + @Before + public void before() throws Exception { + store = create(); + } + + @Test + public void isEmptyAfterCreate() { + assertTrue(store.list().isEmpty()); + assertFalse(store.getCurrentKey().isPresent()); + } + + @Test + public void add() { + Key key = generateRandomKey(); + KeyIdentifier keyId = key.getId(); + + store.add(key); + + Key keyFromStore = store.get(keyId); + assertEquals(key.getId(), keyFromStore.getId()); + assertEquals(key.getPrivateKeyBase64(), keyFromStore.getPrivateKeyBase64()); + assertEquals(key.isValid(), keyFromStore.isValid()); + + assertTrue(store.list().contains(keyId)); + assertTrue(store.list(keyId.getType()).contains(keyId)); + } + + @Test(expected = IllegalStateException.class) + public void addDuplicate() { + Key key = generateRandomKey(); + store.add(key); + store.add(key); + } + + @Test + public void update() { + Key key = generateRandomKey(); + store.add(key); + + Key keyUpdated = new GenericKey(key.getId(), !key.isValid(), key.getPrivateKeyBase64()); + store.update(keyUpdated); + + Key keyFromStore = store.get(key.getId()); + assertEquals(key.getId(), keyFromStore.getId()); + assertEquals(key.getPrivateKeyBase64(), keyFromStore.getPrivateKeyBase64()); + assertEquals(key.isValid(), !keyFromStore.isValid()); + } + + @Test(expected = ObjectNotFoundException.class) + public void updateNonExisting() { + store.update(generateRandomKey()); + } + + @Test + public void delete() { + Key key = generateRandomKey(); + store.add(key); + + store.delete(key.getId()); + assertFalse(store.list().contains(key.getId())); + assertFalse(store.list(key.getId().getType()).contains(key.getId())); + } + + @Test(expected = ObjectNotFoundException.class) + public void deleteNonExisting() { + store.delete(generateRandomKey().getId()); + } + + @Test + public void setCurrentKey() { + Key key = generateRandomKey(); + store.add(key); + store.setCurrentKey(key.getId()); + Optional currentKey = store.getCurrentKey(); + assertTrue(currentKey.isPresent()); + assertEquals(currentKey.get(), key.getId()); + } + + @Test(expected = IllegalArgumentException.class) + public void setCurrentKeyNonExisting() { + store.setCurrentKey(generateRandomKey().getId()); + } + +} diff --git a/src/test/java/io/kamax/mxisd/test/storage/crypto/MemoryKeyStoreTest.java b/src/test/java/io/kamax/mxisd/test/storage/crypto/MemoryKeyStoreTest.java new file mode 100644 index 0000000..af918da --- /dev/null +++ b/src/test/java/io/kamax/mxisd/test/storage/crypto/MemoryKeyStoreTest.java @@ -0,0 +1,33 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2019 Kamax Sàrl + * + * https://www.kamax.io/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package io.kamax.mxisd.test.storage.crypto; + +import io.kamax.mxisd.storage.crypto.KeyStore; +import io.kamax.mxisd.storage.crypto.MemoryKeyStore; + +public class MemoryKeyStoreTest extends KeyStoreTest { + + @Override + public KeyStore create() { + return new MemoryKeyStore(); + } + +}