Compare commits
	
		
			47 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 44a80461a0 | ||
|  | 85d9f9e704 | ||
|  | 6278301672 | ||
|  | 5ed0c66cfd | ||
|  | ea58b6985a | ||
|  | a44f781495 | ||
|  | 0d42ee695a | ||
|  | f331af0941 | ||
|  | e2c8a56135 | ||
|  | a67c5d7ae1 | ||
|  | 80352070f1 | ||
|  | 39447b8b8b | ||
|  | 9d4680f55a | ||
|  | d1ea0fbf0f | ||
|  | ee21f051fb | ||
|  | 6cc17abf2c | ||
|  | a7b5accd75 | ||
|  | 6bb0c93f57 | ||
|  | 9abdcc15ba | ||
|  | eb903bf226 | ||
|  | 1cbb0a135b | ||
|  | 1587103c0a | ||
|  | 838d79ae15 | ||
|  | 96c47ecf76 | ||
|  | c5cea933a4 | ||
|  | 57c7e4a91d | ||
|  | 1dce59a02e | ||
|  | de840b9d00 | ||
|  | 53c85d2248 | ||
|  | 254dc5684f | ||
|  | de92e98f7d | ||
|  | d5f9137056 | ||
|  | 1307e3aa43 | ||
|  | dfedde0df6 | ||
|  | 93bd7354c2 | ||
|  | c302789898 | ||
|  | 96155c1876 | ||
|  | 95ee328281 | ||
|  | 72a1794cc3 | ||
|  | 37ddd0e588 | ||
|  | 4d63bba251 | ||
|  | aadfae2965 | ||
|  | 2f7e5e4025 | ||
|  | 77dc75d383 | ||
|  | f3b528d1ba | ||
|  | 91e5e08e70 | ||
|  | acd8c7d7c5 | 
| @@ -53,6 +53,7 @@ As an enhanced Identity service: | |||||||
|   - Central Matrix Identity servers |   - Central Matrix Identity servers | ||||||
| - [Session Control](docs/threepids/session/session.md): Extensive control of where 3PIDs are transmitted so they are not | - [Session Control](docs/threepids/session/session.md): Extensive control of where 3PIDs are transmitted so they are not | ||||||
|   leaked publicly by users |   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) | - [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) |   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, | - [Directory search](docs/features/directory.md) which allows you to search for users within your organisation, | ||||||
| @@ -80,8 +81,6 @@ A basic troubleshooting guide is available [here](docs/troubleshooting.md). | |||||||
| ## Community | ## Community | ||||||
| Over Matrix: [#mxisd:kamax.io](https://matrix.to/#/#mxisd:kamax.io) ([Preview](https://view.matrix.org/room/!NPRUEisLjcaMtHIzDr:kamax.io/)) | Over Matrix: [#mxisd:kamax.io](https://matrix.to/#/#mxisd:kamax.io) ([Preview](https://view.matrix.org/room/!NPRUEisLjcaMtHIzDr:kamax.io/)) | ||||||
|  |  | ||||||
| For more high-level discussion about the Identity Server architecture/API, go to  [#matrix-identity:kamax.io](https://matrix.to/#/#matrix-identity:kamax.io) |  | ||||||
|  |  | ||||||
| ## Commercial | ## Commercial | ||||||
| If you would prefer professional support/custom development for mxisd and/or for Matrix in general, including other open | If you would prefer professional support/custom development for mxisd and/or for Matrix in general, including other open | ||||||
| source technologies/products: | source technologies/products: | ||||||
|   | |||||||
							
								
								
									
										11
									
								
								build.gradle
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								build.gradle
									
									
									
									
									
								
							| @@ -146,12 +146,23 @@ dependencies { | |||||||
|     // HTTP server |     // HTTP server | ||||||
|     compile 'io.undertow:undertow-core:2.0.16.Final' |     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 'junit:junit:4.12' | ||||||
|     testCompile 'com.github.tomakehurst:wiremock:2.8.0' |     testCompile 'com.github.tomakehurst:wiremock:2.8.0' | ||||||
|     testCompile 'com.unboundid:unboundid-ldapsdk:4.0.9' |     testCompile 'com.unboundid:unboundid-ldapsdk:4.0.9' | ||||||
|     testCompile 'com.icegreen:greenmail:1.5.9' |     testCompile 'com.icegreen:greenmail:1.5.9' | ||||||
| } | } | ||||||
|  |  | ||||||
|  | jar { | ||||||
|  |     manifest { | ||||||
|  |         attributes( | ||||||
|  |             'Implementation-Version': mxisdVersion() | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| shadowJar { | shadowJar { | ||||||
|     baseName = project.name |     baseName = project.name | ||||||
|     classifier = null |     classifier = null | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ It allows to use Identity stores configured in mxisd to authenticate users on yo | |||||||
|  |  | ||||||
| Authentication is divided into two parts: | Authentication is divided into two parts: | ||||||
| - [Basic](#basic): authenticate with a regular username. | - [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 | ## Basic | ||||||
| Authentication by username is possible by linking synapse and mxisd together using a specific module for synapse, also | 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: | |||||||
| </VirtualHost> | </VirtualHost> | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|  | ##### 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 | #### DNS Overwrite | ||||||
|  |  | ||||||
| Just like you need to configure a reverse proxy to send client requests to mxisd, you also need to configure mxisd with | 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. | 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 `/`. | `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 | #### Username rewrite | ||||||
| In mxisd config: | In mxisd config: | ||||||
| ```yaml | ```yaml | ||||||
|   | |||||||
| @@ -1,25 +1,107 @@ | |||||||
| # Integration as an Application Service | # Application Service | ||||||
| **WARNING:** These features are currently highly experimental. They can be removed or modified without notice.   | **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       | `false` | 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: | ||||||
|  |   enabled: true | ||||||
|  |   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 | 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. | account was already provisioned on the Homeserver. | ||||||
|  |  | ||||||
| ### Requirements | #### Requirements | ||||||
| - [Identity store(s)](../../stores/README.md) supporting the Profile feature | - [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. | - At least one email entry in the identity store for each user that could be invited. | ||||||
|  |  | ||||||
| ### Configuration | #### Configuration | ||||||
| In your mxisd config file: | In your mxisd config file: | ||||||
| ```yaml | ```yaml | ||||||
| matrix: |  | ||||||
|   listener: |  | ||||||
|     url:  '<URL TO THE CS API OF THE HOMESERVER>' |  | ||||||
|     localpart: 'appservice-mxisd' |  | ||||||
|     token: |  | ||||||
|       hs: 'HS_TOKEN_CHANGE_ME' |  | ||||||
|  |  | ||||||
| synapseSql: | synapseSql: | ||||||
|   enabled: false ## Do not use this line if Synapse is used as an Identity Store |   enabled: false ## Do not use this line if Synapse is used as an Identity Store | ||||||
|   type: '<DB TYPE>' |   type: '<DB TYPE>' | ||||||
| @@ -33,40 +115,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.   | 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. | See [the Template generator documentation](../../threepids/notification/template-generator.md) for more info. | ||||||
|  |  | ||||||
| ### Homeserver integration | #### Test | ||||||
| #### 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 |  | ||||||
| Invite a user which is part of your domain while an appropriate Identity store is used. | Invite a user which is part of your domain while an appropriate Identity store is used. | ||||||
|  |  | ||||||
|  | ### Auto-reject of expired 3PID invites | ||||||
|  | *TBC* | ||||||
|   | |||||||
| @@ -1,6 +1,13 @@ | |||||||
| # Identity | # Identity | ||||||
| Implementation of the [Identity Service API r0.1.0](https://matrix.org/docs/spec/identity_service/r0.1.0.html). | 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 | ## Lookups | ||||||
| If you would like to use the central matrix.org Identity server to ensure maximum discovery at the cost of potentially | 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: | 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.   | **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). | For more info, see the [relevant issue](https://github.com/kamax-matrix/mxisd/issues/76). | ||||||
|  |  | ||||||
| ## Room Invitations | ## Invitations | ||||||
| Resolution can be customized using the following configuration: | ### 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: | ||||||
|  |         - '<THIS_ROLE>' | ||||||
|  |         - '<OR_THIS_ROLE>' | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Resolution | ||||||
|  | Resolution of 3PID invitations can be customized using the following configuration: | ||||||
|  |  | ||||||
| `invite.resolution.recursive`   | `invite.resolution.recursive`   | ||||||
| - Default value: `true`   | - Default value: `true`   | ||||||
| @@ -26,5 +103,5 @@ Resolution can be customized using the following configuration: | |||||||
| - Default value: `1`   | - Default value: `1`   | ||||||
| - Description: How often, in minutes, mxisd should try to resolve pending invites. | - 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) | See the [3PID session documents](../threepids/session) | ||||||
|   | |||||||
							
								
								
									
										111
									
								
								docs/features/registration.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								docs/features/registration.md
									
									
									
									
									
										Normal file
									
								
							| @@ -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: | ||||||
|  | - `*<domain>` will match the domain and any sub-domain(s) | ||||||
|  | - `.<domain>` will only match sub-domain(s) | ||||||
|  | - `<domain>` 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. | ||||||
| @@ -153,3 +153,5 @@ infrastructure: | |||||||
|  |  | ||||||
| - [Enable extra features](features/) | - [Enable extra features](features/) | ||||||
| - [Use your own Identity stores](stores/README.md) | - [Use your own Identity stores](stores/README.md) | ||||||
|  | - [Hardening your mxisd installation](install/security.md) | ||||||
|  | - [Learn about day-to-day operations](operations.md) | ||||||
|   | |||||||
| @@ -20,3 +20,6 @@ docker run --rm -e MATRIX_DOMAIN=example.org -v /data/mxisd/etc:/etc/mxisd -v /d | |||||||
| ``` | ``` | ||||||
|  |  | ||||||
| For more info, including the list of possible tags, see [the public repository](https://hub.docker.com/r/kamax/mxisd/) | For more info, including the list of possible tags, see [the public repository](https://hub.docker.com/r/kamax/mxisd/) | ||||||
|  |  | ||||||
|  | ## Troubleshoot | ||||||
|  | Please read the [Troubleshooting guide](../troubleshooting.md). | ||||||
							
								
								
									
										30
									
								
								docs/install/security.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								docs/install/security.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | |||||||
|  | # Security hardening | ||||||
|  | ## Overview | ||||||
|  | This document outlines the various operations you may want to perform to increase the security of your installation and | ||||||
|  | avoid leak of credentials/key pairs | ||||||
|  |  | ||||||
|  | ## Configuration | ||||||
|  | Your config file should have the following ownership: | ||||||
|  | - Dedicated user for mxisd, used to run the software | ||||||
|  | - Dedicated group for mxisd, used by other applications to access and read configuration files | ||||||
|  |  | ||||||
|  | Your config file should have the following access: | ||||||
|  | - Read and write for the mxisd user | ||||||
|  | - Read for the mxisd group | ||||||
|  | - Nothing for others | ||||||
|  |  | ||||||
|  | This translates into `640` and be applied with `chmod 640 /path/to/config/file.yaml`. | ||||||
|  |  | ||||||
|  | ## Data | ||||||
|  | The only sensible place is the key store where mxisd's signing keys are stored. You should therefore limit access to only | ||||||
|  | the mxisd user, and deny access to anything else. | ||||||
|  |  | ||||||
|  | Your key store should have the following access: | ||||||
|  | - Read and write for the mxisd user | ||||||
|  | - Nothing for the mxisd group | ||||||
|  | - Nothing for others | ||||||
|  |  | ||||||
|  | The identity store can either be a file or a directory, depending on your version. v1.4 and higher are using a directory, | ||||||
|  | everything before is using a file. | ||||||
|  | - If your version is directory-based, you will want to apply chmod `700` on it. | ||||||
|  | - If your version is file-based, you will want to apply chmod `600` on it. | ||||||
							
								
								
									
										21
									
								
								docs/operations.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								docs/operations.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | # Operations Guide | ||||||
|  | - [Overview](#overview) | ||||||
|  | - [Maintenance](#maintenance) | ||||||
|  | - [Backuo](#backup) | ||||||
|  |  | ||||||
|  | ## Overview | ||||||
|  | This document gives various information for the day-to-day management and operations of mxisd. | ||||||
|  |  | ||||||
|  | ## Maintenance | ||||||
|  | mxisd does not require any maintenance task to run at optimal performance. | ||||||
|  |  | ||||||
|  | ## Backup | ||||||
|  | ### Run | ||||||
|  | mxisd requires all file in its configuration and data directory to be backed up.   | ||||||
|  | They are usually located at: | ||||||
|  | - `/etc/mxisd` | ||||||
|  | - `/var/lib/mxisd` | ||||||
|  |  | ||||||
|  | ### Restore | ||||||
|  | Reinstall mxisd, restore the two folders above in the appropriate location (depending on your install method) and you | ||||||
|  | will be good to go. Simply start mxisd to restore functionality. | ||||||
| @@ -89,7 +89,7 @@ ldap: | |||||||
| #### 3PIDs | #### 3PIDs | ||||||
| You can also change the attribute lists for 3PID, like email or phone numbers. | You can also change the attribute lists for 3PID, like email or phone numbers. | ||||||
|  |  | ||||||
| The following example would overwrite the [default list of attributes](../../src/main/resources/application.yaml#L67) | The following example would overwrite the [default list of attributes](../../src/main/java/io/kamax/mxisd/config/ldap/LdapConfig.java#L64) | ||||||
| for emails and phone number: | for emails and phone number: | ||||||
| ```yaml | ```yaml | ||||||
| ldap: | ldap: | ||||||
|   | |||||||
| @@ -102,9 +102,41 @@ sql: | |||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ### Identity | ### Identity | ||||||
|  | **NOTE**: Only single lookup is supported. Bulk lookup always returns no mapping. This is a restriction as the Matrix API | ||||||
|  | does not allow paging or otherwise limit of results of the API, potentially leading to thousands and thousands 3PIDs at once. | ||||||
|  |  | ||||||
| ```yaml | ```yaml | ||||||
| sql: | sql: | ||||||
|   identity: |   identity: | ||||||
|  |     enabled: <boolean> | ||||||
|     type: <string> |     type: <string> | ||||||
|     query: <string> |     query: <string> | ||||||
|  |     medium: | ||||||
|  |      mediumTypeExample: <dedicated query> | ||||||
| ``` | ``` | ||||||
|  | `type` is used to tell mxisd how to process the returned `uid` column containing the User ID: | ||||||
|  | - `localpart` will build a full Matrix ID using the `matrix.domain` value. | ||||||
|  | - `mxid` will use the ID as-is. If it is not a valid Matrix ID, lookup(s) will fail. | ||||||
|  |  | ||||||
|  | A specific query can also given per 3PID medium type. | ||||||
|  |  | ||||||
|  | ### Profile | ||||||
|  | ```yaml | ||||||
|  | sql: | ||||||
|  |   profile: | ||||||
|  |     enabled: <boolean> | ||||||
|  |     displayName: | ||||||
|  |       query: <string> | ||||||
|  |     threepid: | ||||||
|  |       query: <string> | ||||||
|  |     role: | ||||||
|  |       type: <string> | ||||||
|  |       query: <string> | ||||||
|  |      | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | For the `role` query, `type` can be used to tell mxisd how to inject the User ID in the query: | ||||||
|  | - `localpart` will extract and set only the localpart. | ||||||
|  | - `mxid` will use the ID as-is. | ||||||
|  |  | ||||||
|  | On each query, the first parameter `?` is set as a string with the corresponding ID format. | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| # Synapse Identity Store | # 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 | ## Features | ||||||
| |                       Name                      | Supported | | |                       Name                      | Supported | | ||||||
| @@ -9,7 +10,8 @@ Synapse's Database itself can be used as an Identity store. | |||||||
| | [Identity](../features/identity.md)             | Yes       | | | [Identity](../features/identity.md)             | Yes       | | ||||||
| | [Profile](../features/profile.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 | ## Configuration | ||||||
| ### Basic | ### Basic | ||||||
|   | |||||||
| @@ -12,8 +12,8 @@ threepid: | |||||||
|       connectors: |       connectors: | ||||||
|         smtp: |         smtp: | ||||||
|           host: 'smtpHostname' |           host: 'smtpHostname' | ||||||
|           port: 587 |           tls: 1 # 0 = no STARTLS, 1 = try, 2 = force, 3 = TLS/SSL | ||||||
|           tls: 1 # 0 = no STARTLS, 1 = try, 2 = force |           port: 587 # Set appropriate value depending on your TLS setting | ||||||
|           login: 'smtpLogin' |           login: 'smtpLogin' | ||||||
|           password: 'smtpPassword' |           password: 'smtpPassword' | ||||||
| ``` | ``` | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ threepid: | |||||||
|     msisdn: |     msisdn: | ||||||
|       connectors: |       connectors: | ||||||
|         twilio: |         twilio: | ||||||
|           accountSid: 'myAccountSid' |           account_sid: 'myAccountSid' | ||||||
|           authToken: 'myAuthToken' |           auth_token: 'myAuthToken' | ||||||
|           number: '+123456789' |           number: '+123456789' | ||||||
| ``` | ``` | ||||||
|   | |||||||
| @@ -1,63 +1,109 @@ | |||||||
| # Notifications: Generate from templates | # Notifications: Template generator | ||||||
| To create notification content, you can use the `template` generator if supported for the 3PID medium which will read | Most of the Identity actions will trigger a notification of some kind, informing the user of some confirmation, next step | ||||||
| content from configured files. | or just informing them about the current state of things. | ||||||
|  |  | ||||||
| Placeholders can be integrated into the templates to dynamically populate such content with relevant information like | Those notifications are by default generated from templates and by replacing placeholder tokens in them with the relevant | ||||||
| the 3PID that was requested, the domain of your Identity server, etc. | values of the notification. It is possible to customize the value of some placeholders, making easy to set values in the builtin templates, and/or | ||||||
|  | provide your own custom templates. | ||||||
|  |  | ||||||
| Templates can be configured for each event that would send a notification to the end user. Events share a set of common | Templates for the following events/actions are available: | ||||||
| placeholders and also have their own individual set of placeholders. | - [3PID invite](../../features/identity.md) | ||||||
|  | - [3PID session: validation](../session/session.md) | ||||||
|  | - [3PID session: fraudulent unbind](https://github.com/kamax-matrix/mxisd/wiki/mxisd-and-your-privacy#improving-your-privacy-one-commit-at-the-time) | ||||||
|  | - [Matrix ID invite](../../features/experimental/application-service.md#email-notification-about-room-invites-by-matrix-ids) | ||||||
|  |  | ||||||
|  | ## Placeholders | ||||||
|  | All placeholders **MUST** be surrounded with `%` in the template. Per example, the `DOMAIN` placeholder would become | ||||||
|  | `%DOMAIN%` within the template. This ensures replacement doesn't happen on non-placeholder strings. | ||||||
|  |  | ||||||
|  | ### Global | ||||||
|  | The following placeholders are available in every template: | ||||||
|  |  | ||||||
|  | | Placeholder         | Purpose                                                                      | | ||||||
|  | |---------------------|------------------------------------------------------------------------------| | ||||||
|  | | `DOMAIN`            | Identity server authoritative domain, as configured in `matrix.domain`       | | ||||||
|  | | `DOMAIN_PRETTY`     | Same as `DOMAIN` with the first letter upper case and all other lower case   | | ||||||
|  | | `FROM_EMAIL`        | Email address configured in `threepid.medium.<3PID medium>.identity.from`    | | ||||||
|  | | `FROM_NAME`         | Name configured in `threepid.medium.<3PID medium>.identity.name`             | | ||||||
|  | | `RECIPIENT_MEDIUM`  | The 3PID medium, like `email` or `msisdn`                                    | | ||||||
|  | | `RECIPIENT_ADDRESS` | The address to which the notification is sent                                | | ||||||
|  |  | ||||||
|  | ### Room invitation | ||||||
|  | Specific placeholders: | ||||||
|  |  | ||||||
|  | | Placeholder         | Purpose                                                                                  | | ||||||
|  | |---------------------|------------------------------------------------------------------------------------------| | ||||||
|  | | `SENDER_ID`         | Matrix ID of the user who made the invite                                                | | ||||||
|  | | `SENDER_NAME`       | Display name of the user who made the invite, if not available/set, empty                | | ||||||
|  | | `SENDER_NAME_OR_ID` | Display name of the user who made the invite. If not available/set, its Matrix ID        | | ||||||
|  | | `INVITE_MEDIUM`     | The 3PID medium for the invite.                                                          | | ||||||
|  | | `INVITE_ADDRESS`    | The 3PID address for the invite.                                                         | | ||||||
|  | | `ROOM_ID`           | The Matrix ID of the Room in which the invite took place                                 | | ||||||
|  | | `ROOM_NAME`         | The Name of the room in which the invite took place. If not available/set, empty         | | ||||||
|  | | `ROOM_NAME_OR_ID`   | The Name of the room in which the invite took place. If not available/set, its Matrix ID | | ||||||
|  | | `REGISTER_URL`      | The URL to provide to the user allowing them to register their account, if needed        | | ||||||
|  |  | ||||||
|  | ### Validation of 3PID Session | ||||||
|  | Specific placeholders: | ||||||
|  |  | ||||||
|  | | Placeholder        | Purpose                                                                              | | ||||||
|  | |--------------------|--------------------------------------------------------------------------------------| | ||||||
|  | | `VALIDATION_LINK`  | URL, including token, to validate the 3PID session.                                  | | ||||||
|  | | `VALIDATION_TOKEN` | The token needed to validate the session, in case the user cannot use the link.      | | ||||||
|  | | `NEXT_URL`         | URL to redirect to after the sessions has been validated.                            | | ||||||
|  |  | ||||||
|  | ## Templates | ||||||
|  | mxisd comes with a set of builtin templates to easily get started. Those templates can be found | ||||||
|  | [in the repository](https://github.com/kamax-matrix/mxisd/tree/master/src/main/resources/threepids). If you want to use | ||||||
|  | customized templates, we recommend using the builtin templates as a starting point. | ||||||
|  |  | ||||||
|  | > **NOTE**: The link above point to the latest version of the built-in templates. Those might be different from your | ||||||
|  | version. Be sure to view the repo at the current tag. | ||||||
|  |  | ||||||
| ## Configuration | ## Configuration | ||||||
|  | All configuration is specific to [3PID mediums](https://matrix.org/docs/spec/appendices.html#pid-types) and happen | ||||||
|  | under the namespace `threepid.medium.<medium>.generators.template`. | ||||||
|  |  | ||||||
|  | Under such namespace, the following keys are available: | ||||||
|  | - `invite`: Path to the 3PID invite notification template | ||||||
|  | - `session.validation`: Path to the 3PID session validation notification template | ||||||
|  | - `session.unbind.fraudulent`: Path to the 3PID session fraudulent unbind notification template | ||||||
|  | - `generic.matrixId`: Path to the Matrix ID invite notification template | ||||||
|  | - `placeholder`: Map of key/values to set static values for some placeholders. | ||||||
|  |  | ||||||
|  | The `placeholder` map supports the following keys, mapped to their respective template placeholders: | ||||||
|  | - `REGISTER_URL` | ||||||
|  |  | ||||||
|  | ### Example | ||||||
|  | #### Simple | ||||||
|  | ```yaml | ||||||
|  | threepid: | ||||||
|  |   medium: | ||||||
|  |     email: | ||||||
|  |       generators: | ||||||
|  |         template: | ||||||
|  |           placeholder: | ||||||
|  |             REGISTER_URL: 'https://matrix-client.example.org' | ||||||
|  | ``` | ||||||
|  | In this configuration, the builtin templates are used and a static value for the `REGISTER_URL` is set, allowing to point | ||||||
|  | a newly invited user to a webapp allowing the creation of its account on the server. | ||||||
|  |  | ||||||
|  | #### Advanced | ||||||
| To configure paths to the various templates: | To configure paths to the various templates: | ||||||
| ```yaml | ```yaml | ||||||
| threepid: | threepid: | ||||||
|   medium: |   medium: | ||||||
|     <YOUR 3PID MEDIUM HERE>: |     email: | ||||||
|       generators: |       generators: | ||||||
|         template: |         template: | ||||||
|           invite: '/path/to/invite-template.eml' |           invite: '/path/to/invite-template.eml' | ||||||
|           session: |           session: | ||||||
|             validation: '/path/to/validate-template.eml' |             validation: '/path/to/validate-template.eml' | ||||||
|             unbind: |             unbind: | ||||||
|               frandulent: '/path/to/unbind-fraudulent-template.eml' |               fraudulent: '/path/to/unbind-fraudulent-template.eml' | ||||||
|           generic: |           generic: | ||||||
|             matrixId: '/path/to/mxid-invite-template.eml' |             matrixId: '/path/to/mxid-invite-template.eml' | ||||||
|  |           placeholder: | ||||||
|  |             REGISTER_URL: 'https://matrix-client.example.org' | ||||||
| ``` | ``` | ||||||
| The `template` generator is usually the default, so no further configuration is needed. | In this configuration, a custom template is used for each event and a static value for the `REGISTER_URL` is set. | ||||||
|  |  | ||||||
| ##  Global placeholders |  | ||||||
| | Placeholder           | Purpose                                                                      | |  | ||||||
| |-----------------------|------------------------------------------------------------------------------| |  | ||||||
| | `%DOMAIN%`            | Identity server authoritative domain, as configured in `matrix.domain`       | |  | ||||||
| | `%DOMAIN_PRETTY%`     | Same as `%DOMAIN%` with the first letter upper case and all other lower case | |  | ||||||
| | `%FROM_EMAIL%`        | Email address configured in `threepid.medium.<3PID medium>.identity.from`    | |  | ||||||
| | `%FROM_NAME%`         | Name configured in `threepid.medium.<3PID medium>.identity.name`             | |  | ||||||
| | `%RECIPIENT_MEDIUM%`  | The 3PID medium, like `email` or `msisdn`                                    | |  | ||||||
| | `%RECIPIENT_ADDRESS%` | The address to which the notification is sent                                | |  | ||||||
|  |  | ||||||
| ## Events |  | ||||||
| ### Room invitation |  | ||||||
| This template is used when someone is invited into a room using an email address which has no known bind to a Matrix ID. |  | ||||||
| #### Placeholders |  | ||||||
| | Placeholder           | Purpose                                                                                  | |  | ||||||
| |-----------------------|------------------------------------------------------------------------------------------| |  | ||||||
| | `%SENDER_ID%`         | Matrix ID of the user who made the invite                                                | |  | ||||||
| | `%SENDER_NAME%`       | Display name of the user who made the invite, if not available/set, empty                | |  | ||||||
| | `%SENDER_NAME_OR_ID%` | Display name of the user who made the invite. If not available/set, its Matrix ID        | |  | ||||||
| | `%INVITE_MEDIUM%`     | The 3PID medium for the invite.                                                          | |  | ||||||
| | `%INVITE_ADDRESS%`    | The 3PID address for the invite.                                                         | |  | ||||||
| | `%ROOM_ID%`           | The Matrix ID of the Room in which the invite took place                                 | |  | ||||||
| | `%ROOM_NAME%`         | The Name of the room in which the invite took place. If not available/set, empty         | |  | ||||||
| | `%ROOM_NAME_OR_ID%`   | The Name of the room in which the invite took place. If not available/set, its Matrix ID | |  | ||||||
|  |  | ||||||
| ### Validation of 3PID Session |  | ||||||
| This template is used when to user which added their 3PID address to their profile/settings and the session policy |  | ||||||
| allows at least local sessions.   |  | ||||||
|  |  | ||||||
| #### Placeholders |  | ||||||
| | Placeholder          | Purpose                                                                              | |  | ||||||
| |----------------------|--------------------------------------------------------------------------------------| |  | ||||||
| | `%VALIDATION_LINK%`  | URL, including token, to validate the 3PID session.                                  | |  | ||||||
| | `%VALIDATION_TOKEN%` | The token needed to validate the session, in case the user cannot use the link.      | |  | ||||||
| | `%NEXT_URL%`         | URL to redirect to after the sessions has been validated.                            | |  | ||||||
|   | |||||||
| @@ -19,6 +19,11 @@ If you use the [Docker image](install/docker.md), this goes to the container log | |||||||
|  |  | ||||||
| For any other platform, please refer to your package maintainer. | For any other platform, please refer to your package maintainer. | ||||||
|  |  | ||||||
|  | ### Increase verbosity | ||||||
|  | To increase log verbosity and better track issues, the following means are available: | ||||||
|  | - Add the `-v` command line parameter | ||||||
|  | - Use the environment variable and value `MXISD_LOG_LEVEL=debug` | ||||||
|  |  | ||||||
| ### Reading them | ### Reading them | ||||||
| Before reporting an issue, it is important to produce clean and complete logs so they can be understood. | Before reporting an issue, it is important to produce clean and complete logs so they can be understood. | ||||||
|  |  | ||||||
|   | |||||||
| @@ -14,6 +14,11 @@ | |||||||
| # NOTE: in Synapse Homeserver, the Matrix domain is defined as 'server_name' in configuration file. | # 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. | # 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: | matrix: | ||||||
|   domain: '' |   domain: '' | ||||||
|  |  | ||||||
| @@ -86,19 +91,19 @@ threepid: | |||||||
|           # SMTP host |           # SMTP host | ||||||
|           host: "smtp.example.org" |           host: "smtp.example.org" | ||||||
|  |  | ||||||
|           # SMTP port |           # TLS mode for the connection | ||||||
|           port: 587 |  | ||||||
|  |  | ||||||
|           # STARTLS mode for the connection. |  | ||||||
|           # SSL/TLS is currently not supported. See https://github.com/kamax-matrix/mxisd/issues/125 |  | ||||||
|           # |  | ||||||
|           # Possible values: |           # Possible values: | ||||||
|           #  0    Disable any kind of TLS entirely |           #  0    Disable any kind of TLS entirely | ||||||
|           #  1    Enable STARTLS if supported by server (default) |           #  1    Enable STARTLS if supported by server (default) | ||||||
|           #  2    Force STARTLS and fail if not available |           #  2    Force STARTLS and fail if not available | ||||||
|  |           #  3    Use full TLS/SSL instead of STARTLS | ||||||
|           # |           # | ||||||
|           tls: 1 |           tls: 1 | ||||||
|  |  | ||||||
|  |           # SMTP port | ||||||
|  |           # Be sure to adapt depending on your TLS choice, if changed from default | ||||||
|  |           port: 587 | ||||||
|  |  | ||||||
|           # Login for SMTP |           # Login for SMTP | ||||||
|           login: "matrix-identity@example.org" |           login: "matrix-identity@example.org" | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,5 +3,7 @@ Maintainer: Kamax.io <foss@kamax.io> | |||||||
| Homepage: https://github.com/kamax-matrix/mxisd | Homepage: https://github.com/kamax-matrix/mxisd | ||||||
| Description: Federated Matrix Identity Server | Description: Federated Matrix Identity Server | ||||||
| Architecture: all | Architecture: all | ||||||
|  | Section: net | ||||||
|  | Priority: optional | ||||||
| Depends: openjdk-8-jre | openjdk-8-jre-headless | openjdk-8-jdk | openjdk-8-jdk-headless | Depends: openjdk-8-jre | openjdk-8-jre-headless | openjdk-8-jdk | openjdk-8-jdk-headless | ||||||
| Version: 0 | Version: 0 | ||||||
|   | |||||||
| @@ -21,23 +21,30 @@ | |||||||
| package io.kamax.mxisd; | package io.kamax.mxisd; | ||||||
|  |  | ||||||
| import io.kamax.mxisd.config.MxisdConfig; | 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.OptionsHandler; | ||||||
| import io.kamax.mxisd.http.undertow.handler.SaneHandler; | 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.AsNotFoundHandler; | ||||||
| import io.kamax.mxisd.http.undertow.handler.as.v1.AsTransactionHandler; | 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.RestAuthHandler; | ||||||
| import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginGetHandler; | 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.LoginHandler; | ||||||
| import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginPostHandler; | 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.directory.v1.UserDirectorySearchHandler; | ||||||
| import io.kamax.mxisd.http.undertow.handler.identity.v1.*; | 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.InternalProfileHandler; | ||||||
| import io.kamax.mxisd.http.undertow.handler.profile.v1.ProfileHandler; | 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.StatusHandler; | ||||||
|  | import io.kamax.mxisd.http.undertow.handler.status.VersionHandler; | ||||||
| import io.undertow.Handlers; | import io.undertow.Handlers; | ||||||
| import io.undertow.Undertow; | import io.undertow.Undertow; | ||||||
| import io.undertow.server.HttpHandler; | import io.undertow.server.HttpHandler; | ||||||
|  |  | ||||||
|  | import java.util.Objects; | ||||||
|  |  | ||||||
| public class HttpMxisd { | public class HttpMxisd { | ||||||
|  |  | ||||||
|     // Core |     // Core | ||||||
| @@ -46,6 +53,12 @@ public class HttpMxisd { | |||||||
|     // I/O |     // I/O | ||||||
|     private Undertow httpSrv; |     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) { |     public HttpMxisd(MxisdConfig cfg) { | ||||||
|         m = new Mxisd(cfg); |         m = new Mxisd(cfg); | ||||||
|     } |     } | ||||||
| @@ -54,10 +67,12 @@ public class HttpMxisd { | |||||||
|         m.start(); |         m.start(); | ||||||
|  |  | ||||||
|         HttpHandler helloHandler = SaneHandler.around(new HelloHandler()); |         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 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 sessValidateHandler = SaneHandler.around(new SessionValidateHandler(m.getSession(), m.getConfig().getServer(), m.getConfig().getView())); |  | ||||||
|  |         HttpHandler storeInvHandler = SaneHandler.around(new StoreInviteHandler(m.getConfig().getServer(), m.getInvite(), m.getKeyManager())); | ||||||
|  |  | ||||||
|         httpSrv = Undertow.builder().addHttpListener(m.getConfig().getServer().getPort(), "0.0.0.0").setHandler(Handlers.routing() |         httpSrv = Undertow.builder().addHttpListener(m.getConfig().getServer().getPort(), "0.0.0.0").setHandler(Handlers.routing() | ||||||
|  |  | ||||||
| @@ -65,6 +80,7 @@ public class HttpMxisd { | |||||||
|  |  | ||||||
|                 // Status endpoints |                 // Status endpoints | ||||||
|                 .get(StatusHandler.Path, SaneHandler.around(new StatusHandler())) |                 .get(StatusHandler.Path, SaneHandler.around(new StatusHandler())) | ||||||
|  |                 .get(VersionHandler.Path, SaneHandler.around(new VersionHandler())) | ||||||
|  |  | ||||||
|                 // Authentication endpoints |                 // Authentication endpoints | ||||||
|                 .get(LoginHandler.Path, SaneHandler.around(new LoginGetHandler(m.getAuth(), m.getHttpClient()))) |                 .get(LoginHandler.Path, SaneHandler.around(new LoginGetHandler(m.getAuth(), m.getHttpClient()))) | ||||||
| @@ -77,40 +93,53 @@ public class HttpMxisd { | |||||||
|                 // Key endpoints |                 // Key endpoints | ||||||
|                 .get(KeyGetHandler.Path, SaneHandler.around(new KeyGetHandler(m.getKeyManager()))) |                 .get(KeyGetHandler.Path, SaneHandler.around(new KeyGetHandler(m.getKeyManager()))) | ||||||
|                 .get(RegularKeyIsValidHandler.Path, SaneHandler.around(new RegularKeyIsValidHandler(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 |                 // Identity endpoints | ||||||
|                 .get(HelloHandler.Path, helloHandler) |                 .get(HelloHandler.Path, helloHandler) | ||||||
|                 .get(HelloHandler.Path + "/", helloHandler) // Be lax with possibly trailing slash |                 .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(BulkLookupHandler.Path, SaneHandler.around(new BulkLookupHandler(m.getIdentity()))) | ||||||
|                 .post(StoreInviteHandler.Path, storeInvHandler) |                 .post(StoreInviteHandler.Path, storeInvHandler) | ||||||
|                 .post(SessionStartHandler.Path, SaneHandler.around(new SessionStartHandler(m.getSession()))) |                 .post(SessionStartHandler.Path, SaneHandler.around(new SessionStartHandler(m.getSession()))) | ||||||
|                 .get(SessionValidateHandler.Path, sessValidateHandler) |                 .get(SessionValidateHandler.Path, SaneHandler.around(new SessionValidationGetHandler(m.getSession(), m.getConfig()))) | ||||||
|                 .post(SessionValidateHandler.Path, sessValidateHandler) |                 .post(SessionValidateHandler.Path, SaneHandler.around(new SessionValidationPostHandler(m.getSession()))) | ||||||
|                 .get(SessionTpidGetValidatedHandler.Path, SaneHandler.around(new SessionTpidGetValidatedHandler(m.getSession()))) |                 .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(SessionTpidUnbindHandler.Path, SaneHandler.around(new SessionTpidUnbindHandler(m.getSession()))) | ||||||
|  |                 .post(SignEd25519Handler.Path, SaneHandler.around(new SignEd25519Handler(m.getConfig(), m.getInvite(), m.getSign()))) | ||||||
|  |  | ||||||
|                 // Profile endpoints |                 // Profile endpoints | ||||||
|                 .get(ProfileHandler.Path, SaneHandler.around(new ProfileHandler(m.getProfile()))) |                 .get(ProfileHandler.Path, SaneHandler.around(new ProfileHandler(m.getProfile()))) | ||||||
|                 .get(InternalProfileHandler.Path, SaneHandler.around(new InternalProfileHandler(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 |                 // Application Service endpoints | ||||||
|                 .get("/_matrix/app/v1/users/**", asNotFoundHandler) |                 .get(AsUserHandler.Path, asUserHandler) | ||||||
|                 .get("/users/**", asNotFoundHandler) // Legacy endpoint |  | ||||||
|                 .get("/_matrix/app/v1/rooms/**", asNotFoundHandler) |                 .get("/_matrix/app/v1/rooms/**", asNotFoundHandler) | ||||||
|                 .get("/rooms/**", asNotFoundHandler) // Legacy endpoint |  | ||||||
|                 .put(AsTransactionHandler.Path, asTxnHandler) |                 .put(AsTransactionHandler.Path, asTxnHandler) | ||||||
|  |  | ||||||
|  |                 .get("/users/{" + AsUserHandler.ID + "}", asUserHandler) // Legacy endpoint | ||||||
|  |                 .get("/rooms/**", asNotFoundHandler) // Legacy endpoint | ||||||
|                 .put("/transactions/{" + AsTransactionHandler.ID + "}", asTxnHandler) // Legacy endpoint |                 .put("/transactions/{" + AsTransactionHandler.ID + "}", asTxnHandler) // Legacy endpoint | ||||||
|  |  | ||||||
|  |                 // Banned endpoints | ||||||
|  |                 .get(InternalInfoHandler.Path, SaneHandler.around(new InternalInfoHandler())) | ||||||
|  |  | ||||||
|         ).build(); |         ).build(); | ||||||
|  |  | ||||||
|         httpSrv.start(); |         httpSrv.start(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void stop() { |     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(); |         m.stop(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -20,8 +20,6 @@ | |||||||
|  |  | ||||||
| package io.kamax.mxisd; | 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.as.AppSvcManager; | ||||||
| import io.kamax.mxisd.auth.AuthManager; | import io.kamax.mxisd.auth.AuthManager; | ||||||
| import io.kamax.mxisd.auth.AuthProviders; | 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.backend.sql.synapse.Synapse; | ||||||
| import io.kamax.mxisd.config.MxisdConfig; | import io.kamax.mxisd.config.MxisdConfig; | ||||||
| import io.kamax.mxisd.crypto.CryptoFactory; | 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.DirectoryManager; | ||||||
| import io.kamax.mxisd.directory.DirectoryProviders; | import io.kamax.mxisd.directory.DirectoryProviders; | ||||||
| import io.kamax.mxisd.dns.ClientDnsOverwrite; | import io.kamax.mxisd.dns.ClientDnsOverwrite; | ||||||
| @@ -40,15 +41,18 @@ import io.kamax.mxisd.lookup.provider.BridgeFetcher; | |||||||
| import io.kamax.mxisd.lookup.provider.RemoteIdentityServerFetcher; | import io.kamax.mxisd.lookup.provider.RemoteIdentityServerFetcher; | ||||||
| import io.kamax.mxisd.lookup.strategy.LookupStrategy; | import io.kamax.mxisd.lookup.strategy.LookupStrategy; | ||||||
| import io.kamax.mxisd.lookup.strategy.RecursivePriorityLookupStrategy; | import io.kamax.mxisd.lookup.strategy.RecursivePriorityLookupStrategy; | ||||||
|  | import io.kamax.mxisd.matrix.HomeserverFederationResolver; | ||||||
| import io.kamax.mxisd.matrix.IdentityServerUtils; | import io.kamax.mxisd.matrix.IdentityServerUtils; | ||||||
| import io.kamax.mxisd.notification.NotificationHandlerSupplier; | import io.kamax.mxisd.notification.NotificationHandlerSupplier; | ||||||
| import io.kamax.mxisd.notification.NotificationHandlers; | import io.kamax.mxisd.notification.NotificationHandlers; | ||||||
| import io.kamax.mxisd.notification.NotificationManager; | import io.kamax.mxisd.notification.NotificationManager; | ||||||
| import io.kamax.mxisd.profile.ProfileManager; | import io.kamax.mxisd.profile.ProfileManager; | ||||||
| import io.kamax.mxisd.profile.ProfileProviders; | import io.kamax.mxisd.profile.ProfileProviders; | ||||||
|  | import io.kamax.mxisd.registration.RegistrationManager; | ||||||
| import io.kamax.mxisd.session.SessionManager; | import io.kamax.mxisd.session.SessionManager; | ||||||
| import io.kamax.mxisd.storage.IStorage; | import io.kamax.mxisd.storage.IStorage; | ||||||
| import io.kamax.mxisd.storage.ormlite.OrmLiteSqlStorage; | 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.CloseableHttpClient; | ||||||
| import org.apache.http.impl.client.HttpClients; | import org.apache.http.impl.client.HttpClients; | ||||||
|  |  | ||||||
| @@ -56,6 +60,10 @@ import java.util.ServiceLoader; | |||||||
|  |  | ||||||
| public class Mxisd { | 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 MxisdConfig cfg; | ||||||
|  |  | ||||||
|     private CloseableHttpClient httpClient; |     private CloseableHttpClient httpClient; | ||||||
| @@ -63,8 +71,9 @@ public class Mxisd { | |||||||
|  |  | ||||||
|     private IStorage store; |     private IStorage store; | ||||||
|  |  | ||||||
|     private KeyManager keyMgr; |     private Ed25519KeyManager keyMgr; | ||||||
|     private SignatureManager signMgr; |     private SignatureManager signMgr; | ||||||
|  |     private ClientDnsOverwrite clientDns; | ||||||
|  |  | ||||||
|     // Features |     // Features | ||||||
|     private AuthManager authMgr; |     private AuthManager authMgr; | ||||||
| @@ -75,6 +84,10 @@ public class Mxisd { | |||||||
|     private AppSvcManager asHander; |     private AppSvcManager asHander; | ||||||
|     private SessionManager sessMgr; |     private SessionManager sessMgr; | ||||||
|     private NotificationManager notifMgr; |     private NotificationManager notifMgr; | ||||||
|  |     private RegistrationManager regMgr; | ||||||
|  |  | ||||||
|  |     // HS-specific classes | ||||||
|  |     private Synapse synapse; | ||||||
|  |  | ||||||
|     public Mxisd(MxisdConfig cfg) { |     public Mxisd(MxisdConfig cfg) { | ||||||
|         this.cfg = cfg.build(); |         this.cfg = cfg.build(); | ||||||
| @@ -82,22 +95,23 @@ public class Mxisd { | |||||||
|  |  | ||||||
|     private void build() { |     private void build() { | ||||||
|         httpClient = HttpClients.custom() |         httpClient = HttpClients.custom() | ||||||
|                 .setUserAgent("mxisd") |                 .setUserAgent(Agent) | ||||||
|                 .setMaxConnPerRoute(Integer.MAX_VALUE) |                 .setMaxConnPerRoute(Integer.MAX_VALUE) | ||||||
|                 .setMaxConnTotal(Integer.MAX_VALUE) |                 .setMaxConnTotal(Integer.MAX_VALUE) | ||||||
|                 .build(); |                 .build(); | ||||||
|  |  | ||||||
|  |         FederationDnsOverwrite fedDns = new FederationDnsOverwrite(cfg.getDns().getOverwrite()); | ||||||
|  |         HomeserverFederationResolver resolver = new HomeserverFederationResolver(fedDns, httpClient); | ||||||
|         IdentityServerUtils.setHttpClient(httpClient); |         IdentityServerUtils.setHttpClient(httpClient); | ||||||
|         srvFetcher = new RemoteIdentityServerFetcher(httpClient); |         srvFetcher = new RemoteIdentityServerFetcher(httpClient); | ||||||
|  |  | ||||||
|         store = new OrmLiteSqlStorage(cfg); |         store = new OrmLiteSqlStorage(cfg); | ||||||
|         keyMgr = CryptoFactory.getKeyManager(cfg.getKey()); |         keyMgr = CryptoFactory.getKeyManager(cfg.getKey()); | ||||||
|         signMgr = CryptoFactory.getSignatureManager(keyMgr, cfg.getServer()); |         signMgr = CryptoFactory.getSignatureManager(keyMgr); | ||||||
|         ClientDnsOverwrite clientDns = new ClientDnsOverwrite(cfg.getDns().getOverwrite()); |         clientDns = new ClientDnsOverwrite(cfg.getDns().getOverwrite()); | ||||||
|         FederationDnsOverwrite fedDns = new FederationDnsOverwrite(cfg.getDns().getOverwrite()); |  | ||||||
|         Synapse synapse = new Synapse(cfg.getSynapseSql()); |  | ||||||
|         BridgeFetcher bridgeFetcher = new BridgeFetcher(cfg.getLookup().getRecursive().getBridge(), srvFetcher); |  | ||||||
|  |  | ||||||
|  |         synapse = new Synapse(cfg.getSynapseSql()); | ||||||
|  |         BridgeFetcher bridgeFetcher = new BridgeFetcher(cfg.getLookup().getRecursive().getBridge(), srvFetcher); | ||||||
|         ServiceLoader.load(IdentityStoreSupplier.class).iterator().forEachRemaining(p -> p.accept(this)); |         ServiceLoader.load(IdentityStoreSupplier.class).iterator().forEachRemaining(p -> p.accept(this)); | ||||||
|         ServiceLoader.load(NotificationHandlerSupplier.class).iterator().forEachRemaining(p -> p.accept(this)); |         ServiceLoader.load(NotificationHandlerSupplier.class).iterator().forEachRemaining(p -> p.accept(this)); | ||||||
|  |  | ||||||
| @@ -105,10 +119,11 @@ public class Mxisd { | |||||||
|         pMgr = new ProfileManager(ProfileProviders.get(), clientDns, httpClient); |         pMgr = new ProfileManager(ProfileProviders.get(), clientDns, httpClient); | ||||||
|         notifMgr = new NotificationManager(cfg.getNotification(), NotificationHandlers.get()); |         notifMgr = new NotificationManager(cfg.getNotification(), NotificationHandlers.get()); | ||||||
|         sessMgr = new SessionManager(cfg.getSession(), cfg.getMatrix(), store, notifMgr, idStrategy, httpClient); |         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, resolver, notifMgr, pMgr); | ||||||
|         authMgr = new AuthManager(cfg, AuthProviders.get(), idStrategy, invMgr, clientDns, httpClient); |         authMgr = new AuthManager(cfg, AuthProviders.get(), idStrategy, invMgr, clientDns, httpClient); | ||||||
|         dirMgr = new DirectoryManager(cfg.getDirectory(), clientDns, httpClient, DirectoryProviders.get()); |         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() { |     public MxisdConfig getConfig() { | ||||||
| @@ -119,6 +134,10 @@ public class Mxisd { | |||||||
|         return httpClient; |         return httpClient; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public ClientDnsOverwrite getClientDns() { | ||||||
|  |         return clientDns; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     public IRemoteIdentityServerFetcher getServerFetcher() { |     public IRemoteIdentityServerFetcher getServerFetcher() { | ||||||
|         return srvFetcher; |         return srvFetcher; | ||||||
|     } |     } | ||||||
| @@ -127,7 +146,7 @@ public class Mxisd { | |||||||
|         return keyMgr; |         return keyMgr; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public InvitationManager getInvitationManager() { |     public InvitationManager getInvite() { | ||||||
|         return invMgr; |         return invMgr; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -155,6 +174,10 @@ public class Mxisd { | |||||||
|         return signMgr; |         return signMgr; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public RegistrationManager getReg() { | ||||||
|  |         return regMgr; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     public AppSvcManager getAs() { |     public AppSvcManager getAs() { | ||||||
|         return asHander; |         return asHander; | ||||||
|     } |     } | ||||||
| @@ -163,6 +186,14 @@ public class Mxisd { | |||||||
|         return notifMgr; |         return notifMgr; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public IStorage getStore() { | ||||||
|  |         return store; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public Synapse getSynapse() { | ||||||
|  |         return synapse; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     public void start() { |     public void start() { | ||||||
|         build(); |         build(); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -36,18 +36,38 @@ public class MxisdStandaloneExec { | |||||||
|     private static final Logger log = LoggerFactory.getLogger("App"); |     private static final Logger log = LoggerFactory.getLogger("App"); | ||||||
|  |  | ||||||
|     public static void main(String[] args) { |     public static void main(String[] args) { | ||||||
|         try { |         String logLevel = System.getenv("MXISD_LOG_LEVEL"); | ||||||
|             log.info("------------- mxisd starting -------------"); |         if (StringUtils.isNotBlank(logLevel)) { | ||||||
|             MxisdConfig cfg = null; |             System.setProperty("org.slf4j.simpleLogger.log.io.kamax.mxisd", logLevel); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             MxisdConfig cfg = null; | ||||||
|             Iterator<String> argsIt = Arrays.asList(args).iterator(); |             Iterator<String> argsIt = Arrays.asList(args).iterator(); | ||||||
|             while (argsIt.hasNext()) { |             while (argsIt.hasNext()) { | ||||||
|                 String arg = argsIt.next(); |                 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("  -v               Increase log level (log more info)"); | ||||||
|  |                     System.out.println("  -vv              Further increase log level"); | ||||||
|  |                     System.out.println(" "); | ||||||
|  |                     System.exit(0); | ||||||
|  |                 } else if (StringUtils.equals(arg, "-v")) { | ||||||
|  |                     System.setProperty("org.slf4j.simpleLogger.log.io.kamax.mxisd", "debug"); | ||||||
|  |                 } else if (StringUtils.equals(arg, "-vv")) { | ||||||
|  |                     System.setProperty("org.slf4j.simpleLogger.log.io.kamax.mxisd", "trace"); | ||||||
|  |                 } else if (StringUtils.equalsAny(arg, "-c", "--config")) { | ||||||
|                     String cfgFile = argsIt.next(); |                     String cfgFile = argsIt.next(); | ||||||
|                     cfg = YamlConfigLoader.loadFromFile(cfgFile); |                     cfg = YamlConfigLoader.loadFromFile(cfgFile); | ||||||
|  |                 } else if (StringUtils.equals("--version", arg)) { | ||||||
|  |                     System.out.println(Mxisd.Version); | ||||||
|  |                     System.exit(0); | ||||||
|                 } else { |                 } else { | ||||||
|                     log.info("Invalid argument: {}", arg); |                     System.err.println("Invalid argument: " + arg); | ||||||
|  |                     System.err.println("Try '--help' for available arguments"); | ||||||
|                     System.exit(1); |                     System.exit(1); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| @@ -56,14 +76,17 @@ public class MxisdStandaloneExec { | |||||||
|                 cfg = YamlConfigLoader.tryLoadFromFile("mxisd.yaml").orElseGet(MxisdConfig::new); |                 cfg = YamlConfigLoader.tryLoadFromFile("mxisd.yaml").orElseGet(MxisdConfig::new); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             log.info("mxisd starting"); | ||||||
|  |             log.info("Version: {}", Mxisd.Version); | ||||||
|  |  | ||||||
|             HttpMxisd mxisd = new HttpMxisd(cfg); |             HttpMxisd mxisd = new HttpMxisd(cfg); | ||||||
|             Runtime.getRuntime().addShutdownHook(new Thread(() -> { |             Runtime.getRuntime().addShutdownHook(new Thread(() -> { | ||||||
|                 mxisd.stop(); |                 mxisd.stop(); | ||||||
|                 log.info("------------- mxisd stopped -------------"); |                 log.info("mxisd stopped"); | ||||||
|             })); |             })); | ||||||
|             mxisd.start(); |             mxisd.start(); | ||||||
|  |  | ||||||
|             log.info("------------- mxisd started -------------"); |             log.info("mxisd started"); | ||||||
|         } catch (ConfigurationException e) { |         } catch (ConfigurationException e) { | ||||||
|             log.error(e.getDetailedMessage()); |             log.error(e.getDetailedMessage()); | ||||||
|             log.error(e.getMessage()); |             log.error(e.getMessage()); | ||||||
|   | |||||||
| @@ -22,76 +22,183 @@ package io.kamax.mxisd.as; | |||||||
|  |  | ||||||
| import com.google.gson.JsonObject; | import com.google.gson.JsonObject; | ||||||
| import io.kamax.matrix.MatrixID; | import io.kamax.matrix.MatrixID; | ||||||
| import io.kamax.matrix.ThreePidMedium; |  | ||||||
| import io.kamax.matrix._MatrixID; | 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.event.EventKey; | ||||||
| import io.kamax.matrix.json.GsonUtil; | import io.kamax.matrix.json.GsonUtil; | ||||||
| import io.kamax.mxisd.backend.sql.synapse.Synapse; | import io.kamax.mxisd.Mxisd; | ||||||
| import io.kamax.mxisd.config.MatrixConfig; | 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.config.MxisdConfig; | ||||||
|  | import io.kamax.mxisd.exception.ConfigurationException; | ||||||
| import io.kamax.mxisd.exception.HttpMatrixException; | import io.kamax.mxisd.exception.HttpMatrixException; | ||||||
| import io.kamax.mxisd.exception.NotAllowedException; | 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.IStorage; | ||||||
| import io.kamax.mxisd.storage.ormlite.dao.ASTransactionDao; | import io.kamax.mxisd.storage.ormlite.dao.ASTransactionDao; | ||||||
| import io.kamax.mxisd.util.GsonParser; | 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.Logger; | ||||||
| import org.slf4j.LoggerFactory; | 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.io.InputStream; | ||||||
|  | import java.nio.charset.StandardCharsets; | ||||||
| import java.time.Instant; | import java.time.Instant; | ||||||
| import java.util.*; | import java.util.*; | ||||||
| import java.util.concurrent.CompletableFuture; | import java.util.concurrent.CompletableFuture; | ||||||
| import java.util.concurrent.ConcurrentHashMap; | import java.util.concurrent.ConcurrentHashMap; | ||||||
| import java.util.stream.Collectors; |  | ||||||
|  |  | ||||||
| public class AppSvcManager { | 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 MatrixApplicationServiceClient client; | ||||||
|     private IStorage store; |     private Map<String, EventTypeProcessor> processors = new HashMap<>(); | ||||||
|     private ProfileManager profiler; |     private Map<String, CompletableFuture<String>> transactionsInProgress = new ConcurrentHashMap<>(); | ||||||
|     private NotificationManager notif; |  | ||||||
|     private Synapse synapse; |  | ||||||
|  |  | ||||||
|     private Map<String, CompletableFuture<String>> 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(); |         We process the configuration to make sure all is fine and setting default values if needed | ||||||
|         this.store = store; |          */ | ||||||
|         this.profiler = profiler; |  | ||||||
|         this.notif = notif; |  | ||||||
|         this.synapse = synapse; |  | ||||||
|  |  | ||||||
|         parser = new GsonParser(); |         // By default, the feature is enabled | ||||||
|         transactionsInProgress = new ConcurrentHashMap<>(); |         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<String> 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) { |     public AppSvcManager withToken(String token) { | ||||||
|  |         ensureEnabled(); | ||||||
|  |  | ||||||
|         if (StringUtils.isBlank(token)) { |         if (StringUtils.isBlank(token)) { | ||||||
|             throw new HttpMatrixException(401, "M_UNAUTHORIZED", "No HS 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"); |             throw new NotAllowedException("Invalid HS token"); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         return this; |         return this; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public void processUser(String userId) { | ||||||
|  |         client.createUser(MatrixID.asAcceptable(userId).getLocalPart()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     public CompletableFuture<String> processTransaction(String txnId, InputStream is) { |     public CompletableFuture<String> processTransaction(String txnId, InputStream is) { | ||||||
|  |         ensureEnabled(); | ||||||
|  |  | ||||||
|         if (StringUtils.isEmpty(txnId)) { |         if (StringUtils.isEmpty(txnId)) { | ||||||
|             throw new IllegalArgumentException("Transaction ID cannot be empty"); |             throw new IllegalArgumentException("Transaction ID cannot be empty"); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         synchronized (this) { |         synchronized (this) { | ||||||
|             Optional<ASTransactionDao> dao = store.getTransactionResult(cfg.getListener().getLocalpart(), txnId); |             Optional<ASTransactionDao> dao = store.getTransactionResult(cfg.getUser().getMain(), txnId); | ||||||
|             if (dao.isPresent()) { |             if (dao.isPresent()) { | ||||||
|                 log.info("AS Transaction {} already processed - returning computed result", txnId); |                 log.info("AS Transaction {} already processed - returning computed result", txnId); | ||||||
|                 return CompletableFuture.completedFuture(dao.get().getResult()); |                 return CompletableFuture.completedFuture(dao.get().getResult()); | ||||||
| @@ -122,7 +229,7 @@ public class AppSvcManager { | |||||||
|  |  | ||||||
|             try { |             try { | ||||||
|                 log.info("Saving transaction details to store"); |                 log.info("Saving transaction details to store"); | ||||||
|                 store.insertTransactionResult(cfg.getListener().getLocalpart(), txnId, end, result); |                 store.insertTransactionResult(cfg.getUser().getMain(), txnId, end, result); | ||||||
|             } finally { |             } finally { | ||||||
|                 log.debug("Removing CompletedFuture from transaction map"); |                 log.debug("Removing CompletedFuture from transaction map"); | ||||||
|                 transactionsInProgress.remove(txnId); |                 transactionsInProgress.remove(txnId); | ||||||
| @@ -139,7 +246,7 @@ public class AppSvcManager { | |||||||
|         return future; |         return future; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public void processTransaction(List<JsonObject> eventsJson) { |     private void processTransaction(List<JsonObject> eventsJson) { | ||||||
|         log.info("Processing transaction events: start"); |         log.info("Processing transaction events: start"); | ||||||
|  |  | ||||||
|         eventsJson.forEach(ev -> { |         eventsJson.forEach(ev -> { | ||||||
| @@ -165,54 +272,14 @@ public class AppSvcManager { | |||||||
|             _MatrixID sender = MatrixID.asAcceptable(senderId); |             _MatrixID sender = MatrixID.asAcceptable(senderId); | ||||||
|             log.debug("Sender: {}", senderId); |             log.debug("Sender: {}", senderId); | ||||||
|  |  | ||||||
|             if (!StringUtils.equals("m.room.member", GsonUtil.getStringOrNull(ev, "type"))) { |             String evType = StringUtils.defaultIfBlank(EventKey.Type.getStringOrNull(ev), "<EMPTY/MISSING>"); | ||||||
|                 log.debug("This is not a room membership event, skipping"); |             EventTypeProcessor p = processors.get(evType); | ||||||
|  |             if (Objects.isNull(p)) { | ||||||
|  |                 log.debug("No event processor for type {}, skipping", evType); | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             if (!StringUtils.equals("invite", GsonUtil.getStringOrNull(ev, "membership"))) { |             p.process(ev, sender, roomId); | ||||||
|                 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<String, String> 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); |  | ||||||
|  |  | ||||||
|             log.debug("Event {}: processing end", evId); |             log.debug("Event {}: processing end", evId); | ||||||
|         }); |         }); | ||||||
|   | |||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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); | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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<IThreePidInviteReply> 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"; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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<SingleLookupReply> 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"; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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!"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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); | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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<String, String> 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); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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<String, CommandProcessor> 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(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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<Namespace> users = new ArrayList<>(); | ||||||
|  |         private List<Namespace> aliases = new ArrayList<>(); | ||||||
|  |         private List<Namespace> rooms = new ArrayList<>(); | ||||||
|  |  | ||||||
|  |         public List<Namespace> getUsers() { | ||||||
|  |             return users; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public void setUsers(List<Namespace> users) { | ||||||
|  |             this.users = users; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public List<Namespace> getAliases() { | ||||||
|  |             return aliases; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public void setAliases(List<Namespace> aliases) { | ||||||
|  |             this.aliases = aliases; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public List<Namespace> getRooms() { | ||||||
|  |             return rooms; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public void setRooms(List<Namespace> 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; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -23,7 +23,9 @@ package io.kamax.mxisd.backend.sql; | |||||||
| import io.kamax.matrix.ThreePid; | import io.kamax.matrix.ThreePid; | ||||||
| import io.kamax.matrix._MatrixID; | import io.kamax.matrix._MatrixID; | ||||||
| import io.kamax.matrix._ThreePid; | import io.kamax.matrix._ThreePid; | ||||||
|  | import io.kamax.mxisd.UserIdType; | ||||||
| import io.kamax.mxisd.config.sql.SqlConfig; | import io.kamax.mxisd.config.sql.SqlConfig; | ||||||
|  | import io.kamax.mxisd.exception.InternalServerError; | ||||||
| import io.kamax.mxisd.profile.ProfileProvider; | import io.kamax.mxisd.profile.ProfileProvider; | ||||||
| import org.slf4j.Logger; | import org.slf4j.Logger; | ||||||
| import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||||
| @@ -33,16 +35,14 @@ import java.sql.PreparedStatement; | |||||||
| import java.sql.ResultSet; | import java.sql.ResultSet; | ||||||
| import java.sql.SQLException; | import java.sql.SQLException; | ||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.Collections; |  | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Optional; | import java.util.Optional; | ||||||
|  |  | ||||||
| public abstract class SqlProfileProvider implements ProfileProvider { | 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 SqlConfig.Profile cfg; | ||||||
|  |  | ||||||
|     private SqlConnectionPool pool; |     private SqlConnectionPool pool; | ||||||
|  |  | ||||||
|     public SqlProfileProvider(SqlConfig cfg) { |     public SqlProfileProvider(SqlConfig cfg) { | ||||||
| @@ -50,6 +50,12 @@ public abstract class SqlProfileProvider implements ProfileProvider { | |||||||
|         this.pool = new SqlConnectionPool(cfg); |         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 |     @Override | ||||||
|     public Optional<String> getDisplayName(_MatrixID user) { |     public Optional<String> getDisplayName(_MatrixID user) { | ||||||
|         String stmtSql = cfg.getDisplayName().getQuery(); |         String stmtSql = cfg.getDisplayName().getQuery(); | ||||||
| @@ -94,7 +100,33 @@ public abstract class SqlProfileProvider implements ProfileProvider { | |||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public List<String> getRoles(_MatrixID user) { |     public List<String> getRoles(_MatrixID user) { | ||||||
|         return Collections.emptyList(); |         log.info("Querying roles for {}", user.getId()); | ||||||
|  |  | ||||||
|  |         List<String> 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); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -32,7 +32,7 @@ public class GenericSqlStoreSupplier implements IdentityStoreSupplier { | |||||||
|     @Override |     @Override | ||||||
|     public void accept(Mxisd mxisd) { |     public void accept(Mxisd mxisd) { | ||||||
|         if (mxisd.getConfig().getSql().getAuth().isEnabled()) { |         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()) { |         if (mxisd.getConfig().getSql().getDirectory().isEnabled()) { | ||||||
|   | |||||||
| @@ -43,6 +43,10 @@ public class SynapseQueries { | |||||||
|         return "SELECT medium, address FROM user_threepids WHERE user_id = ?"; |         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) { |     public static String findByDisplayName(String type, String domain) { | ||||||
|         if (StringUtils.equals("sqlite", type)) { |         if (StringUtils.equals("sqlite", type)) { | ||||||
|             return "select " + getUserId(type, domain) + ", displayname from profiles p where displayname like ?"; |             return "select " + getUserId(type, domain) + ", displayname from profiles p where displayname like ?"; | ||||||
|   | |||||||
| @@ -26,9 +26,13 @@ import io.kamax.mxisd.config.MxisdConfig; | |||||||
| import io.kamax.mxisd.directory.DirectoryProviders; | import io.kamax.mxisd.directory.DirectoryProviders; | ||||||
| import io.kamax.mxisd.lookup.ThreePidProviders; | import io.kamax.mxisd.lookup.ThreePidProviders; | ||||||
| import io.kamax.mxisd.profile.ProfileProviders; | import io.kamax.mxisd.profile.ProfileProviders; | ||||||
|  | import org.slf4j.Logger; | ||||||
|  | import org.slf4j.LoggerFactory; | ||||||
|  |  | ||||||
| public class SynapseSqlStoreSupplier implements IdentityStoreSupplier { | public class SynapseSqlStoreSupplier implements IdentityStoreSupplier { | ||||||
|  |  | ||||||
|  |     private static final Logger log = LoggerFactory.getLogger(SynapseSqlStoreSupplier.class); | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void accept(Mxisd mxisd) { |     public void accept(Mxisd mxisd) { | ||||||
|         accept(mxisd.getConfig()); |         accept(mxisd.getConfig()); | ||||||
| @@ -44,6 +48,7 @@ public class SynapseSqlStoreSupplier implements IdentityStoreSupplier { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (cfg.getSynapseSql().getProfile().isEnabled()) { |         if (cfg.getSynapseSql().getProfile().isEnabled()) { | ||||||
|  |             log.debug("Profile is enabled, registering provider"); | ||||||
|             ProfileProviders.register(() -> new SynapseSqlProfileProvider(cfg.getSynapseSql())); |             ProfileProviders.register(() -> new SynapseSqlProfileProvider(cfg.getSynapseSql())); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
							
								
								
									
										287
									
								
								src/main/java/io/kamax/mxisd/config/AppServiceConfig.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										287
									
								
								src/main/java/io/kamax/mxisd/config/AppServiceConfig.java
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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<String> allowedRoles = new ArrayList<>(); | ||||||
|  |  | ||||||
|  |         public Boolean getEnabled() { | ||||||
|  |             return enabled; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public void setEnabled(Boolean enabled) { | ||||||
|  |             this.enabled = enabled; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public List<String> getAllowedRoles() { | ||||||
|  |             return allowedRoles; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public void setAllowedRoles(List<String> 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(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -20,18 +20,53 @@ | |||||||
|  |  | ||||||
| package io.kamax.mxisd.config; | package io.kamax.mxisd.config; | ||||||
|  |  | ||||||
| import io.kamax.mxisd.util.GsonUtil; | import io.kamax.matrix.json.GsonUtil; | ||||||
| import org.slf4j.Logger; | import org.slf4j.Logger; | ||||||
| import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||||
|  |  | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.List; | ||||||
|  |  | ||||||
| public class InvitationConfig { | 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 { |     public static class Resolution { | ||||||
|  |  | ||||||
|         private boolean recursive = true; |         private boolean recursive = true; | ||||||
|         private long timer = 1; |         private long timer = 5; | ||||||
|  |  | ||||||
|         public boolean isRecursive() { |         public boolean isRecursive() { | ||||||
|             return recursive; |             return recursive; | ||||||
| @@ -51,7 +86,43 @@ public class InvitationConfig { | |||||||
|  |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public static class SenderPolicy { | ||||||
|  |  | ||||||
|  |         private List<String> hasRole = new ArrayList<>(); | ||||||
|  |  | ||||||
|  |         public List<String> getHasRole() { | ||||||
|  |             return hasRole; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public void setHasRole(List<String> 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 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() { |     public Resolution getResolution() { | ||||||
|         return resolution; |         return resolution; | ||||||
| @@ -61,9 +132,19 @@ public class InvitationConfig { | |||||||
|         this.resolution = resolution; |         this.resolution = resolution; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public Policies getPolicy() { | ||||||
|  |         return policy; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setPolicy(Policies policy) { | ||||||
|  |         this.policy = policy; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     public void build() { |     public void build() { | ||||||
|         log.info("--- Invite config ---"); |         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())); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 <http://www.gnu.org/licenses/>. |  | ||||||
|  */ |  | ||||||
|  |  | ||||||
| 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); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
| } |  | ||||||
| @@ -63,7 +63,6 @@ public class MatrixConfig { | |||||||
|  |  | ||||||
|     private String domain; |     private String domain; | ||||||
|     private Identity identity = new Identity(); |     private Identity identity = new Identity(); | ||||||
|     private ListenerConfig listener = new ListenerConfig(); |  | ||||||
|  |  | ||||||
|     public String getDomain() { |     public String getDomain() { | ||||||
|         return domain; |         return domain; | ||||||
| @@ -81,14 +80,6 @@ public class MatrixConfig { | |||||||
|         this.identity = identity; |         this.identity = identity; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public ListenerConfig getListener() { |  | ||||||
|         return listener; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void setListener(ListenerConfig listener) { |  | ||||||
|         this.listener = listener; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public void build() { |     public void build() { | ||||||
|         log.info("--- Matrix config ---"); |         log.info("--- Matrix config ---"); | ||||||
|  |  | ||||||
| @@ -99,8 +90,6 @@ public class MatrixConfig { | |||||||
|         log.info("Domain: {}", getDomain()); |         log.info("Domain: {}", getDomain()); | ||||||
|         log.info("Identity:"); |         log.info("Identity:"); | ||||||
|         log.info("\tServers: {}", GsonUtil.get().toJson(identity.getServers())); |         log.info("\tServers: {}", GsonUtil.get().toJson(identity.getServers())); | ||||||
|  |  | ||||||
|         listener.build(); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -83,6 +83,7 @@ public class MxisdConfig { | |||||||
|  |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     private AppServiceConfig appsvc = new AppServiceConfig(); | ||||||
|     private AuthenticationConfig auth = new AuthenticationConfig(); |     private AuthenticationConfig auth = new AuthenticationConfig(); | ||||||
|     private DirectoryConfig directory = new DirectoryConfig(); |     private DirectoryConfig directory = new DirectoryConfig(); | ||||||
|     private Dns dns = new Dns(); |     private Dns dns = new Dns(); | ||||||
| @@ -97,6 +98,7 @@ public class MxisdConfig { | |||||||
|     private MemoryStoreConfig memory = new MemoryStoreConfig(); |     private MemoryStoreConfig memory = new MemoryStoreConfig(); | ||||||
|     private NotificationConfig notification = new NotificationConfig(); |     private NotificationConfig notification = new NotificationConfig(); | ||||||
|     private NetIqLdapConfig netiq = new NetIqLdapConfig(); |     private NetIqLdapConfig netiq = new NetIqLdapConfig(); | ||||||
|  |     private RegisterConfig register = new RegisterConfig(); | ||||||
|     private ServerConfig server = new ServerConfig(); |     private ServerConfig server = new ServerConfig(); | ||||||
|     private SessionConfig session = new SessionConfig(); |     private SessionConfig session = new SessionConfig(); | ||||||
|     private StorageConfig storage = new StorageConfig(); |     private StorageConfig storage = new StorageConfig(); | ||||||
| @@ -107,6 +109,14 @@ public class MxisdConfig { | |||||||
|     private ViewConfig view = new ViewConfig(); |     private ViewConfig view = new ViewConfig(); | ||||||
|     private WordpressConfig wordpress = new WordpressConfig(); |     private WordpressConfig wordpress = new WordpressConfig(); | ||||||
|  |  | ||||||
|  |     public AppServiceConfig getAppsvc() { | ||||||
|  |         return appsvc; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setAppsvc(AppServiceConfig appsvc) { | ||||||
|  |         this.appsvc = appsvc; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     public AuthenticationConfig getAuth() { |     public AuthenticationConfig getAuth() { | ||||||
|         return auth; |         return auth; | ||||||
|     } |     } | ||||||
| @@ -219,6 +229,14 @@ public class MxisdConfig { | |||||||
|         this.netiq = netiq; |         this.netiq = netiq; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public RegisterConfig getRegister() { | ||||||
|  |         return register; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setRegister(RegisterConfig register) { | ||||||
|  |         this.register = register; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     public ServerConfig getServer() { |     public ServerConfig getServer() { | ||||||
|         return server; |         return server; | ||||||
|     } |     } | ||||||
| @@ -297,6 +315,7 @@ public class MxisdConfig { | |||||||
|             log.debug("server.name is empty, using matrix.domain"); |             log.debug("server.name is empty, using matrix.domain"); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         getAppsvc().build(); | ||||||
|         getAuth().build(); |         getAuth().build(); | ||||||
|         getDirectory().build(); |         getDirectory().build(); | ||||||
|         getExec().build(); |         getExec().build(); | ||||||
| @@ -310,6 +329,7 @@ public class MxisdConfig { | |||||||
|         getMemory().build(); |         getMemory().build(); | ||||||
|         getNetiq().build(); |         getNetiq().build(); | ||||||
|         getNotification().build(); |         getNotification().build(); | ||||||
|  |         getRegister().build(); | ||||||
|         getRest().build(); |         getRest().build(); | ||||||
|         getSession().build(); |         getSession().build(); | ||||||
|         getServer().build(); |         getServer().build(); | ||||||
|   | |||||||
							
								
								
									
										201
									
								
								src/main/java/io/kamax/mxisd/config/RegisterConfig.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								src/main/java/io/kamax/mxisd/config/RegisterConfig.java
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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<String> blacklist = new ArrayList<>(); | ||||||
|  |         private List<String> whitelist = new ArrayList<>(); | ||||||
|  |  | ||||||
|  |         public List<String> getBlacklist() { | ||||||
|  |             return blacklist; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public void setBlacklist(List<String> blacklist) { | ||||||
|  |             this.blacklist = blacklist; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public List<String> getWhitelist() { | ||||||
|  |             return whitelist; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public void setWhitelist(List<String> 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<String> buildPatterns(List<String> domains) { | ||||||
|  |             log.debug("Building email policy"); | ||||||
|  |             return domains.stream().map(d -> { | ||||||
|  |                 if (StringUtils.startsWith(d, "*")) { | ||||||
|  |                     log.debug("Found domain and subdomain policy"); | ||||||
|  |                     d = "(.*)" + d.substring(1); | ||||||
|  |                 } else if (StringUtils.startsWith(d, ".")) { | ||||||
|  |                     log.debug("Found subdomain-only policy"); | ||||||
|  |                     d = "(.*)" + d; | ||||||
|  |                 } else { | ||||||
|  |                     log.debug("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<String> domains = buildPatterns(getDomain().getBlacklist()); | ||||||
|  |                 getPattern().getBlacklist().addAll(domains); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (Objects.nonNull(getDomain().getWhitelist())) { | ||||||
|  |                 if (Objects.isNull(getPattern().getWhitelist())) { | ||||||
|  |                     getPattern().setWhitelist(new ArrayList<>()); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 List<String> 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<String, Object> 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<String, Object> getThreepid() { | ||||||
|  |             return threepid; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public void setThreepid(Map<String, Object> 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.debug("--- Registration config ---"); | ||||||
|  |  | ||||||
|  |         log.debug("Before Build"); | ||||||
|  |         log.debug(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.debug("After Build"); | ||||||
|  |         log.debug(GsonUtil.getPrettyForLog(this)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -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 { |     public static class Profile { | ||||||
|  |  | ||||||
|         private Boolean enabled; |         private Boolean enabled; | ||||||
|         private ProfileDisplayName displayName = new ProfileDisplayName(); |         private ProfileDisplayName displayName = new ProfileDisplayName(); | ||||||
|         private ProfileThreepids threepid = new ProfileThreepids(); |         private ProfileThreepids threepid = new ProfileThreepids(); | ||||||
|  |         private ProfileRoles role = new ProfileRoles(); | ||||||
|  |  | ||||||
|         public Boolean isEnabled() { |         public Boolean isEnabled() { | ||||||
|             return enabled; |             return enabled; | ||||||
| @@ -223,6 +247,14 @@ public abstract class SqlConfig { | |||||||
|             this.threepid = threepid; |             this.threepid = threepid; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         public ProfileRoles getRole() { | ||||||
|  |             return role; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         public void setRole(ProfileRoles role) { | ||||||
|  |             this.role = role; | ||||||
|  |         } | ||||||
|  |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private boolean enabled; |     private boolean enabled; | ||||||
| @@ -323,10 +355,11 @@ public abstract class SqlConfig { | |||||||
|             log.info("3PID mapping query: {}", getIdentity().getQuery()); |             log.info("3PID mapping query: {}", getIdentity().getQuery()); | ||||||
|             log.info("Identity medium queries: {}", GsonUtil.build().toJson(getIdentity().getMedium())); |             log.info("Identity medium queries: {}", GsonUtil.build().toJson(getIdentity().getMedium())); | ||||||
|             log.info("Profile:"); |             log.info("Profile:"); | ||||||
|             log.info("\tEnabled: {}", getProfile().isEnabled()); |             log.info("  Enabled: {}", getProfile().isEnabled()); | ||||||
|             if (getProfile().isEnabled()) { |             if (getProfile().isEnabled()) { | ||||||
|                 log.info("\tDisplay name query: {}", getProfile().getDisplayName().getQuery()); |                 log.info("  Display name query: {}", getProfile().getDisplayName().getQuery()); | ||||||
|                 log.info("\tProfile 3PID query: {}", getProfile().getThreepid().getQuery()); |                 log.info("  Profile 3PID query: {}", getProfile().getThreepid().getQuery()); | ||||||
|  |                 log.info("  Role query: {}", getProfile().getRole().getQuery()); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -20,6 +20,7 @@ | |||||||
|  |  | ||||||
| package io.kamax.mxisd.config.sql.synapse; | package io.kamax.mxisd.config.sql.synapse; | ||||||
|  |  | ||||||
|  | import io.kamax.mxisd.UserIdType; | ||||||
| import io.kamax.mxisd.backend.sql.synapse.SynapseQueries; | import io.kamax.mxisd.backend.sql.synapse.SynapseQueries; | ||||||
| import io.kamax.mxisd.config.sql.SqlConfig; | import io.kamax.mxisd.config.sql.SqlConfig; | ||||||
| import org.apache.commons.lang.StringUtils; | import org.apache.commons.lang.StringUtils; | ||||||
| @@ -48,9 +49,17 @@ public class SynapseSqlProviderConfig extends SqlConfig { | |||||||
|             if (StringUtils.isBlank(getProfile().getDisplayName().getQuery())) { |             if (StringUtils.isBlank(getProfile().getDisplayName().getQuery())) { | ||||||
|                 getProfile().getDisplayName().setQuery(SynapseQueries.getDisplayName()); |                 getProfile().getDisplayName().setQuery(SynapseQueries.getDisplayName()); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             if (StringUtils.isBlank(getProfile().getThreepid().getQuery())) { |             if (StringUtils.isBlank(getProfile().getThreepid().getQuery())) { | ||||||
|                 getProfile().getThreepid().setQuery(SynapseQueries.getThreepids()); |                 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(); |         printConfig(); | ||||||
|   | |||||||
| @@ -77,6 +77,7 @@ public class GenericTemplateConfig { | |||||||
|     private String invite; |     private String invite; | ||||||
|     private Session session = new Session(); |     private Session session = new Session(); | ||||||
|     private Map<String, String> generic = new HashMap<>(); |     private Map<String, String> generic = new HashMap<>(); | ||||||
|  |     private Map<String, String> placeholder = new HashMap<>(); | ||||||
|  |  | ||||||
|     public String getInvite() { |     public String getInvite() { | ||||||
|         return invite; |         return invite; | ||||||
| @@ -98,4 +99,12 @@ public class GenericTemplateConfig { | |||||||
|         this.generic = generic; |         this.generic = generic; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public Map<String, String> getPlaceholder() { | ||||||
|  |         return placeholder; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public void setPlaceholder(Map<String, String> placeholder) { | ||||||
|  |         this.placeholder = placeholder; | ||||||
|  |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -20,9 +20,12 @@ | |||||||
|  |  | ||||||
| package io.kamax.mxisd.crypto; | package io.kamax.mxisd.crypto; | ||||||
|  |  | ||||||
| import io.kamax.matrix.crypto.*; |  | ||||||
| import io.kamax.mxisd.config.KeyConfig; | 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.io.FileUtils; | ||||||
| import org.apache.commons.lang3.StringUtils; | import org.apache.commons.lang3.StringUtils; | ||||||
|  |  | ||||||
| @@ -31,10 +34,10 @@ import java.io.IOException; | |||||||
|  |  | ||||||
| public class CryptoFactory { | public class CryptoFactory { | ||||||
|  |  | ||||||
|     public static KeyManager getKeyManager(KeyConfig keyCfg) { |     public static Ed25519KeyManager getKeyManager(KeyConfig keyCfg) { | ||||||
|         _KeyStore store; |         KeyStore store; | ||||||
|         if (StringUtils.equals(":memory:", keyCfg.getPath())) { |         if (StringUtils.equals(":memory:", keyCfg.getPath())) { | ||||||
|             store = new KeyMemoryStore(); |             store = new MemoryKeyStore(); | ||||||
|         } else { |         } else { | ||||||
|             File keyStore = new File(keyCfg.getPath()); |             File keyStore = new File(keyCfg.getPath()); | ||||||
|             if (!keyStore.exists()) { |             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) { |     public static SignatureManager getSignatureManager(Ed25519KeyManager keyMgr) { | ||||||
|         return new SignatureManager(keyMgr, cfg.getName()); |         return new Ed25519SignatureManager(keyMgr); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										51
									
								
								src/main/java/io/kamax/mxisd/crypto/GenericKey.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/main/java/io/kamax/mxisd/crypto/GenericKey.java
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										44
									
								
								src/main/java/io/kamax/mxisd/crypto/Key.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/main/java/io/kamax/mxisd/crypto/Key.java
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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(); | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										27
									
								
								src/main/java/io/kamax/mxisd/crypto/KeyAlgorithm.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/main/java/io/kamax/mxisd/crypto/KeyAlgorithm.java
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | package io.kamax.mxisd.crypto; | ||||||
|  |  | ||||||
|  | public interface KeyAlgorithm { | ||||||
|  |  | ||||||
|  |     String Ed25519 = "ed25519"; | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										54
									
								
								src/main/java/io/kamax/mxisd/crypto/KeyIdentifier.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/main/java/io/kamax/mxisd/crypto/KeyIdentifier.java
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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 <code>ed25519</code>. | ||||||
|  |      * | ||||||
|  |      * @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(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										41
									
								
								src/main/java/io/kamax/mxisd/crypto/KeyManager.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/main/java/io/kamax/mxisd/crypto/KeyManager.java
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | package io.kamax.mxisd.crypto; | ||||||
|  |  | ||||||
|  | import java.util.List; | ||||||
|  |  | ||||||
|  | public interface KeyManager { | ||||||
|  |  | ||||||
|  |     KeyIdentifier generateKey(KeyType type); | ||||||
|  |  | ||||||
|  |     List<KeyIdentifier> getKeys(KeyType type); | ||||||
|  |  | ||||||
|  |     Key getServerSigningKey(); | ||||||
|  |  | ||||||
|  |     Key getKey(KeyIdentifier id); | ||||||
|  |  | ||||||
|  |     void disableKey(KeyIdentifier id); | ||||||
|  |  | ||||||
|  |     String getPublicKeyBase64(KeyIdentifier id); | ||||||
|  |  | ||||||
|  |     boolean isValid(KeyType type, String publicKeyBase64); | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										39
									
								
								src/main/java/io/kamax/mxisd/crypto/KeyType.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/main/java/io/kamax/mxisd/crypto/KeyType.java
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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 | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | package io.kamax.mxisd.crypto; | ||||||
|  |  | ||||||
|  | public class RegularKeyIdentifier extends GenericKeyIdentifier { | ||||||
|  |  | ||||||
|  |     public RegularKeyIdentifier(String algo, String serial) { | ||||||
|  |         super(KeyType.Regular, algo, serial); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										29
									
								
								src/main/java/io/kamax/mxisd/crypto/Signature.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/main/java/io/kamax/mxisd/crypto/Signature.java
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | package io.kamax.mxisd.crypto; | ||||||
|  |  | ||||||
|  | public interface Signature { | ||||||
|  |  | ||||||
|  |     KeyIdentifier getKey(); | ||||||
|  |  | ||||||
|  |     String getSignature(); | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										97
									
								
								src/main/java/io/kamax/mxisd/crypto/SignatureManager.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								src/main/java/io/kamax/mxisd/crypto/SignatureManager.java
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | |||||||
|  | /* | ||||||
|  |  * 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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | package io.kamax.mxisd.crypto; | ||||||
|  |  | ||||||
|  | import com.google.gson.JsonElement; | ||||||
|  | import com.google.gson.JsonObject; | ||||||
|  | import io.kamax.matrix.event.EventKey; | ||||||
|  | import io.kamax.matrix.json.MatrixJson; | ||||||
|  |  | ||||||
|  | import java.nio.charset.StandardCharsets; | ||||||
|  | import java.util.Objects; | ||||||
|  |  | ||||||
|  | public interface SignatureManager { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Sign the message and add the signature to the <code>signatures</code> key. | ||||||
|  |      * <p> | ||||||
|  |      * If the key does not exist yet, it is created. If the key exist, the produced signature will be merged with any | ||||||
|  |      * existing ones. | ||||||
|  |      * | ||||||
|  |      * @param domain  The domain under which the signature should be added | ||||||
|  |      * @param message The message to sign and add the produced signature to | ||||||
|  |      * @return The provided message with the new signature | ||||||
|  |      * @throws IllegalArgumentException If the <code>signatures</code> value is not a JSON object | ||||||
|  |      */ | ||||||
|  |     default JsonObject signMessageGson(String domain, JsonObject message) throws IllegalArgumentException { | ||||||
|  |         JsonElement signEl = message.remove(EventKey.Signatures.get()); | ||||||
|  |         JsonObject oldSigns = new JsonObject(); | ||||||
|  |         if (!Objects.isNull(signEl)) { | ||||||
|  |             if (!signEl.isJsonObject()) { | ||||||
|  |                 throw new IllegalArgumentException("Message contains a signatures key that is not a JSON object value"); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             oldSigns = signEl.getAsJsonObject(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         JsonObject newSigns = signMessageGson(domain, MatrixJson.encodeCanonical(message)); | ||||||
|  |         oldSigns.entrySet().forEach(entry -> newSigns.add(entry.getKey(), entry.getValue())); | ||||||
|  |         message.add(EventKey.Signatures.get(), newSigns); | ||||||
|  |  | ||||||
|  |         return message; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Sign the message and produce a <code>signatures</code> 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 <code>signatures</code> 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); | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										58
									
								
								src/main/java/io/kamax/mxisd/crypto/ed25519/Ed25519Key.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/main/java/io/kamax/mxisd/crypto/ed25519/Ed25519Key.java
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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<KeyIdentifier> 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<KeyIdentifier> 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)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -22,8 +22,12 @@ package io.kamax.mxisd.exception; | |||||||
|  |  | ||||||
| public class ObjectNotFoundException extends RuntimeException { | public class ObjectNotFoundException extends RuntimeException { | ||||||
|  |  | ||||||
|  |     public ObjectNotFoundException(String message) { | ||||||
|  |         super(message); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     public ObjectNotFoundException(String type, String id) { |     public ObjectNotFoundException(String type, String id) { | ||||||
|         super(type + " with ID " + id + " does not exist"); |         this(type + " with ID " + id + " does not exist"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -22,6 +22,9 @@ package io.kamax.mxisd.http.io.identity; | |||||||
|  |  | ||||||
| import io.kamax.mxisd.lookup.SingleLookupReply; | import io.kamax.mxisd.lookup.SingleLookupReply; | ||||||
|  |  | ||||||
|  | import java.util.HashMap; | ||||||
|  | import java.util.Map; | ||||||
|  |  | ||||||
| public class SingeLookupReplyJson { | public class SingeLookupReplyJson { | ||||||
|  |  | ||||||
|     private String address; |     private String address; | ||||||
| @@ -30,6 +33,7 @@ public class SingeLookupReplyJson { | |||||||
|     private long not_after; |     private long not_after; | ||||||
|     private long not_before; |     private long not_before; | ||||||
|     private long ts; |     private long ts; | ||||||
|  |     private Map<String, Map<String, String>> signatures = new HashMap<>(); | ||||||
|  |  | ||||||
|     public SingeLookupReplyJson(SingleLookupReply reply) { |     public SingeLookupReplyJson(SingleLookupReply reply) { | ||||||
|         this.address = reply.getRequest().getThreePid(); |         this.address = reply.getRequest().getThreePid(); | ||||||
| @@ -64,4 +68,8 @@ public class SingeLookupReplyJson { | |||||||
|         return ts; |         return ts; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public Map<String, Map<String, String>> getSignatures() { | ||||||
|  |         return signatures; | ||||||
|  |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -23,30 +23,49 @@ package io.kamax.mxisd.http.undertow.handler; | |||||||
| import com.google.gson.JsonElement; | import com.google.gson.JsonElement; | ||||||
| import com.google.gson.JsonObject; | import com.google.gson.JsonObject; | ||||||
| import io.kamax.matrix.json.GsonUtil; | 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.HttpMatrixException; | ||||||
| import io.kamax.mxisd.exception.InternalServerError; | import io.kamax.mxisd.exception.InternalServerError; | ||||||
| import io.kamax.mxisd.proxy.Response; | import io.kamax.mxisd.proxy.Response; | ||||||
|  | import io.kamax.mxisd.util.RestClientUtils; | ||||||
| import io.undertow.server.HttpHandler; | import io.undertow.server.HttpHandler; | ||||||
| import io.undertow.server.HttpServerExchange; | import io.undertow.server.HttpServerExchange; | ||||||
|  | import io.undertow.server.handlers.form.FormData; | ||||||
| import io.undertow.util.HttpString; | import io.undertow.util.HttpString; | ||||||
| import org.apache.commons.io.IOUtils; | 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.Logger; | ||||||
| import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||||
|  |  | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.io.UnsupportedEncodingException; | import java.io.UnsupportedEncodingException; | ||||||
| import java.net.InetSocketAddress; | import java.net.InetSocketAddress; | ||||||
|  | import java.net.URI; | ||||||
| import java.net.URLDecoder; | import java.net.URLDecoder; | ||||||
| import java.nio.charset.StandardCharsets; | import java.nio.charset.StandardCharsets; | ||||||
| import java.util.Deque; | import java.util.*; | ||||||
| import java.util.LinkedList; |  | ||||||
| import java.util.Map; |  | ||||||
| import java.util.Optional; |  | ||||||
|  |  | ||||||
| public abstract class BasicHttpHandler implements HttpHandler { | 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) { |     protected String getRemoteHostAddress(HttpServerExchange exchange) { | ||||||
|         return ((InetSocketAddress) exchange.getConnection().getPeerAddress()).getAddress().getHostAddress(); |         return ((InetSocketAddress) exchange.getConnection().getPeerAddress()).getAddress().getHostAddress(); | ||||||
| @@ -101,6 +120,20 @@ public abstract class BasicHttpHandler implements HttpHandler { | |||||||
|         return GsonUtil.parseObj(getBodyUtf8(exchange)); |         return GsonUtil.parseObj(getBodyUtf8(exchange)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     protected String getOrThrow(FormData data, String key) { | ||||||
|  |         FormData.FormValue value = data.getFirst(key); | ||||||
|  |         if (Objects.isNull(value)) { | ||||||
|  |             throw new IllegalArgumentException("Form key " + key + " is missing"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         String object = value.getValue(); | ||||||
|  |         if (Objects.isNull(object)) { | ||||||
|  |             throw new IllegalArgumentException("Form key " + key + " does not have a value"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return object; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     protected void putHeader(HttpServerExchange ex, String name, String value) { |     protected void putHeader(HttpServerExchange ex, String name, String value) { | ||||||
|         ex.getResponseHeaders().put(HttpString.tryFromString(name), value); |         ex.getResponseHeaders().put(HttpString.tryFromString(name), value); | ||||||
|     } |     } | ||||||
| @@ -149,4 +182,34 @@ public abstract class BasicHttpHandler implements HttpHandler { | |||||||
|         upstream.getHeaders().forEach((key, value) -> exchange.getResponseHeaders().addAll(HttpString.tryFromString(key), value)); |         upstream.getHeaders().forEach((key, value) -> exchange.getResponseHeaders().addAll(HttpString.tryFromString(key), value)); | ||||||
|         writeBodyAsUtf8(exchange, upstream.getBody()); |         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); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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."); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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, "{}"); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -20,6 +20,8 @@ | |||||||
|  |  | ||||||
| package io.kamax.mxisd.http.undertow.handler.identity.v1; | 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.kamax.mxisd.http.IsAPIv1; | ||||||
| import io.undertow.server.HttpServerExchange; | import io.undertow.server.HttpServerExchange; | ||||||
| import org.slf4j.Logger; | import org.slf4j.Logger; | ||||||
| @@ -31,11 +33,19 @@ public class EphemeralKeyIsValidHandler extends KeyIsValidHandler { | |||||||
|  |  | ||||||
|     private transient final Logger log = LoggerFactory.getLogger(EphemeralKeyIsValidHandler.class); |     private transient final Logger log = LoggerFactory.getLogger(EphemeralKeyIsValidHandler.class); | ||||||
|  |  | ||||||
|  |     private KeyManager mgr; | ||||||
|  |  | ||||||
|  |     public EphemeralKeyIsValidHandler(KeyManager mgr) { | ||||||
|  |         this.mgr = mgr; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void handleRequest(HttpServerExchange exchange) { |     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); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -21,18 +21,20 @@ | |||||||
| package io.kamax.mxisd.http.undertow.handler.identity.v1; | package io.kamax.mxisd.http.undertow.handler.identity.v1; | ||||||
|  |  | ||||||
| import com.google.gson.JsonObject; | import com.google.gson.JsonObject; | ||||||
| import io.kamax.matrix.crypto.KeyManager; | import io.kamax.mxisd.crypto.GenericKeyIdentifier; | ||||||
| import io.kamax.mxisd.exception.BadRequestException; | import io.kamax.mxisd.crypto.KeyManager; | ||||||
|  | import io.kamax.mxisd.crypto.KeyType; | ||||||
| import io.kamax.mxisd.http.IsAPIv1; | import io.kamax.mxisd.http.IsAPIv1; | ||||||
| import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; | import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; | ||||||
| import io.undertow.server.HttpServerExchange; | import io.undertow.server.HttpServerExchange; | ||||||
|  | import org.apache.commons.lang3.StringUtils; | ||||||
| import org.slf4j.Logger; | import org.slf4j.Logger; | ||||||
| import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||||
|  |  | ||||||
| public class KeyGetHandler extends BasicHttpHandler { | public class KeyGetHandler extends BasicHttpHandler { | ||||||
|  |  | ||||||
|     public static final String Key = "key"; |     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); |     private transient final Logger log = LoggerFactory.getLogger(KeyGetHandler.class); | ||||||
|  |  | ||||||
| @@ -45,17 +47,17 @@ public class KeyGetHandler extends BasicHttpHandler { | |||||||
|     @Override |     @Override | ||||||
|     public void handleRequest(HttpServerExchange exchange) { |     public void handleRequest(HttpServerExchange exchange) { | ||||||
|         String key = getQueryParameter(exchange, Key); |         String key = getQueryParameter(exchange, Key); | ||||||
|         String[] v = key.split(":", 2); |         if (StringUtils.isBlank(key)) { | ||||||
|         String keyType = v[0]; |             throw new IllegalArgumentException("Key ID cannot be empty or blank"); | ||||||
|         int keyId = Integer.parseInt(v[1]); |  | ||||||
|  |  | ||||||
|         if (!"ed25519".contentEquals(keyType)) { |  | ||||||
|             throw new BadRequestException("Invalid algorithm: " + keyType); |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         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(); |         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); |         respond(exchange, obj); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -20,10 +20,10 @@ | |||||||
|  |  | ||||||
| package io.kamax.mxisd.http.undertow.handler.identity.v1; | 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.kamax.mxisd.http.IsAPIv1; | ||||||
| import io.undertow.server.HttpServerExchange; | import io.undertow.server.HttpServerExchange; | ||||||
| import org.apache.commons.lang.StringUtils; |  | ||||||
| import org.slf4j.Logger; | import org.slf4j.Logger; | ||||||
| import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||||
|  |  | ||||||
| @@ -41,12 +41,11 @@ public class RegularKeyIsValidHandler extends KeyIsValidHandler { | |||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void handleRequest(HttpServerExchange exchange) { |     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); |         log.info("Validating public key {}", pubKey); | ||||||
|  |  | ||||||
|         // TODO do in manager |         respondJson(exchange, mgr.isValid(KeyType.Regular, pubKey) ? validKey : invalidKey); | ||||||
|         boolean valid = StringUtils.equals(pubKey, mgr.getPublicKeyBase64(mgr.getCurrentIndex())); |  | ||||||
|         respondJson(exchange, valid ? validKey : invalidKey); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -20,94 +20,36 @@ | |||||||
|  |  | ||||||
| package io.kamax.mxisd.http.undertow.handler.identity.v1; | package io.kamax.mxisd.http.undertow.handler.identity.v1; | ||||||
|  |  | ||||||
| import io.kamax.mxisd.config.ServerConfig; |  | ||||||
| import io.kamax.mxisd.config.ViewConfig; |  | ||||||
| import io.kamax.mxisd.http.IsAPIv1; | import io.kamax.mxisd.http.IsAPIv1; | ||||||
| import io.kamax.mxisd.http.io.identity.SuccessStatusJson; |  | ||||||
| import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; | import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; | ||||||
| import io.kamax.mxisd.session.SessionManager; | import io.kamax.mxisd.session.SessionManager; | ||||||
| import io.kamax.mxisd.session.ValidationResult; | import io.kamax.mxisd.session.ValidationResult; | ||||||
| import io.kamax.mxisd.util.FileUtil; | import org.apache.commons.lang3.StringUtils; | ||||||
| import io.undertow.server.HttpServerExchange; |  | ||||||
| import io.undertow.util.HttpString; |  | ||||||
| import org.apache.commons.lang.StringUtils; |  | ||||||
| import org.slf4j.Logger; | import org.slf4j.Logger; | ||||||
| import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||||
|  |  | ||||||
| import java.io.IOException; | public abstract class SessionValidateHandler extends BasicHttpHandler { | ||||||
| import java.net.MalformedURLException; |  | ||||||
| import java.net.URL; |  | ||||||
|  |  | ||||||
| public class SessionValidateHandler extends BasicHttpHandler { |  | ||||||
|  |  | ||||||
|     public static final String Path = IsAPIv1.Base + "/validate/{medium}/submitToken"; |     public static final String Path = IsAPIv1.Base + "/validate/{medium}/submitToken"; | ||||||
|  |  | ||||||
|     private transient final Logger log = LoggerFactory.getLogger(SessionValidateHandler.class); |     private transient final Logger log = LoggerFactory.getLogger(SessionValidateHandler.class); | ||||||
|  |  | ||||||
|     private SessionManager mgr; |     private SessionManager mgr; | ||||||
|     private ServerConfig srvCfg; |  | ||||||
|     private ViewConfig viewCfg; |  | ||||||
|  |  | ||||||
|     public SessionValidateHandler(SessionManager mgr, ServerConfig srvCfg, ViewConfig viewCfg) { |     public SessionValidateHandler(SessionManager mgr) { | ||||||
|         this.mgr = mgr; |         this.mgr = mgr; | ||||||
|         this.srvCfg = srvCfg; |  | ||||||
|         this.viewCfg = viewCfg; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     protected ValidationResult handleRequest(String sid, String secret, String token) { | ||||||
|     public void handleRequest(HttpServerExchange exchange) { |         if (StringUtils.isEmpty(sid)) { | ||||||
|         String medium = getQueryParameter(exchange, "medium"); |             throw new IllegalArgumentException("sid is not set or is empty"); | ||||||
|         String sid = getQueryParameter(exchange, "sid"); |  | ||||||
|         String secret = getQueryParameter(exchange, "client_secret"); |  | ||||||
|         String token = getQueryParameter(exchange, "token"); |  | ||||||
|  |  | ||||||
|         boolean isHtmlRequest = false; |  | ||||||
|         for (String v : exchange.getRequestHeaders().get("Accept")) { |  | ||||||
|             if (StringUtils.startsWithIgnoreCase(v, "text/html")) { |  | ||||||
|                 isHtmlRequest = true; |  | ||||||
|                 break; |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (isHtmlRequest) { |         if (StringUtils.isEmpty(secret)) { | ||||||
|             handleHtmlRequest(exchange, medium, sid, secret, token); |             throw new IllegalArgumentException("client secret is not set or is empty"); | ||||||
|         } else { |  | ||||||
|             handleJsonRequest(exchange, sid, secret, token); |  | ||||||
|         } |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|     private void handleHtmlRequest(HttpServerExchange exchange, String medium, String sid, String secret, String token) { |         return mgr.validate(sid, secret, token); | ||||||
|         log.info("Validating session {} for medium {}", sid, medium); |  | ||||||
|         ValidationResult r = mgr.validate(sid, secret, token); |  | ||||||
|         log.info("Session {} was validated", sid); |  | ||||||
|         if (r.getNextUrl().isPresent()) { |  | ||||||
|             String url = r.getNextUrl().get(); |  | ||||||
|             try { |  | ||||||
|                 url = new URL(url).toString(); |  | ||||||
|             } catch (MalformedURLException e) { |  | ||||||
|                 log.info("Session next URL {} is not a valid one, will prepend public URL {}", url, srvCfg.getPublicUrl()); |  | ||||||
|                 url = srvCfg.getPublicUrl() + r.getNextUrl().get(); |  | ||||||
|             } |  | ||||||
|             log.info("Session {} validation: next URL is present, redirecting to {}", sid, url); |  | ||||||
|             exchange.setStatusCode(302); |  | ||||||
|             exchange.getResponseHeaders().add(HttpString.tryFromString("Location"), url); |  | ||||||
|         } else { |  | ||||||
|             try { |  | ||||||
|                 String data = FileUtil.load(viewCfg.getSession().getOnTokenSubmit().getSuccess()); |  | ||||||
|                 writeBodyAsUtf8(exchange, data); |  | ||||||
|             } catch (IOException e) { |  | ||||||
|                 throw new RuntimeException(e); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private void handleJsonRequest(HttpServerExchange exchange, String sid, String secret, String token) { |  | ||||||
|         log.info("Requested: {}", exchange.getRequestURL()); |  | ||||||
|  |  | ||||||
|         mgr.validate(sid, secret, token); |  | ||||||
|         log.info("Session {} was validated", sid); |  | ||||||
|  |  | ||||||
|         respondJson(exchange, new SuccessStatusJson(true)); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,82 @@ | |||||||
|  | /* | ||||||
|  |  * 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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | package io.kamax.mxisd.http.undertow.handler.identity.v1; | ||||||
|  |  | ||||||
|  | import io.kamax.mxisd.config.MxisdConfig; | ||||||
|  | import io.kamax.mxisd.config.ServerConfig; | ||||||
|  | import io.kamax.mxisd.config.ViewConfig; | ||||||
|  | import io.kamax.mxisd.session.SessionManager; | ||||||
|  | import io.kamax.mxisd.session.ValidationResult; | ||||||
|  | import io.kamax.mxisd.util.FileUtil; | ||||||
|  | import io.undertow.server.HttpServerExchange; | ||||||
|  | import io.undertow.util.HttpString; | ||||||
|  | import org.slf4j.Logger; | ||||||
|  | import org.slf4j.LoggerFactory; | ||||||
|  |  | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.net.MalformedURLException; | ||||||
|  | import java.net.URL; | ||||||
|  |  | ||||||
|  | public class SessionValidationGetHandler extends SessionValidateHandler { | ||||||
|  |  | ||||||
|  |     private transient final Logger log = LoggerFactory.getLogger(SessionValidationGetHandler.class); | ||||||
|  |  | ||||||
|  |     private ServerConfig srvCfg; | ||||||
|  |     private ViewConfig viewCfg; | ||||||
|  |  | ||||||
|  |     public SessionValidationGetHandler(SessionManager mgr, MxisdConfig cfg) { | ||||||
|  |         super(mgr); | ||||||
|  |         this.srvCfg = cfg.getServer(); | ||||||
|  |         this.viewCfg = cfg.getView(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void handleRequest(HttpServerExchange exchange) { | ||||||
|  |         log.info("Handling GET request to validate session"); | ||||||
|  |  | ||||||
|  |         String sid = getQueryParameter(exchange, "sid"); | ||||||
|  |         String secret = getQueryParameter(exchange, "client_secret"); | ||||||
|  |         String token = getQueryParameter(exchange, "token"); | ||||||
|  |  | ||||||
|  |         ValidationResult r = handleRequest(sid, secret, token); | ||||||
|  |         log.info("Session {} was validated", sid); | ||||||
|  |         if (r.getNextUrl().isPresent()) { | ||||||
|  |             String url = r.getNextUrl().get(); | ||||||
|  |             try { | ||||||
|  |                 url = new URL(url).toString(); | ||||||
|  |             } catch (MalformedURLException e) { | ||||||
|  |                 log.info("Session next URL {} is not a valid one, will prepend public URL {}", url, srvCfg.getPublicUrl()); | ||||||
|  |                 url = srvCfg.getPublicUrl() + r.getNextUrl().get(); | ||||||
|  |             } | ||||||
|  |             log.info("Session {} validation: next URL is present, redirecting to {}", sid, url); | ||||||
|  |             exchange.setStatusCode(302); | ||||||
|  |             exchange.getResponseHeaders().add(HttpString.tryFromString("Location"), url); | ||||||
|  |         } else { | ||||||
|  |             try { | ||||||
|  |                 String data = FileUtil.load(viewCfg.getSession().getOnTokenSubmit().getSuccess()); | ||||||
|  |                 writeBodyAsUtf8(exchange, data); | ||||||
|  |             } catch (IOException e) { | ||||||
|  |                 throw new RuntimeException(e); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,80 @@ | |||||||
|  | /* | ||||||
|  |  * 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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | package io.kamax.mxisd.http.undertow.handler.identity.v1; | ||||||
|  |  | ||||||
|  | import com.google.gson.JsonObject; | ||||||
|  | import io.kamax.matrix.json.GsonUtil; | ||||||
|  | import io.kamax.mxisd.http.io.identity.SuccessStatusJson; | ||||||
|  | import io.kamax.mxisd.session.SessionManager; | ||||||
|  | import io.undertow.server.HttpServerExchange; | ||||||
|  | import io.undertow.server.handlers.form.FormData; | ||||||
|  | import io.undertow.server.handlers.form.FormParserFactory; | ||||||
|  | import org.apache.commons.lang3.StringUtils; | ||||||
|  | import org.slf4j.Logger; | ||||||
|  | import org.slf4j.LoggerFactory; | ||||||
|  |  | ||||||
|  | import java.io.IOException; | ||||||
|  |  | ||||||
|  | public class SessionValidationPostHandler extends SessionValidateHandler { | ||||||
|  |  | ||||||
|  |     private transient final Logger log = LoggerFactory.getLogger(SessionValidationPostHandler.class); | ||||||
|  |  | ||||||
|  |     private FormParserFactory factory; | ||||||
|  |  | ||||||
|  |     public SessionValidationPostHandler(SessionManager mgr) { | ||||||
|  |         super(mgr); | ||||||
|  |         factory = FormParserFactory.builder().build(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void handleRequest(HttpServerExchange exchange) throws IOException { | ||||||
|  |         log.info("Handling POST request to validate session"); | ||||||
|  |  | ||||||
|  |         String sid; | ||||||
|  |         String secret; | ||||||
|  |         String token; | ||||||
|  |  | ||||||
|  |         String contentType = getContentType(exchange).orElseThrow(() -> new IllegalArgumentException("Content type header is not set")); | ||||||
|  |         if (StringUtils.equals(contentType, "application/json")) { // FIXME use MIME parsing tools | ||||||
|  |             log.info("Parsing as JSON data"); | ||||||
|  |  | ||||||
|  |             JsonObject body = parseJsonObject(exchange); | ||||||
|  |             sid = GsonUtil.getStringOrThrow(body, "sid"); | ||||||
|  |             secret = GsonUtil.getStringOrThrow(body, "client_secret"); | ||||||
|  |             token = GsonUtil.getStringOrThrow(body, "token"); | ||||||
|  |         } else if (StringUtils.equals(contentType, "application/x-www-form-urlencoded")) { // FIXME use MIME parsing tools | ||||||
|  |             log.info("Parsing as Form data"); | ||||||
|  |  | ||||||
|  |             FormData data = factory.createParser(exchange).parseBlocking(); | ||||||
|  |             sid = getOrThrow(data, "sid"); | ||||||
|  |             secret = getOrThrow(data, "client_secret"); | ||||||
|  |             token = getOrThrow(data, "token"); | ||||||
|  |         } else { | ||||||
|  |             log.info("Unsupported Content type: {}", contentType); | ||||||
|  |             throw new IllegalArgumentException("Unsupported Content type: " + contentType); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         handleRequest(sid, secret, token); | ||||||
|  |         respondJson(exchange, new SuccessStatusJson(true)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -21,10 +21,10 @@ | |||||||
| package io.kamax.mxisd.http.undertow.handler.identity.v1; | package io.kamax.mxisd.http.undertow.handler.identity.v1; | ||||||
|  |  | ||||||
| import com.google.gson.JsonObject; | 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.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.IsAPIv1; | ||||||
| import io.kamax.mxisd.http.io.identity.SingeLookupReplyJson; | import io.kamax.mxisd.http.io.identity.SingeLookupReplyJson; | ||||||
| import io.kamax.mxisd.lookup.SingleLookupReply; | import io.kamax.mxisd.lookup.SingleLookupReply; | ||||||
| @@ -42,10 +42,12 @@ public class SingleLookupHandler extends LookupHandler { | |||||||
|  |  | ||||||
|     private transient final Logger log = LoggerFactory.getLogger(SingleLookupHandler.class); |     private transient final Logger log = LoggerFactory.getLogger(SingleLookupHandler.class); | ||||||
|  |  | ||||||
|  |     private ServerConfig cfg; | ||||||
|     private LookupStrategy strategy; |     private LookupStrategy strategy; | ||||||
|     private SignatureManager signMgr; |     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.strategy = strategy; | ||||||
|         this.signMgr = signMgr; |         this.signMgr = signMgr; | ||||||
|     } |     } | ||||||
| @@ -69,11 +71,8 @@ public class SingleLookupHandler extends LookupHandler { | |||||||
|             respondJson(exchange, "{}"); |             respondJson(exchange, "{}"); | ||||||
|         } else { |         } else { | ||||||
|             SingleLookupReply lookup = lookupOpt.get(); |             SingleLookupReply lookup = lookupOpt.get(); | ||||||
|  |  | ||||||
|             // FIXME signing should be done in the business model, not in the controller |  | ||||||
|             JsonObject obj = GsonUtil.makeObj(new SingeLookupReplyJson(lookup)); |             JsonObject obj = GsonUtil.makeObj(new SingeLookupReplyJson(lookup)); | ||||||
|             obj.add(EventKey.Signatures.get(), signMgr.signMessageGson(MatrixJson.encodeCanonical(obj))); |             signMgr.signMessageGson(cfg.getName(), obj); | ||||||
|  |  | ||||||
|             respondJson(exchange, obj); |             respondJson(exchange, obj); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -24,9 +24,9 @@ import com.google.gson.JsonObject; | |||||||
| import com.google.gson.reflect.TypeToken; | import com.google.gson.reflect.TypeToken; | ||||||
| import io.kamax.matrix.MatrixID; | import io.kamax.matrix.MatrixID; | ||||||
| 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.matrix.json.GsonUtil; | ||||||
| import io.kamax.mxisd.config.ServerConfig; | import io.kamax.mxisd.config.ServerConfig; | ||||||
|  | import io.kamax.mxisd.crypto.KeyManager; | ||||||
| import io.kamax.mxisd.exception.BadRequestException; | import io.kamax.mxisd.exception.BadRequestException; | ||||||
| import io.kamax.mxisd.http.IsAPIv1; | import io.kamax.mxisd.http.IsAPIv1; | ||||||
| import io.kamax.mxisd.http.io.identity.StoreInviteRequest; | 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); |         IThreePidInvite invite = new ThreePidInvite(sender, inv.getMedium(), inv.getAddress(), inv.getRoomId(), parameters); | ||||||
|         IThreePidInviteReply reply = invMgr.storeInvite(invite); |         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())); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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<String> 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); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -26,7 +26,7 @@ import io.undertow.server.HttpServerExchange; | |||||||
|  |  | ||||||
| public class StatusHandler extends BasicHttpHandler { | public class StatusHandler extends BasicHttpHandler { | ||||||
|  |  | ||||||
|     public static final String Path = "/_matrix/identity/status"; |     public static final String Path = "/status"; | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public void handleRequest(HttpServerExchange exchange) { |     public void handleRequest(HttpServerExchange exchange) { | ||||||
|   | |||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -18,10 +18,9 @@ | |||||||
|  * along with this program.  If not, see <http://www.gnu.org/licenses/>. |  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| package io.kamax.mxisd.as; | package io.kamax.mxisd.invitation; | ||||||
| 
 | 
 | ||||||
| import io.kamax.matrix._MatrixID; | import io.kamax.matrix._MatrixID; | ||||||
| import io.kamax.mxisd.invitation.IThreePidInvite; |  | ||||||
| 
 | 
 | ||||||
| public interface IMatrixIdInvite extends IThreePidInvite { | public interface IMatrixIdInvite extends IThreePidInvite { | ||||||
| 
 | 
 | ||||||
| @@ -20,6 +20,8 @@ | |||||||
|  |  | ||||||
| package io.kamax.mxisd.invitation; | package io.kamax.mxisd.invitation; | ||||||
|  |  | ||||||
|  | import java.util.List; | ||||||
|  |  | ||||||
| public interface IThreePidInviteReply { | public interface IThreePidInviteReply { | ||||||
|  |  | ||||||
|     String getId(); |     String getId(); | ||||||
| @@ -30,4 +32,6 @@ public interface IThreePidInviteReply { | |||||||
|  |  | ||||||
|     String getDisplayName(); |     String getDisplayName(); | ||||||
|  |  | ||||||
|  |     List<String> getPublicKeys(); | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -23,21 +23,29 @@ package io.kamax.mxisd.invitation; | |||||||
| import com.google.gson.JsonArray; | import com.google.gson.JsonArray; | ||||||
| import com.google.gson.JsonObject; | import com.google.gson.JsonObject; | ||||||
| import io.kamax.matrix.MatrixID; | 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.matrix.json.GsonUtil; | ||||||
| import io.kamax.mxisd.config.InvitationConfig; | import io.kamax.mxisd.config.InvitationConfig; | ||||||
| import io.kamax.mxisd.dns.FederationDnsOverwrite; | import io.kamax.mxisd.config.MxisdConfig; | ||||||
|  | import io.kamax.mxisd.config.ServerConfig; | ||||||
|  | import io.kamax.mxisd.crypto.*; | ||||||
| import io.kamax.mxisd.exception.BadRequestException; | import io.kamax.mxisd.exception.BadRequestException; | ||||||
|  | import io.kamax.mxisd.exception.ConfigurationException; | ||||||
| import io.kamax.mxisd.exception.MappingAlreadyExistsException; | import io.kamax.mxisd.exception.MappingAlreadyExistsException; | ||||||
|  | import io.kamax.mxisd.exception.ObjectNotFoundException; | ||||||
| import io.kamax.mxisd.lookup.SingleLookupReply; | import io.kamax.mxisd.lookup.SingleLookupReply; | ||||||
| import io.kamax.mxisd.lookup.ThreePidMapping; | import io.kamax.mxisd.lookup.ThreePidMapping; | ||||||
| import io.kamax.mxisd.lookup.strategy.LookupStrategy; | import io.kamax.mxisd.lookup.strategy.LookupStrategy; | ||||||
|  | import io.kamax.mxisd.matrix.HomeserverFederationResolver; | ||||||
| import io.kamax.mxisd.notification.NotificationManager; | import io.kamax.mxisd.notification.NotificationManager; | ||||||
|  | import io.kamax.mxisd.profile.ProfileManager; | ||||||
| import io.kamax.mxisd.storage.IStorage; | import io.kamax.mxisd.storage.IStorage; | ||||||
| import io.kamax.mxisd.storage.ormlite.dao.ThreePidInviteIO; | 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.io.IOUtils; | ||||||
| import org.apache.commons.lang.RandomStringUtils; | import org.apache.commons.lang3.RandomStringUtils; | ||||||
| import org.apache.commons.lang.StringUtils; | import org.apache.commons.lang3.StringUtils; | ||||||
| import org.apache.http.client.methods.CloseableHttpResponse; | import org.apache.http.client.methods.CloseableHttpResponse; | ||||||
| import org.apache.http.client.methods.HttpPost; | import org.apache.http.client.methods.HttpPost; | ||||||
| import org.apache.http.conn.ssl.NoopHostnameVerifier; | import org.apache.http.conn.ssl.NoopHostnameVerifier; | ||||||
| @@ -49,14 +57,13 @@ import org.apache.http.impl.client.HttpClients; | |||||||
| import org.apache.http.ssl.SSLContextBuilder; | import org.apache.http.ssl.SSLContextBuilder; | ||||||
| import org.slf4j.Logger; | import org.slf4j.Logger; | ||||||
| import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||||
| import org.xbill.DNS.*; |  | ||||||
|  |  | ||||||
| import javax.net.ssl.HostnameVerifier; | import javax.net.ssl.HostnameVerifier; | ||||||
| import javax.net.ssl.SSLContext; | import javax.net.ssl.SSLContext; | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.net.MalformedURLException; |  | ||||||
| import java.net.URL; |  | ||||||
| import java.nio.charset.StandardCharsets; | import java.nio.charset.StandardCharsets; | ||||||
|  | import java.time.DateTimeException; | ||||||
|  | import java.time.Instant; | ||||||
| import java.util.*; | import java.util.*; | ||||||
| import java.util.concurrent.ConcurrentHashMap; | import java.util.concurrent.ConcurrentHashMap; | ||||||
| import java.util.concurrent.ForkJoinPool; | import java.util.concurrent.ForkJoinPool; | ||||||
| @@ -64,14 +71,20 @@ import java.util.concurrent.TimeUnit; | |||||||
|  |  | ||||||
| public class InvitationManager { | 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 InvitationConfig cfg; | ||||||
|  |     private ServerConfig srvCfg; | ||||||
|     private IStorage storage; |     private IStorage storage; | ||||||
|     private LookupStrategy lookupMgr; |     private LookupStrategy lookupMgr; | ||||||
|  |     private KeyManager keyMgr; | ||||||
|     private SignatureManager signMgr; |     private SignatureManager signMgr; | ||||||
|     private FederationDnsOverwrite dns; |     private HomeserverFederationResolver resolver; | ||||||
|     private NotificationManager notifMgr; |     private NotificationManager notifMgr; | ||||||
|  |     private ProfileManager profileMgr; | ||||||
|  |  | ||||||
|     private CloseableHttpClient client; |     private CloseableHttpClient client; | ||||||
|     private Timer refreshTimer; |     private Timer refreshTimer; | ||||||
| @@ -79,23 +92,29 @@ public class InvitationManager { | |||||||
|     private Map<String, IThreePidInviteReply> invitations = new ConcurrentHashMap<>(); |     private Map<String, IThreePidInviteReply> invitations = new ConcurrentHashMap<>(); | ||||||
|  |  | ||||||
|     public InvitationManager( |     public InvitationManager( | ||||||
|             InvitationConfig cfg, |             MxisdConfig mxisdCfg, | ||||||
|             IStorage storage, |             IStorage storage, | ||||||
|             LookupStrategy lookupMgr, |             LookupStrategy lookupMgr, | ||||||
|  |             KeyManager keyMgr, | ||||||
|             SignatureManager signMgr, |             SignatureManager signMgr, | ||||||
|             FederationDnsOverwrite dns, |             HomeserverFederationResolver resolver, | ||||||
|             NotificationManager notifMgr |             NotificationManager notifMgr, | ||||||
|  |             ProfileManager profileMgr | ||||||
|     ) { |     ) { | ||||||
|         this.cfg = cfg; |         this.cfg = requireValid(mxisdCfg); | ||||||
|  |         this.srvCfg = mxisdCfg.getServer(); | ||||||
|         this.storage = storage; |         this.storage = storage; | ||||||
|         this.lookupMgr = lookupMgr; |         this.lookupMgr = lookupMgr; | ||||||
|  |         this.keyMgr = keyMgr; | ||||||
|         this.signMgr = signMgr; |         this.signMgr = signMgr; | ||||||
|         this.dns = dns; |         this.resolver = resolver; | ||||||
|         this.notifMgr = notifMgr; |         this.notifMgr = notifMgr; | ||||||
|  |         this.profileMgr = profileMgr; | ||||||
|  |  | ||||||
|         log.info("Loading saved invites"); |         log.info("Loading saved invites"); | ||||||
|         Collection<ThreePidInviteIO> ioList = storage.getInvites(); |         Collection<ThreePidInviteIO> ioList = storage.getInvites(); | ||||||
|         ioList.forEach(io -> { |         ioList.forEach(io -> { | ||||||
|  |             io.getProperties().putIfAbsent(CreatedAtPropertyKey, defaultCreateTs); | ||||||
|             log.info("Processing invite {}", GsonUtil.get().toJson(io)); |             log.info("Processing invite {}", GsonUtil.get().toJson(io)); | ||||||
|             ThreePidInvite invite = new ThreePidInvite( |             ThreePidInvite invite = new ThreePidInvite( | ||||||
|                     MatrixID.asAcceptable(io.getSender()), |                     MatrixID.asAcceptable(io.getSender()), | ||||||
| @@ -105,7 +124,7 @@ public class InvitationManager { | |||||||
|                     io.getProperties() |                     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); |             invitations.put(reply.getId(), reply); | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
| @@ -122,81 +141,69 @@ public class InvitationManager { | |||||||
|  |  | ||||||
|         log.info("Setting up invitation mapping refresh timer"); |         log.info("Setting up invitation mapping refresh timer"); | ||||||
|         refreshTimer = new 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(() -> { |         Runtime.getRuntime().addShutdownHook(new Thread(() -> { | ||||||
|             refreshTimer.cancel(); |             refreshTimer.cancel(); | ||||||
|             ForkJoinPool.commonPool().awaitQuiescence(1, TimeUnit.MINUTES); |             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) { |     private InvitationConfig requireValid(MxisdConfig cfg) { | ||||||
|         return invite.getSender().getDomain().toLowerCase() + invite.getMedium().toLowerCase() + invite.getAddress().toLowerCase(); |         // 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) { |     private String getIdForLog(IThreePidInviteReply reply) { | ||||||
|         return reply.getInvite().getSender().getId() + ":" + reply.getInvite().getRoomId() + ":" + reply.getInvite().getMedium() + ":" + reply.getInvite().getAddress(); |         return reply.getInvite().getSender().getId() + ":" + reply.getInvite().getRoomId() + ":" + reply.getInvite().getMedium() + ":" + reply.getInvite().getAddress(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private String getSrvRecordName(String domain) { |  | ||||||
|         return "_matrix._tcp." + domain; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // TODO use caching mechanism |  | ||||||
|     // TODO export in matrix-java-sdk |  | ||||||
|     private String findHomeserverForDomain(String domain) { |  | ||||||
|         Optional<String> entryOpt = dns.findHost(domain); |  | ||||||
|         if (entryOpt.isPresent()) { |  | ||||||
|             String entry = entryOpt.get(); |  | ||||||
|             log.info("Found DNS overwrite for {} to {}", domain, entry); |  | ||||||
|             try { |  | ||||||
|                 return new URL(entry).toString(); |  | ||||||
|             } catch (MalformedURLException e) { |  | ||||||
|                 log.warn("Skipping homeserver Federation DNS overwrite for {} - not a valid URL: {}", domain, entry); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         log.debug("Performing SRV lookup for {}", domain); |  | ||||||
|         String lookupDns = getSrvRecordName(domain); |  | ||||||
|         log.info("Lookup name: {}", lookupDns); |  | ||||||
|  |  | ||||||
|         try { |  | ||||||
|             List<SRVRecord> srvRecords = new ArrayList<>(); |  | ||||||
|             Record[] rawRecords = new Lookup(lookupDns, Type.SRV).run(); |  | ||||||
|             if (rawRecords != null && rawRecords.length > 0) { |  | ||||||
|                 for (Record record : rawRecords) { |  | ||||||
|                     if (Type.SRV == record.getType()) { |  | ||||||
|                         srvRecords.add((SRVRecord) record); |  | ||||||
|                     } else { |  | ||||||
|                         log.info("Got non-SRV record: {}", record.toString()); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 srvRecords.sort(Comparator.comparingInt(SRVRecord::getPriority)); |  | ||||||
|                 for (SRVRecord record : srvRecords) { |  | ||||||
|                     log.info("Found SRV record: {}", record.toString()); |  | ||||||
|                     return "https://" + record.getTarget().toString(true) + ":" + record.getPort(); |  | ||||||
|                 } |  | ||||||
|             } else { |  | ||||||
|                 log.info("No SRV record for {}", lookupDns); |  | ||||||
|             } |  | ||||||
|         } catch (TextParseException e) { |  | ||||||
|             log.warn("Unable to perform DNS SRV query for {}: {}", lookupDns, e.getMessage()); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         log.info("Performing basic lookup using domain name {}", domain); |  | ||||||
|         return "https://" + domain + ":8448"; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     private Optional<SingleLookupReply> lookup3pid(String medium, String address) { |     private Optional<SingleLookupReply> lookup3pid(String medium, String address) { | ||||||
|         if (!cfg.getResolution().isRecursive()) { |         if (!cfg.getResolution().isRecursive()) { | ||||||
|             log.warn("/!\\ /!\\ --- RECURSIVE INVITE RESOLUTION HAS BEEN DISABLED --- /!\\ /!\\"); |             log.warn("/!\\ /!\\ --- RECURSIVE INVITE RESOLUTION HAS BEEN DISABLED --- /!\\ /!\\"); | ||||||
| @@ -205,19 +212,56 @@ public class InvitationManager { | |||||||
|         return lookupMgr.find(medium, address, cfg.getResolution().isRecursive()); |         return lookupMgr.find(medium, address, cfg.getResolution().isRecursive()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public List<IThreePidInviteReply> 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<String> allowedRoles = cfg.getPolicy().getIfSender().getHasRole(); | ||||||
|  |         if (Objects.isNull(allowedRoles)) { | ||||||
|  |             log.info("No allowed role configured for sender, allowing"); | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         List<String> 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 |     public synchronized IThreePidInviteReply storeInvite(IThreePidInvite invitation) { // TODO better sync | ||||||
|         if (!notifMgr.isMediumSupported(invitation.getMedium())) { |         if (!notifMgr.isMediumSupported(invitation.getMedium())) { | ||||||
|             throw new BadRequestException("Medium type " + invitation.getMedium() + " is not supported"); |             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()); |         log.info("Handling invite for {}:{} from {} in room {}", invitation.getMedium(), invitation.getAddress(), invitation.getSender(), invitation.getRoomId()); | ||||||
|         IThreePidInviteReply reply = invitations.get(invId); |         IThreePidInviteReply reply = invitations.get(invId); | ||||||
|         if (reply != null) { |         if (reply != null) { | ||||||
|             log.info("Invite is already pending for {}:{}, returning data", invitation.getMedium(), invitation.getAddress()); |             log.info("Invite is already pending for {}:{}, returning data", invitation.getMedium(), invitation.getAddress()); | ||||||
|             if (!StringUtils.equals(invitation.getRoomId(), reply.getInvite().getRoomId())) { |             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()); |                 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 { |             } else { | ||||||
|                 // FIXME we should check attempt and send if bigger |                 // FIXME we should check attempt and send if bigger | ||||||
|             } |             } | ||||||
| @@ -232,8 +276,21 @@ public class InvitationManager { | |||||||
|  |  | ||||||
|         String token = RandomStringUtils.randomAlphanumeric(64); |         String token = RandomStringUtils.randomAlphanumeric(64); | ||||||
|         String displayName = invitation.getAddress().substring(0, 3) + "..."; |         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()); |         log.info("Performing invite to {}:{}", invitation.getMedium(), invitation.getAddress()); | ||||||
|         notifMgr.sendForReply(reply); |         notifMgr.sendForReply(reply); | ||||||
| @@ -246,6 +303,78 @@ public class InvitationManager { | |||||||
|         return reply; |         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 {}", reply.getId(), 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() { |     public void lookupMappingsForInvites() { | ||||||
|         if (!invitations.isEmpty()) { |         if (!invitations.isEmpty()) { | ||||||
|             log.info("Checking for existing mapping for pending invites"); |             log.info("Checking for existing mapping for pending invites"); | ||||||
| @@ -266,12 +395,34 @@ 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) { |     private void publishMapping(IThreePidInviteReply reply, String mxid) { | ||||||
|         String medium = reply.getInvite().getMedium(); |         String medium = reply.getInvite().getMedium(); | ||||||
|         String address = reply.getInvite().getAddress(); |         String address = reply.getInvite().getAddress(); | ||||||
|         String domain = reply.getInvite().getSender().getDomain(); |         String domain = reply.getInvite().getSender().getDomain(); | ||||||
|         log.info("Discovering HS for domain {}", domain); |         log.info("Discovering HS for domain {}", domain); | ||||||
|         String hsUrlOpt = findHomeserverForDomain(domain); |         String hsUrlOpt = resolver.resolve(domain).toString(); | ||||||
|  |  | ||||||
|         // TODO this is needed as this will block if called during authentication cycle due to synapse implementation |         // TODO this is needed as this will block if called during authentication cycle due to synapse implementation | ||||||
|         new Thread(() -> { // FIXME need to make this retry-able and within a general background working pool |         new Thread(() -> { // FIXME need to make this retry-able and within a general background working pool | ||||||
| @@ -280,7 +431,7 @@ public class InvitationManager { | |||||||
|             JsonObject obj = new JsonObject(); |             JsonObject obj = new JsonObject(); | ||||||
|             obj.addProperty("mxid", mxid); |             obj.addProperty("mxid", mxid); | ||||||
|             obj.addProperty("token", reply.getToken()); |             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(); |             JsonObject objUp = new JsonObject(); | ||||||
|             objUp.addProperty("mxid", mxid); |             objUp.addProperty("mxid", mxid); | ||||||
| @@ -298,30 +449,45 @@ public class InvitationManager { | |||||||
|             content.addProperty("address", address); |             content.addProperty("address", address); | ||||||
|             content.addProperty("mxid", mxid); |             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); |             StringEntity entity = new StringEntity(content.toString(), StandardCharsets.UTF_8); | ||||||
|             entity.setContentType("application/json"); |             entity.setContentType("application/json"); | ||||||
|             req.setEntity(entity); |             req.setEntity(entity); | ||||||
|  |  | ||||||
|  |             Instant resolvedAt = Instant.now(); | ||||||
|  |             boolean couldPublish = false; | ||||||
|  |             boolean shouldArchive = true; | ||||||
|             try { |             try { | ||||||
|                 log.info("Posting onBind event to {}", req.getURI()); |                 log.info("Posting onBind event to {}", req.getURI()); | ||||||
|                 CloseableHttpResponse response = client.execute(req); |                 CloseableHttpResponse response = client.execute(req); | ||||||
|                 int statusCode = response.getStatusLine().getStatusCode(); |                 int statusCode = response.getStatusLine().getStatusCode(); | ||||||
|                 log.info("Answer code: {}", statusCode); |                 log.info("Answer code: {}", statusCode); | ||||||
|                 if (statusCode >= 300 && statusCode != 403) { |                 if (statusCode >= 300 && statusCode != 403) { | ||||||
|                     log.warn("Answer body: {}", IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8)); |                     log.info("Answer body: {}", IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8)); | ||||||
|                 } else { |                     log.warn("HS returned an error."); | ||||||
|                     if (statusCode == 403) { |  | ||||||
|                         log.info("Invite was obsolete"); |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     invitations.remove(getId(reply.getInvite())); |                     shouldArchive = statusCode != 502; | ||||||
|                     storage.deleteInvite(reply.getId()); |                     if (shouldArchive) { | ||||||
|                     log.info("Removed invite from internal store"); |                         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(); |                 response.close(); | ||||||
|             } catch (IOException e) { |             } catch (IOException e) { | ||||||
|                 log.warn("Unable to tell HS {} about invite being mapped", domain, 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(); |         }).start(); | ||||||
|     } |     } | ||||||
| @@ -337,7 +503,7 @@ public class InvitationManager { | |||||||
|         @Override |         @Override | ||||||
|         public void run() { |         public void run() { | ||||||
|             try { |             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<SingleLookupReply> result = lookup3pid(reply.getInvite().getMedium(), reply.getInvite().getAddress()); |                 Optional<SingleLookupReply> result = lookup3pid(reply.getInvite().getMedium(), reply.getInvite().getAddress()); | ||||||
|                 if (result.isPresent()) { |                 if (result.isPresent()) { | ||||||
|                     SingleLookupReply lookup = result.get(); |                     SingleLookupReply lookup = result.get(); | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ | |||||||
|  * along with this program.  If not, see <http://www.gnu.org/licenses/>. |  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| package io.kamax.mxisd.as; | package io.kamax.mxisd.invitation; | ||||||
| 
 | 
 | ||||||
| import io.kamax.matrix._MatrixID; | import io.kamax.matrix._MatrixID; | ||||||
| 
 | 
 | ||||||
| @@ -20,18 +20,24 @@ | |||||||
|  |  | ||||||
| package io.kamax.mxisd.invitation; | package io.kamax.mxisd.invitation; | ||||||
|  |  | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.List; | ||||||
|  |  | ||||||
| public class ThreePidInviteReply implements IThreePidInviteReply { | public class ThreePidInviteReply implements IThreePidInviteReply { | ||||||
|  |  | ||||||
|     private String id; |     private String id; | ||||||
|     private IThreePidInvite invite; |     private IThreePidInvite invite; | ||||||
|     private String token; |     private String token; | ||||||
|     private String displayName; |     private String displayName; | ||||||
|  |     private List<String> publicKeys; | ||||||
|  |  | ||||||
|     public ThreePidInviteReply(String id, IThreePidInvite invite, String token, String displayName) { |     public ThreePidInviteReply(String id, IThreePidInvite invite, String token, String displayName, List<String> publicKeys) { | ||||||
|         this.id = id; |         this.id = id; | ||||||
|         this.invite = invite; |         this.invite = invite; | ||||||
|         this.token = token; |         this.token = token; | ||||||
|         this.displayName = displayName; |         this.displayName = displayName; | ||||||
|  |         this.publicKeys = Collections.unmodifiableList(new ArrayList<>(publicKeys)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
| @@ -54,4 +60,9 @@ public class ThreePidInviteReply implements IThreePidInviteReply { | |||||||
|         return displayName; |         return displayName; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public List<String> getPublicKeys() { | ||||||
|  |         return publicKeys; | ||||||
|  |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -27,6 +27,8 @@ import io.kamax.matrix._MatrixID; | |||||||
| import io.kamax.mxisd.http.io.identity.SingeLookupReplyJson; | import io.kamax.mxisd.http.io.identity.SingeLookupReplyJson; | ||||||
|  |  | ||||||
| import java.time.Instant; | import java.time.Instant; | ||||||
|  | import java.util.HashMap; | ||||||
|  | import java.util.Map; | ||||||
|  |  | ||||||
| public class SingleLookupReply { | public class SingleLookupReply { | ||||||
|  |  | ||||||
| @@ -39,6 +41,7 @@ public class SingleLookupReply { | |||||||
|     private Instant notBefore; |     private Instant notBefore; | ||||||
|     private Instant notAfter; |     private Instant notAfter; | ||||||
|     private Instant timestamp; |     private Instant timestamp; | ||||||
|  |     private Map<String, Map<String, String>> signatures = new HashMap<>(); | ||||||
|  |  | ||||||
|     public static SingleLookupReply fromRecursive(SingleLookupRequest request, String body) { |     public static SingleLookupReply fromRecursive(SingleLookupRequest request, String body) { | ||||||
|         SingleLookupReply reply = new SingleLookupReply(); |         SingleLookupReply reply = new SingleLookupReply(); | ||||||
| @@ -52,6 +55,7 @@ public class SingleLookupReply { | |||||||
|             reply.notAfter = Instant.ofEpochMilli(json.getNot_after()); |             reply.notAfter = Instant.ofEpochMilli(json.getNot_after()); | ||||||
|             reply.notBefore = Instant.ofEpochMilli(json.getNot_before()); |             reply.notBefore = Instant.ofEpochMilli(json.getNot_before()); | ||||||
|             reply.timestamp = Instant.ofEpochMilli(json.getTs()); |             reply.timestamp = Instant.ofEpochMilli(json.getTs()); | ||||||
|  |             reply.signatures = new HashMap<>(json.getSignatures()); | ||||||
|         } catch (JsonSyntaxException e) { |         } catch (JsonSyntaxException e) { | ||||||
|             // stub - we only want to try, nothing more |             // stub - we only want to try, nothing more | ||||||
|         } |         } | ||||||
| @@ -107,4 +111,12 @@ public class SingleLookupReply { | |||||||
|         return timestamp; |         return timestamp; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public Map<String, Map<String, String>> getSignatures() { | ||||||
|  |         return signatures; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public Map<String, String> getSignature(String host) { | ||||||
|  |         return signatures.computeIfAbsent(host, k -> new HashMap<>()); | ||||||
|  |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,216 @@ | |||||||
|  | /* | ||||||
|  |  * 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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | package io.kamax.mxisd.matrix; | ||||||
|  |  | ||||||
|  | import com.google.gson.JsonObject; | ||||||
|  | import io.kamax.matrix.json.GsonUtil; | ||||||
|  | import io.kamax.matrix.json.InvalidJsonException; | ||||||
|  | import io.kamax.mxisd.dns.FederationDnsOverwrite; | ||||||
|  | import org.apache.commons.lang3.StringUtils; | ||||||
|  | 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 org.xbill.DNS.*; | ||||||
|  |  | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.net.MalformedURLException; | ||||||
|  | import java.net.URL; | ||||||
|  | import java.util.*; | ||||||
|  |  | ||||||
|  | public class HomeserverFederationResolver { | ||||||
|  |  | ||||||
|  |     private static final Logger log = LoggerFactory.getLogger(HomeserverFederationResolver.class); | ||||||
|  |  | ||||||
|  |     private FederationDnsOverwrite dns; | ||||||
|  |     private CloseableHttpClient client; | ||||||
|  |  | ||||||
|  |     public HomeserverFederationResolver(FederationDnsOverwrite dns, CloseableHttpClient client) { | ||||||
|  |         this.dns = dns; | ||||||
|  |         this.client = client; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private String getDefaultScheme() { | ||||||
|  |         return "https"; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private int getDefaultPort() { | ||||||
|  |         return 8448; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private String getDnsSrvPrefix() { | ||||||
|  |         return "_matrix._tcp."; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private String buildSrvRecordName(String domain) { | ||||||
|  |         return getDnsSrvPrefix() + domain; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private Optional<URL> resolveOverwrite(String domain) { | ||||||
|  |         Optional<String> entryOpt = dns.findHost(domain); | ||||||
|  |         if (!entryOpt.isPresent()) { | ||||||
|  |             log.info("No DNS overwrite for {}", domain); | ||||||
|  |             return Optional.empty(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             return Optional.of(new URL(entryOpt.get())); | ||||||
|  |         } catch (MalformedURLException e) { | ||||||
|  |             log.warn("Skipping homeserver Federation DNS overwrite for {} - not a valid URL: {}", domain, entryOpt.get()); | ||||||
|  |             return Optional.empty(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private Optional<URL> resolveLiteral(String domain) { | ||||||
|  |         if (domain.contains("[") && domain.contains("]")) { | ||||||
|  |             // This is an IPv6 | ||||||
|  |             if (domain.contains("]:")) { | ||||||
|  |                 // With a custom port, we return as is | ||||||
|  |                 return Optional.of(build(domain)); | ||||||
|  |             } else { | ||||||
|  |                 return Optional.of(build(domain + ":" + getDefaultPort())); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (domain.contains(":")) { | ||||||
|  |             // This is a domain or IPv4 with an explicit port, we return as is | ||||||
|  |             return Optional.of(build(domain)); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // At this point, we do not account for the provided string to be an IPv4 without a port. We will therefore | ||||||
|  |         // perform well-known lookup and SRV record. While this is not needed, we don't expect the SRV to return anything | ||||||
|  |         // and the well-known shouldn't either, but it might, leading to a wrong destination potentially. | ||||||
|  |         // | ||||||
|  |         // We accept this risk as mxisd is not meant to be used without DNS domain as per FAQ. We also provide resolution | ||||||
|  |         // override facilities. Therefore, we accept to not handle this case until we get report of such unwanted behaviour | ||||||
|  |         // that still fix mxisd use case and can't be resolved via override. | ||||||
|  |  | ||||||
|  |         return Optional.empty(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private Optional<URL> resolveWellKnown(String domain) { | ||||||
|  |         log.debug("Performing Well-known lookup for {}", domain); | ||||||
|  |         HttpGet wnReq = new HttpGet("https://" + domain + "/.well-known/matrix/server"); | ||||||
|  |         try (CloseableHttpResponse wnRes = client.execute(wnReq)) { | ||||||
|  |             int status = wnRes.getStatusLine().getStatusCode(); | ||||||
|  |             if (status == 200) { | ||||||
|  |                 try { | ||||||
|  |                     JsonObject body = GsonUtil.parseObj(EntityUtils.toString(wnRes.getEntity())); | ||||||
|  |                     String server = GsonUtil.getStringOrNull(body, "m.server"); | ||||||
|  |                     if (StringUtils.isNotBlank(server)) { | ||||||
|  |                         log.debug("Found well-known entry: {}", server); | ||||||
|  |                         return Optional.of(build(server)); | ||||||
|  |                     } | ||||||
|  |                 } catch (InvalidJsonException e) { | ||||||
|  |                     log.info("Could not parse well-known resource: {}", e.getMessage()); | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 log.info("Well-known did not return status code 200 but {}, ignoring", status); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             return Optional.empty(); | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             throw new RuntimeException("Error while trying to lookup well-known for " + domain, e); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private Optional<URL> resolveDnsSrv(String domain) { | ||||||
|  |         log.debug("Performing SRV lookup for {}", domain); | ||||||
|  |         String lookupDns = buildSrvRecordName(domain); | ||||||
|  |         log.debug("Lookup name: {}", lookupDns); | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             List<SRVRecord> srvRecords = new ArrayList<>(); | ||||||
|  |             Record[] rawRecords = new Lookup(lookupDns, Type.SRV).run(); | ||||||
|  |             if (Objects.isNull(rawRecords) || rawRecords.length == 0) { | ||||||
|  |                 log.debug("No SRV record for {}", domain); | ||||||
|  |                 return Optional.empty(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             for (Record record : rawRecords) { | ||||||
|  |                 if (Type.SRV == record.getType()) { | ||||||
|  |                     srvRecords.add((SRVRecord) record); | ||||||
|  |                 } else { | ||||||
|  |                     log.debug("Ignoring non-SRV record: {}", record.toString()); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (srvRecords.size() < 1) { | ||||||
|  |                 log.warn("DNS SRV records were found for {} but none is usable", lookupDns); | ||||||
|  |                 return Optional.empty(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             srvRecords.sort(Comparator.comparingInt(SRVRecord::getPriority)); | ||||||
|  |             SRVRecord record = srvRecords.get(0); | ||||||
|  |             return Optional.of(build(record.getTarget().toString(true) + ":" + record.getPort())); | ||||||
|  |         } catch (TextParseException e) { | ||||||
|  |             log.warn("Unable to perform DNS SRV query for {}: {}", lookupDns, e.getMessage()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return Optional.empty(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public URL build(String authority) { | ||||||
|  |         try { | ||||||
|  |             return new URL(getDefaultScheme() + "://" + authority); | ||||||
|  |         } catch (MalformedURLException e) { | ||||||
|  |             throw new RuntimeException("Could not build URL for " + authority, e); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public URL resolve(String domain) { | ||||||
|  |         Optional<URL> s1 = resolveOverwrite(domain); | ||||||
|  |         if (s1.isPresent()) { | ||||||
|  |             URL dest = s1.get(); | ||||||
|  |             log.info("Resolution of {} via DNS overwrite to {}", domain, dest); | ||||||
|  |             return dest; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         Optional<URL> s2 = resolveLiteral(domain); | ||||||
|  |         if (s2.isPresent()) { | ||||||
|  |             URL dest = s2.get(); | ||||||
|  |             log.info("Resolution of {} as IP literal or IP/hostname with explicit port to {}", domain, dest); | ||||||
|  |             return dest; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         Optional<URL> s3 = resolveWellKnown(domain); | ||||||
|  |         if (s3.isPresent()) { | ||||||
|  |             URL dest = s3.get(); | ||||||
|  |             log.info("Resolution of {} via well-known to {}", domain, dest); | ||||||
|  |             return dest; | ||||||
|  |         } | ||||||
|  |         // The domain needs to be resolved | ||||||
|  |  | ||||||
|  |         Optional<URL> s4 = resolveDnsSrv(domain); | ||||||
|  |         if (s4.isPresent()) { | ||||||
|  |             URL dest = s4.get(); | ||||||
|  |             log.info("Resolution of {} via DNS SRV record to {}", domain, dest); | ||||||
|  |             return dest; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         URL dest = build(domain + ":" + getDefaultPort()); | ||||||
|  |         log.info("Resolution of {} to {}", domain, dest); | ||||||
|  |         return dest; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -21,7 +21,7 @@ | |||||||
| package io.kamax.mxisd.notification; | package io.kamax.mxisd.notification; | ||||||
|  |  | ||||||
| import io.kamax.matrix.ThreePid; | 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.invitation.IThreePidInviteReply; | ||||||
| import io.kamax.mxisd.threepid.session.IThreePidSession; | import io.kamax.mxisd.threepid.session.IThreePidSession; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -21,9 +21,9 @@ | |||||||
| package io.kamax.mxisd.notification; | package io.kamax.mxisd.notification; | ||||||
|  |  | ||||||
| import io.kamax.matrix.ThreePid; | import io.kamax.matrix.ThreePid; | ||||||
| import io.kamax.mxisd.as.IMatrixIdInvite; |  | ||||||
| import io.kamax.mxisd.config.threepid.notification.NotificationConfig; | import io.kamax.mxisd.config.threepid.notification.NotificationConfig; | ||||||
| import io.kamax.mxisd.exception.NotImplementedException; | import io.kamax.mxisd.exception.NotImplementedException; | ||||||
|  | import io.kamax.mxisd.invitation.IMatrixIdInvite; | ||||||
| import io.kamax.mxisd.invitation.IThreePidInviteReply; | import io.kamax.mxisd.invitation.IThreePidInviteReply; | ||||||
| import io.kamax.mxisd.threepid.session.IThreePidSession; | import io.kamax.mxisd.threepid.session.IThreePidSession; | ||||||
| import org.apache.commons.lang.StringUtils; | import org.apache.commons.lang.StringUtils; | ||||||
|   | |||||||
| @@ -37,10 +37,7 @@ import org.slf4j.LoggerFactory; | |||||||
|  |  | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.net.URISyntaxException; | import java.net.URISyntaxException; | ||||||
| import java.util.ArrayList; | import java.util.*; | ||||||
| import java.util.Collection; |  | ||||||
| import java.util.List; |  | ||||||
| import java.util.Optional; |  | ||||||
| import java.util.function.Function; | import java.util.function.Function; | ||||||
| import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||||
|  |  | ||||||
| @@ -113,4 +110,8 @@ public class ProfileManager { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public boolean hasAnyRole(_MatrixID user, List<String> requiredRoles) { | ||||||
|  |         return !requiredRoles.isEmpty() || Collections.disjoint(getRoles(user), requiredRoles); | ||||||
|  |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -38,8 +38,8 @@ import io.kamax.mxisd.notification.NotificationManager; | |||||||
| import io.kamax.mxisd.storage.IStorage; | import io.kamax.mxisd.storage.IStorage; | ||||||
| import io.kamax.mxisd.storage.dao.IThreePidSessionDao; | import io.kamax.mxisd.storage.dao.IThreePidSessionDao; | ||||||
| import io.kamax.mxisd.threepid.session.ThreePidSession; | import io.kamax.mxisd.threepid.session.ThreePidSession; | ||||||
| import org.apache.commons.lang.RandomStringUtils; | import org.apache.commons.lang3.RandomStringUtils; | ||||||
| import org.apache.commons.lang.StringUtils; | import org.apache.commons.lang3.StringUtils; | ||||||
| import org.apache.http.impl.client.CloseableHttpClient; | import org.apache.http.impl.client.CloseableHttpClient; | ||||||
| import org.slf4j.Logger; | import org.slf4j.Logger; | ||||||
| import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||||
| @@ -139,12 +139,13 @@ public class SessionManager { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     public ValidationResult validate(String sid, String secret, String token) { |     public ValidationResult validate(String sid, String secret, String token) { | ||||||
|  |         log.info("Validating session {}", sid); | ||||||
|         ThreePidSession session = getSession(sid, secret); |         ThreePidSession session = getSession(sid, secret); | ||||||
|         log.info("Attempting validation for session {} from {}", session.getId(), session.getServer()); |         log.info("Session {} is from {}", session.getId(), session.getServer()); | ||||||
|  |  | ||||||
|         session.validate(token); |         session.validate(token); | ||||||
|         storage.updateThreePidSession(session.getDao()); |         storage.updateThreePidSession(session.getDao()); | ||||||
|         log.info("Session {} has been validated locally", session.getId()); |         log.info("Session {} has been validated", session.getId()); | ||||||
|  |  | ||||||
|         ValidationResult r = new ValidationResult(session); |         ValidationResult r = new ValidationResult(session); | ||||||
|         session.getNextLink().ifPresent(r::setNextUrl); |         session.getNextLink().ifPresent(r::setNextUrl); | ||||||
|   | |||||||
| @@ -38,6 +38,8 @@ public interface IStorage { | |||||||
|  |  | ||||||
|     void deleteInvite(String id); |     void deleteInvite(String id); | ||||||
|  |  | ||||||
|  |     void insertHistoricalInvite(IThreePidInviteReply data, String resolvedTo, Instant resolvedAt, boolean couldPublish); | ||||||
|  |  | ||||||
|     Optional<IThreePidSessionDao> getThreePidSession(String sid); |     Optional<IThreePidSessionDao> getThreePidSession(String sid); | ||||||
|  |  | ||||||
|     Optional<IThreePidSessionDao> findThreePidSession(ThreePid tpid, String secret); |     Optional<IThreePidSessionDao> findThreePidSession(ThreePid tpid, String secret); | ||||||
|   | |||||||
							
								
								
									
										63
									
								
								src/main/java/io/kamax/mxisd/storage/crypto/FileKeyJson.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								src/main/java/io/kamax/mxisd/storage/crypto/FileKeyJson.java
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										256
									
								
								src/main/java/io/kamax/mxisd/storage/crypto/FileKeyStore.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										256
									
								
								src/main/java/io/kamax/mxisd/storage/crypto/FileKeyStore.java
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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<KeyIdentifier> list() { | ||||||
|  |         List<KeyIdentifier> keyIds = new ArrayList<>(); | ||||||
|  |  | ||||||
|  |         for (KeyType type : KeyType.values()) { | ||||||
|  |             keyIds.addAll(list(type)); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return keyIds; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public List<KeyIdentifier> list(KeyType type) { | ||||||
|  |         List<KeyIdentifier> 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<KeyIdentifier> 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); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										107
									
								
								src/main/java/io/kamax/mxisd/storage/crypto/KeyStore.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								src/main/java/io/kamax/mxisd/storage/crypto/KeyStore.java
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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<KeyIdentifier> 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<KeyIdentifier> 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. <code>getId()</code> 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<KeyIdentifier> getCurrentKey(); | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										113
									
								
								src/main/java/io/kamax/mxisd/storage/crypto/MemoryKeyStore.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								src/main/java/io/kamax/mxisd/storage/crypto/MemoryKeyStore.java
									
									
									
									
									
										Normal file
									
								
							| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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<KeyType, Map<String, Map<String, FileKeyJson>>> keys = new ConcurrentHashMap<>(); | ||||||
|  |     private KeyIdentifier current; | ||||||
|  |  | ||||||
|  |     private Map<String, FileKeyJson> 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<KeyIdentifier> list() { | ||||||
|  |         List<KeyIdentifier> 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<KeyIdentifier> list(KeyType type) { | ||||||
|  |         List<KeyIdentifier> 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<KeyIdentifier> getCurrentKey() { | ||||||
|  |         return Optional.ofNullable(current); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -34,24 +34,18 @@ import io.kamax.mxisd.invitation.IThreePidInviteReply; | |||||||
| import io.kamax.mxisd.storage.IStorage; | import io.kamax.mxisd.storage.IStorage; | ||||||
| import io.kamax.mxisd.storage.dao.IThreePidSessionDao; | import io.kamax.mxisd.storage.dao.IThreePidSessionDao; | ||||||
| import io.kamax.mxisd.storage.ormlite.dao.ASTransactionDao; | 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.ThreePidInviteIO; | ||||||
| import io.kamax.mxisd.storage.ormlite.dao.ThreePidSessionDao; | import io.kamax.mxisd.storage.ormlite.dao.ThreePidSessionDao; | ||||||
| import org.apache.commons.lang.StringUtils; | import org.apache.commons.lang.StringUtils; | ||||||
| import org.slf4j.Logger; |  | ||||||
| import org.slf4j.LoggerFactory; |  | ||||||
|  |  | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.sql.SQLException; | import java.sql.SQLException; | ||||||
| import java.time.Instant; | import java.time.Instant; | ||||||
| import java.util.ArrayList; | import java.util.*; | ||||||
| import java.util.Collection; |  | ||||||
| import java.util.List; |  | ||||||
| import java.util.Optional; |  | ||||||
|  |  | ||||||
| public class OrmLiteSqlStorage implements IStorage { | public class OrmLiteSqlStorage implements IStorage { | ||||||
|  |  | ||||||
|     private transient final Logger log = LoggerFactory.getLogger(OrmLiteSqlStorage.class); |  | ||||||
|  |  | ||||||
|     @FunctionalInterface |     @FunctionalInterface | ||||||
|     private interface Getter<T> { |     private interface Getter<T> { | ||||||
|  |  | ||||||
| @@ -67,6 +61,7 @@ public class OrmLiteSqlStorage implements IStorage { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     private Dao<ThreePidInviteIO, String> invDao; |     private Dao<ThreePidInviteIO, String> invDao; | ||||||
|  |     private Dao<HistoricalThreePidInviteIO, String> expInvDao; | ||||||
|     private Dao<ThreePidSessionDao, String> sessionDao; |     private Dao<ThreePidSessionDao, String> sessionDao; | ||||||
|     private Dao<ASTransactionDao, String> asTxnDao; |     private Dao<ASTransactionDao, String> asTxnDao; | ||||||
|  |  | ||||||
| @@ -86,6 +81,7 @@ public class OrmLiteSqlStorage implements IStorage { | |||||||
|         withCatcher(() -> { |         withCatcher(() -> { | ||||||
|             ConnectionSource connPool = new JdbcConnectionSource("jdbc:" + backend + ":" + path); |             ConnectionSource connPool = new JdbcConnectionSource("jdbc:" + backend + ":" + path); | ||||||
|             invDao = createDaoAndTable(connPool, ThreePidInviteIO.class); |             invDao = createDaoAndTable(connPool, ThreePidInviteIO.class); | ||||||
|  |             expInvDao = createDaoAndTable(connPool, HistoricalThreePidInviteIO.class); | ||||||
|             sessionDao = createDaoAndTable(connPool, ThreePidSessionDao.class); |             sessionDao = createDaoAndTable(connPool, ThreePidSessionDao.class); | ||||||
|             asTxnDao = createDaoAndTable(connPool, ASTransactionDao.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 |     @Override | ||||||
|     public Optional<IThreePidSessionDao> getThreePidSession(String sid) { |     public Optional<IThreePidSessionDao> getThreePidSession(String sid) { | ||||||
|         return withCatcher(() -> Optional.ofNullable(sessionDao.queryForId(sid))); |         return withCatcher(() -> Optional.ofNullable(sessionDao.queryForId(sid))); | ||||||
|   | |||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | 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; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -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 <http://www.gnu.org/licenses/>. | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | package io.kamax.mxisd.threepid.connector.email; | ||||||
|  |  | ||||||
|  | public class BlackholeEmailConnector implements EmailConnector { | ||||||
|  |  | ||||||
|  |     public static final String ID = "none"; | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public String getId() { | ||||||
|  |         return ID; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Override | ||||||
|  |     public void send(String senderAddress, String senderName, String recipient, String content) { | ||||||
|  |         //dev/null | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -33,6 +33,10 @@ public class BuiltInEmailConnectorSupplier implements EmailConnectorSupplier { | |||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public Optional<EmailConnector> apply(EmailConfig cfg, Mxisd mxisd) { |     public Optional<EmailConnector> apply(EmailConfig cfg, Mxisd mxisd) { | ||||||
|  |         if (StringUtils.equals(BlackholeEmailConnector.ID, cfg.getConnector())) { | ||||||
|  |             return Optional.of(new BlackholeEmailConnector()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         if (StringUtils.equals(EmailSmtpConnector.ID, cfg.getConnector())) { |         if (StringUtils.equals(EmailSmtpConnector.ID, cfg.getConnector())) { | ||||||
|             EmailSmtpConfig smtpCfg = GsonUtil.get().fromJson(cfg.getConnectors().getOrDefault(EmailSmtpConnector.ID, new JsonObject()), EmailSmtpConfig.class); |             EmailSmtpConfig smtpCfg = GsonUtil.get().fromJson(cfg.getConnectors().getOrDefault(EmailSmtpConnector.ID, new JsonObject()), EmailSmtpConfig.class); | ||||||
|             return Optional.of(new EmailSmtpConnector(smtpCfg)); |             return Optional.of(new EmailSmtpConnector(smtpCfg)); | ||||||
|   | |||||||
| @@ -22,6 +22,7 @@ package io.kamax.mxisd.threepid.connector.email; | |||||||
|  |  | ||||||
| import com.sun.mail.smtp.SMTPTransport; | import com.sun.mail.smtp.SMTPTransport; | ||||||
| import io.kamax.matrix.ThreePidMedium; | import io.kamax.matrix.ThreePidMedium; | ||||||
|  | import io.kamax.mxisd.Mxisd; | ||||||
| import io.kamax.mxisd.config.threepid.connector.EmailSmtpConfig; | import io.kamax.mxisd.config.threepid.connector.EmailSmtpConfig; | ||||||
| import io.kamax.mxisd.exception.FeatureNotAvailable; | import io.kamax.mxisd.exception.FeatureNotAvailable; | ||||||
| import io.kamax.mxisd.exception.InternalServerError; | import io.kamax.mxisd.exception.InternalServerError; | ||||||
| @@ -30,14 +31,17 @@ import org.apache.commons.lang3.StringUtils; | |||||||
| import org.slf4j.Logger; | import org.slf4j.Logger; | ||||||
| import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||||
|  |  | ||||||
|  | import javax.mail.Header; | ||||||
| import javax.mail.Message; | import javax.mail.Message; | ||||||
| import javax.mail.MessagingException; | import javax.mail.MessagingException; | ||||||
| import javax.mail.Session; | import javax.mail.Session; | ||||||
| import javax.mail.internet.InternetAddress; | import javax.mail.internet.InternetAddress; | ||||||
| import javax.mail.internet.MimeMessage; | import javax.mail.internet.MimeMessage; | ||||||
|  | import javax.mail.internet.MimeUtility; | ||||||
| import java.io.UnsupportedEncodingException; | import java.io.UnsupportedEncodingException; | ||||||
| import java.nio.charset.StandardCharsets; | import java.nio.charset.StandardCharsets; | ||||||
| import java.util.Date; | import java.util.Date; | ||||||
|  | import java.util.Enumeration; | ||||||
| import java.util.Properties; | import java.util.Properties; | ||||||
|  |  | ||||||
| public class EmailSmtpConnector implements EmailConnector { | public class EmailSmtpConnector implements EmailConnector { | ||||||
| @@ -65,6 +69,10 @@ public class EmailSmtpConnector implements EmailConnector { | |||||||
|             sCfg.setProperty("mail.smtp.auth", "true"); |             sCfg.setProperty("mail.smtp.auth", "true"); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         if (cfg.getTls() == 3) { | ||||||
|  |             sCfg.setProperty("mail.smtp.ssl.enable", "true"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         session = Session.getInstance(sCfg); |         session = Session.getInstance(sCfg); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -92,7 +100,16 @@ public class EmailSmtpConnector implements EmailConnector { | |||||||
|         try { |         try { | ||||||
|             InternetAddress sender = new InternetAddress(senderAddress, senderName); |             InternetAddress sender = new InternetAddress(senderAddress, senderName); | ||||||
|             MimeMessage msg = new MimeMessage(session, IOUtils.toInputStream(content, StandardCharsets.UTF_8)); |             MimeMessage msg = new MimeMessage(session, IOUtils.toInputStream(content, StandardCharsets.UTF_8)); | ||||||
|             msg.setHeader("X-Mailer", "mxisd"); // FIXME set version |  | ||||||
|  |             // We must encode our headers ourselves as we have no guarantee that they were in the provided data. | ||||||
|  |             // This is required to support UTF-8 characters from user display names or room names in the subject header per example | ||||||
|  |             Enumeration<Header> headers = msg.getAllHeaders(); | ||||||
|  |             while (headers.hasMoreElements()) { | ||||||
|  |                 Header header = headers.nextElement(); | ||||||
|  |                 msg.setHeader(header.getName(), MimeUtility.encodeText(header.getValue())); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             msg.setHeader("X-Mailer", MimeUtility.encodeText(Mxisd.Agent)); | ||||||
|             msg.setSentDate(new Date()); |             msg.setSentDate(new Date()); | ||||||
|             msg.setFrom(sender); |             msg.setFrom(sender); | ||||||
|             msg.setRecipients(Message.RecipientType.TO, recipient); |             msg.setRecipients(Message.RecipientType.TO, recipient); | ||||||
| @@ -100,8 +117,11 @@ public class EmailSmtpConnector implements EmailConnector { | |||||||
|  |  | ||||||
|             log.info("Sending invite to {} via SMTP using {}:{}", recipient, cfg.getHost(), cfg.getPort()); |             log.info("Sending invite to {} via SMTP using {}:{}", recipient, cfg.getHost(), cfg.getPort()); | ||||||
|             SMTPTransport transport = (SMTPTransport) session.getTransport("smtp"); |             SMTPTransport transport = (SMTPTransport) session.getTransport("smtp"); | ||||||
|  |  | ||||||
|  |             if (cfg.getTls() < 3) { | ||||||
|                 transport.setStartTLS(cfg.getTls() > 0); |                 transport.setStartTLS(cfg.getTls() > 0); | ||||||
|                 transport.setRequireStartTLS(cfg.getTls() > 1); |                 transport.setRequireStartTLS(cfg.getTls() > 1); | ||||||
|  |             } | ||||||
|  |  | ||||||
|             log.info("Connecting to {}:{}", cfg.getHost(), cfg.getPort()); |             log.info("Connecting to {}:{}", cfg.getHost(), cfg.getPort()); | ||||||
|             if (StringUtils.isAllEmpty(cfg.getLogin(), cfg.getPassword())) { |             if (StringUtils.isAllEmpty(cfg.getLogin(), cfg.getPassword())) { | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user