Compare commits
	
		
			63 Commits
		
	
	
		
			v1.3.0-rc.
			...
			v1.4.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 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 | ||
|  | 249cc0ea92 | ||
|  | 99697d7c75 | ||
|  | e133e120d7 | ||
|  | e39d6bfa10 | ||
|  | 217bc423ed | ||
|  | 8f0654c34e | ||
|  | 8afdb3ed83 | ||
|  | bd4ccbc5e5 | ||
|  | 6d1c6ed109 | ||
|  | 1619f5311c | ||
|  | 6fa36ea092 | ||
|  | 471e06536b | ||
|  | 3a6b75996c | ||
|  | 566e4f3137 | ||
|  | a4c18dee5d | ||
|  | 8d6850d346 | ||
|  | 67bc18af7d | ||
|  | 5c660fdcaf | 
							
								
								
									
										25
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								README.md
									
									
									
									
									
								
							| @@ -14,13 +14,14 @@ mxisd - Federated Matrix Identity Server | ||||
|  | ||||
| # Overview | ||||
| mxisd is a Federated Matrix Identity server for self-hosted Matrix infrastructures with [enhanced features](#features). | ||||
| As an enhanced Identity service, it implements the [Matrix Identity service API](https://kamax.io/matrix/api/identity_service/unstable.html) | ||||
| As an enhanced Identity service, it implements the [Identity service API](https://matrix.org/docs/spec/identity_service/r0.1.0.html) | ||||
| and several [extra features](#features) that greatly enhance user experience within Matrix. | ||||
| It is the one stop shop for anything regarding Authentication, Directory and Identity management in Matrix built in a | ||||
| single coherent product. | ||||
|    | ||||
| mxisd is specifically designed to connect to an existing on-premise Identity store (AD/Samba/LDAP, SQL Database, | ||||
| Web services/app, etc.) and ease the integration of a Matrix infrastructure within an existing one. | ||||
| Web services/app, etc.) and ease the integration of a Matrix infrastructure within an existing one.   | ||||
| Check [our FAQ entry](docs/faq.md#what-kind-of-setup-is-mxisd-really-designed-for) to know if mxisd is a good fit for you. | ||||
|  | ||||
| The core principle of mxisd is to map between Matrix IDs and 3PIDs (Third-Party IDentifiers) for the Homeserver and its | ||||
| users. 3PIDs can be anything that uniquely and globally identify a user, like: | ||||
| @@ -30,18 +31,18 @@ users. 3PIDs can be anything that uniquely and globally identify a user, like: | ||||
| - Twitter handle | ||||
| - Facebook ID | ||||
|  | ||||
| If you are unfamiliar with the Identity vocabulary and concepts in Matrix, **please read this [introduction](docs/concepts.md)**. | ||||
| If you are unfamiliar with the Identity vocabulary and concepts in Matrix, **please read this [introduction](docs/concepts.md)**.   | ||||
|  | ||||
| # Features | ||||
| [Identity](docs/features/identity.md): As a [regular Matrix Identity service](https://kamax.io/matrix/api/identity_service/unstable.html#general-principles): | ||||
| [Identity](docs/features/identity.md): As a [regular Matrix Identity service](https://matrix.org/docs/spec/identity_service/r0.1.0.html#general-principles): | ||||
| - Search for people by 3PID using its own Identity stores | ||||
|   ([Spec](https://kamax.io/matrix/api/identity_service/unstable.html#association-lookup)) | ||||
|   ([Spec](https://matrix.org/docs/spec/identity_service/r0.1.0.html#association-lookup)) | ||||
| - Invite people to rooms by 3PID using its own Identity stores, with notifications to the invitee (Email, SMS, etc.) | ||||
|   ([Spec](https://kamax.io/matrix/api/identity_service/unstable.html#post-matrix-identity-api-v1-store-invite)) | ||||
|   ([Spec](https://matrix.org/docs/spec/identity_service/r0.1.0.html#post-matrix-identity-api-v1-store-invite)) | ||||
| - Allow users to add 3PIDs to their settings/profile | ||||
|   ([Spec](https://kamax.io/matrix/api/identity_service/unstable.html#establishing-associations)) | ||||
|   ([Spec](https://matrix.org/docs/spec/identity_service/r0.1.0.html#establishing-associations)) | ||||
| - Register accounts on your Homeserver with 3PIDs | ||||
|   ([Spec](https://kamax.io/matrix/api/identity_service/unstable.html#establishing-associations)) | ||||
|   ([Spec](https://matrix.org/docs/spec/identity_service/r0.1.0.html#establishing-associations)) | ||||
|  | ||||
| As an enhanced Identity service: | ||||
| - [Federation](docs/features/federation.md): Use a recursive lookup mechanism when searching and inviting people by 3PID, | ||||
| @@ -52,6 +53,7 @@ As an enhanced Identity service: | ||||
|   - Central Matrix Identity servers | ||||
| - [Session Control](docs/threepids/session/session.md): Extensive control of where 3PIDs are transmitted so they are not | ||||
|   leaked publicly by users | ||||
| - [Registration control](docs/features/registration.md): Control and restrict user registration based on 3PID patterns or criterias, like a pending invite | ||||
| - [Authentication](docs/features/authentication.md): Use your Identity stores to perform authentication in [synapse](https://github.com/matrix-org/synapse) | ||||
|   via the [REST password provider](https://github.com/kamax-io/matrix-synapse-rest-auth) | ||||
| - [Directory search](docs/features/directory.md) which allows you to search for users within your organisation, | ||||
| @@ -67,15 +69,18 @@ As an enhanced Identity service: | ||||
| - Users can directly find each other using whatever attribute is relevant within your Identity store | ||||
| - Federate your Identity server so you can discover others and/or others can discover you | ||||
|  | ||||
| Also, check [our FAQ entry](docs/faq.md#what-kind-of-setup-is-mxisd-really-designed-for) to know if mxisd is a good fit for you. | ||||
|  | ||||
| # Getting started | ||||
| See the [dedicated document](docs/getting-started.md) | ||||
|  | ||||
| # Support | ||||
| ## Troubleshooting | ||||
| A basic troubleshooting guide is available [here](docs/troubleshooting.md). | ||||
|  | ||||
| ## Community | ||||
| 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 | ||||
| If you would prefer professional support/custom development for mxisd and/or for Matrix in general, including other open | ||||
| source technologies/products: | ||||
|   | ||||
							
								
								
									
										15
									
								
								build.gradle
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								build.gradle
									
									
									
									
									
								
							| @@ -145,6 +145,9 @@ dependencies { | ||||
|  | ||||
|     // HTTP server | ||||
|     compile 'io.undertow:undertow-core:2.0.16.Final' | ||||
|      | ||||
|     // Command parser for AS interface | ||||
|     implementation 'commons-cli:commons-cli:1.4' | ||||
|  | ||||
|     testCompile 'junit:junit:4.12' | ||||
|     testCompile 'com.github.tomakehurst:wiremock:2.8.0' | ||||
| @@ -152,6 +155,14 @@ dependencies { | ||||
|     testCompile 'com.icegreen:greenmail:1.5.9' | ||||
| } | ||||
|  | ||||
| jar { | ||||
|     manifest { | ||||
|         attributes( | ||||
|             'Implementation-Version': mxisdVersion() | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| shadowJar { | ||||
|     baseName = project.name | ||||
|     classifier = null | ||||
| @@ -190,13 +201,13 @@ task debBuild(dependsOn: shadowJar) { | ||||
|         ant.replaceregexp( // FIXME adapt to new config format | ||||
|                 file: "${debBuildConfPath}/${debConfFileName}", | ||||
|                 match: "key:\\R  path:(.*)", | ||||
|                 replace: "key:\n  path: '${debDataPath}/signing.key'" | ||||
|                 replace: "key:\n  path: '${debDataPath}/keys'" | ||||
|         ) | ||||
|  | ||||
|         ant.replaceregexp( // FIXME adapt to new config format | ||||
|                 file: "${debBuildConfPath}/${debConfFileName}", | ||||
|                 match: "storage:\\R  provider:\\R    sqlite:\\R      database:(.*)", | ||||
|                 replace: "storage:\n  provider:\n    sqlite:\n      database: '${debDataPath}/mxisd.db'" | ||||
|                 replace: "storage:\n  provider:\n    sqlite:\n      database: '${debDataPath}/store.db'" | ||||
|         ) | ||||
|  | ||||
|         copy { | ||||
|   | ||||
							
								
								
									
										22
									
								
								docs/faq.md
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								docs/faq.md
									
									
									
									
									
								
							| @@ -16,6 +16,18 @@ of the Matrix protocol is required for some advanced features. | ||||
| If all fails, come over to [the project room](https://matrix.to/#/#mxisd:kamax.io) and we'll do our best to get you | ||||
| started and answer questions you might have. | ||||
|  | ||||
| ### What kind of setup is mxisd really designed for? | ||||
| mxisd is primarily designed for setups that: | ||||
| - [Care for their privacy](https://github.com/kamax-matrix/mxisd/wiki/mxisd-and-your-privacy) | ||||
| - Have their own [domains](https://en.wikipedia.org/wiki/Domain_name) | ||||
| - Use those domains for their email addresses and all other services | ||||
| - Already have an [Identity store](stores/README.md), typically [LDAP-based](stores/ldap.md). | ||||
|  | ||||
| If you meet all the conditions, then you are the prime use case we designed mxisd for.  | ||||
|  | ||||
| If you meet some of the conditions, but not all, mxisd will still be a good fit for you but you won't fully enjoy all its | ||||
| features. | ||||
|  | ||||
| ### Do I need to use mxisd if I run a Homeserver? | ||||
| No, but it is strongly recommended, even if you don't use any Identity store or integration. | ||||
|  | ||||
| @@ -23,9 +35,6 @@ In its default configuration, mxisd uses other federated public servers when per | ||||
| It can also [be configured](features/identity.md#lookups) to use the central matrix.org servers, giving you access to at | ||||
| least the same information as if you were not running it. | ||||
|  | ||||
| It will also give your users a choice to make their 3PIDs available publicly, ensuring they are made aware of the | ||||
| privacy consequences, which is not the case with the central Matrix.org servers. | ||||
|  | ||||
| So mxisd is like your gatekeeper and guardian angel. It does not change what you already know, just adds some nice | ||||
| simple features on top of it. | ||||
|  | ||||
| @@ -47,13 +56,14 @@ Accounts cannot currently migrate/move from one server to another. | ||||
| See a [brief explanation document](concepts.md) about Matrix and mxisd concepts and vocabulary. | ||||
|  | ||||
| ### I already use the synapse LDAP3 auth provider. Why should I care about mxisd? | ||||
| The [synapse LDAP3 auth provider](https://github.com/matrix-org/matrix-synapse-ldap3) is not longer maintained and | ||||
| only handles on specific flow: validate credentials at login. | ||||
| The [synapse LDAP3 auth provider](https://github.com/matrix-org/matrix-synapse-ldap3) is not longer maintained despite | ||||
| saying so and only handles on specific flow: validate credentials at login. | ||||
|  | ||||
| It does not: | ||||
| - Auto-provision user profiles | ||||
| - Integrate with Identity management | ||||
| - Integrate with Directory searches | ||||
| - Integrate with Profile data | ||||
|  | ||||
| mxisd is a replacement and enhancement of it, offering coherent results in all areas, which the LDAP3 auth provider | ||||
| does not. | ||||
| @@ -74,7 +84,7 @@ No. | ||||
| In its default configuration, mxisd does not talk to the central Identity server matrix.org to avoid leaking your private | ||||
| data and those of people you might know. | ||||
|  | ||||
| mxisd [can be configured](features/identity.md#lookups) to talk to the central Identity servers if you wish. | ||||
| [You can configure it](features/identity.md#lookups) to talk to the central Identity servers if you wish. | ||||
|  | ||||
| ### So mxisd is just a big hack! I don't want to use non-official features! | ||||
| mxisd primary concerns are your privacy and to always be compatible with the Matrix ecosystem and the Identity service API.   | ||||
|   | ||||
| @@ -21,7 +21,7 @@ It allows to use Identity stores configured in mxisd to authenticate users on yo | ||||
|  | ||||
| Authentication is divided into two parts: | ||||
| - [Basic](#basic): authenticate with a regular username. | ||||
| - [Advanced](#advanced): same as basic with extra ability to authenticate using a 3PID. | ||||
| - [Advanced](#advanced): same as basic with extra abilities like authenticate using a 3PID or do username rewrite. | ||||
|  | ||||
| ## Basic | ||||
| Authentication by username is possible by linking synapse and mxisd together using a specific module for synapse, also | ||||
| @@ -145,7 +145,49 @@ Your VirtualHost should now look similar to: | ||||
| </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 | ||||
|  | ||||
| Just like you need to configure a reverse proxy to send client requests to mxisd, you also need to configure mxisd with | ||||
| the internal IP of the Homeserver so it can talk to it directly to integrate its directory search. | ||||
|  | ||||
| @@ -165,6 +207,12 @@ In case the hostname is the same as your Matrix domain and `server.name` is not | ||||
|  | ||||
| `value` is the base internal URL of the Homeserver, without any `/_matrix/..` or trailing `/`. | ||||
|  | ||||
| ### Optional features | ||||
|  | ||||
| The following features are available after you have a working Advanced setup: | ||||
|  | ||||
| - Username rewrite: Allows you to rewrite the username of a regular login/pass authentication to a 3PID, that then gets resolved using the regular lookup process. Most common use case is to allow login with numerical usernames on synapse, which is not possible out of the box. | ||||
|  | ||||
| #### Username rewrite | ||||
| In mxisd config: | ||||
| ```yaml | ||||
|   | ||||
| @@ -1,65 +1,66 @@ | ||||
| # Integration as an Application Service | ||||
| # Application Service | ||||
| **WARNING:** These features are currently highly experimental. They can be removed or modified without notice.   | ||||
| All the features requires a Homeserver capable of connecting Application Services. | ||||
| All the features requires a Homeserver capable of connecting [Application Services](https://matrix.org/docs/spec/application_service/r0.1.0.html). | ||||
|  | ||||
| ## Email notification for Room invites by Matrix ID | ||||
| 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. | ||||
| 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) | ||||
|  | ||||
| ### Requirements | ||||
| - [Identity store(s)](../../stores/README.md) supporting the Profile feature | ||||
| - At least one email entry in the identity store for each user that could be invited. | ||||
| ## 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 | ||||
| In your mxisd config file: | ||||
| #### 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 | ||||
| matrix: | ||||
|   listener: | ||||
|     url:  '<URL TO THE CS API OF THE HOMESERVER>' | ||||
|     localpart: 'appservice-mxisd' | ||||
|     token: | ||||
|       hs: 'HS_TOKEN_CHANGE_ME' | ||||
|  | ||||
| synapseSql: | ||||
|   enabled: false ## Do not use this line if Synapse is used as an Identity Store | ||||
|   type: '<DB TYPE>' | ||||
|   connection: '<DB CONNECTION URL>' | ||||
| appsvc: | ||||
|   enabled: true | ||||
|   endpoint: | ||||
|     toHS: | ||||
|       url: 'http://localhost:8008' | ||||
|       token: 'ExampleTokenToHS-ChangeMe!' | ||||
|     toAS: | ||||
|       url: 'http://localhost:8090' | ||||
|       token: 'ExampleTokenToAS-ChangeMe!' | ||||
| ``` | ||||
|  | ||||
| The `synapseSql` section is used to retrieve display names which are not directly accessible in this mode. | ||||
| For details about `type` and `connection`, see the [relevant documentation](../../stores/synapse.md). | ||||
| If you do not configure it, some placeholders will not be available in the notification, like the Room name. | ||||
|  | ||||
| You can also change the default template of the notification using the `generic.matrixId` template option.   | ||||
| See [the Template generator documentation](../../threepids/notification/template-generator.md) for more info. | ||||
|  | ||||
| ### Homeserver integration | ||||
| ### Integration | ||||
| #### Synapse | ||||
| Create a new appservice registration file. Futher config will assume it is in `/etc/matrix-synapse/appservice-mxisd.yaml` | ||||
| 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 | ||||
| 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: [] | ||||
| appsvc: | ||||
|   registration: | ||||
|     synapse: | ||||
|       file: '/etc/matrix-synapse/mxisd-appservice-registration.yaml' | ||||
| ``` | ||||
| `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' | ||||
|   - '/etc/matrix-synapse/mxisd-appservice-registration.yaml' | ||||
|   - ... | ||||
| ``` | ||||
|  | ||||
| @@ -68,5 +69,54 @@ Restart synapse when done to register mxisd. | ||||
| #### Others | ||||
| See your Homeserver documentation on how to integrate. | ||||
|  | ||||
| ### Test | ||||
| ## Capabilities | ||||
| ### Admin commands | ||||
| #### Setup | ||||
| Min config: | ||||
| ```yaml | ||||
| appsvc: | ||||
|   feature: | ||||
|     admin: | ||||
|       allowedRoles: | ||||
|         - '+aMatrixCommunity:example.org' | ||||
|         - 'SomeLdapGroup' | ||||
|         - 'AnyOtherArbitraryRoleFromIdentityStores' | ||||
| ``` | ||||
|  | ||||
| #### Use | ||||
| The following steps assume: | ||||
| - `matrix.domain` set to `example.org` | ||||
| - `appsvc.user.main` set to `mxisd` or not set | ||||
|  | ||||
| 1. Invite `@mxisd:example.org` to a new direct chat | ||||
| 2. Type `!help` to get all available commands | ||||
|  | ||||
| ### Email Notification about room invites by Matrix IDs | ||||
| This feature allows for users found in Identity stores to be instantly notified about Room Invites, regardless if their | ||||
| account was already provisioned on the Homeserver. | ||||
|  | ||||
| #### Requirements | ||||
| - [Identity store(s)](../../stores/README.md) supporting the Profile feature | ||||
| - At least one email entry in the identity store for each user that could be invited. | ||||
|  | ||||
| #### Configuration | ||||
| In your mxisd config file: | ||||
| ```yaml | ||||
| synapseSql: | ||||
|   enabled: false ## Do not use this line if Synapse is used as an Identity Store | ||||
|   type: '<DB TYPE>' | ||||
|   connection: '<DB CONNECTION URL>' | ||||
| ``` | ||||
|  | ||||
| The `synapseSql` section is optional. It is used to retrieve display names which are not directly accessible in this mode. | ||||
| For details about `type` and `connection`, see the [relevant documentation](../../stores/synapse.md). | ||||
| If you do not configure it, some placeholders will not be available in the notification, like the Room name. | ||||
|  | ||||
| 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. | ||||
|  | ||||
| #### Test | ||||
| Invite a user which is part of your domain while an appropriate Identity store is used. | ||||
|  | ||||
| ### Auto-reject of expired 3PID invites | ||||
| *TBC* | ||||
|   | ||||
| @@ -46,15 +46,6 @@ lookup: | ||||
| invite: | ||||
|   resolution: | ||||
|     recursive: false | ||||
| session: | ||||
|   policy: | ||||
|     validation: | ||||
|       forLocal: | ||||
|         toRemote: | ||||
|           enabled: false | ||||
|       forRemote: | ||||
|         toRemote: | ||||
|           enabled: false | ||||
| ```  | ||||
|  | ||||
| There is currently no way to selectively disable federation towards specific servers, but this feature is planned. | ||||
|   | ||||
| @@ -1,7 +1,12 @@ | ||||
| # Identity | ||||
| **WARNING**: This document is incomplete and can be misleading. | ||||
| Implementation of the [Identity Service API r0.1.0](https://matrix.org/docs/spec/identity_service/r0.1.0.html). | ||||
|  | ||||
| Implementation of the [Unofficial Matrix Identity Service API](https://kamax.io/matrix/api/identity_service/unstable.html). | ||||
| - [Lookups](#lookups) | ||||
| - [Invitations](#invitations) | ||||
|   - [Expiration](#expiration) | ||||
|   - [Policies](#policies) | ||||
|   - [Resolution](#resolution) | ||||
| - [3PIDs Management](#3pids-management) | ||||
|  | ||||
| ## Lookups | ||||
| If you would like to use the central matrix.org Identity server to ensure maximum discovery at the cost of potentially | ||||
| @@ -14,8 +19,78 @@ forward: | ||||
| **NOTE:** You should carefully consider enabling this option, which is discouraged.   | ||||
| For more info, see the [relevant issue](https://github.com/kamax-matrix/mxisd/issues/76). | ||||
|  | ||||
| ## Room Invitations | ||||
| Resolution can be customized using the following configuration: | ||||
| ## Invitations | ||||
| ### Expiration | ||||
| #### Overview | ||||
| Matrix does not provide a mean to remove/cancel pending 3PID invitations with the APIs. The current reference | ||||
| implementations also do not provide any mean to do so. This leads to 3PID invites forever stuck in rooms. | ||||
|  | ||||
| To provide this functionality, mxisd uses a workaround: resolve the invite to a dedicated User ID, which can be | ||||
| controlled by mxisd or a bot/service that will then reject the invite. | ||||
|  | ||||
| If this dedicated User ID is to be controlled by mxisd, the [Application Service](experimental/application-service.md) | ||||
| feature must be configured and integrated with your Homeserver, as well as the *Auto-reject 3PID invite capability*. | ||||
|  | ||||
| #### Configuration | ||||
| ```yaml | ||||
| invite: | ||||
|   expiration: | ||||
|     enabled: true/false | ||||
|     after: 5 | ||||
|     resolveTo: '@john.doe:example.org' | ||||
| ``` | ||||
| `enabled` | ||||
| - Purpose: Enable or disable the invite expiration feature. | ||||
| - Default: `true` | ||||
|  | ||||
| `after`  | ||||
| - Purpose: Amount of minutes before an invitation expires. | ||||
| - Default: `10080` (7 days) | ||||
|  | ||||
| `resolveTo` | ||||
| - Purpose: Matrix User ID to resolve the expired invitations to. | ||||
| - Default: Computed from `appsvc.user.inviteExpired` and `matrix.domain` | ||||
|  | ||||
| ### Policies | ||||
| 3PID invite policies are the companion feature of [Registration](registration.md). While the Registration feature acts on | ||||
| requirements for the invitee/register, this feature acts on requirement for the one(s) performing 3PID invites, ensuring | ||||
| a coherent system. | ||||
|  | ||||
| It relies on only allowing people with specific [Roles](profile.md) to perform 3PID invites. This would typically allow | ||||
| a tight-control on a server setup with is "invite-only" or semi-open (relying on trusted people to invite new members). | ||||
|  | ||||
| It's a middle ground between a closed server, where every user must be created or already exists in an Identity store, | ||||
| and an open server, where anyone can register. | ||||
|   | ||||
| #### Integration | ||||
| Because Identity Servers do not control 3PID invites as per Matrix spec, mxisd needs to intercept a set of Homeserver | ||||
| endpoints to apply the policies. | ||||
|  | ||||
| ##### Reverse Proxy | ||||
| ###### nginx | ||||
| **IMPORTANT**: Must be placed before your global `/_matrix` entry: | ||||
| ```nginx | ||||
| location ~* ^/_matrix/client/r0/rooms/([^/]+)/invite$ { | ||||
|     proxy_pass		    http://127.0.0.1:8090; | ||||
|     proxy_set_header	Host $host; | ||||
|     proxy_set_header	X-Forwarded-For $remote_addr; | ||||
| } | ||||
| ``` | ||||
|  | ||||
| #### Configuration | ||||
| The only policy currently available is to restrict 3PID invite to users having a specific (set of) role(s), like so: | ||||
|  | ||||
| ```yaml | ||||
| invite: | ||||
|   policy: | ||||
|     ifSender: | ||||
|       hasRole: | ||||
|         - '<THIS_ROLE>' | ||||
|         - '<OR_THIS_ROLE>' | ||||
| ``` | ||||
|  | ||||
| ### Resolution | ||||
| Resolution of 3PID invitations can be customized using the following configuration: | ||||
|  | ||||
| `invite.resolution.recursive`   | ||||
| - Default value: `true`   | ||||
| @@ -28,5 +103,5 @@ Resolution can be customized using the following configuration: | ||||
| - Default value: `1`   | ||||
| - Description: How often, in minutes, mxisd should try to resolve pending invites. | ||||
|  | ||||
| ## 3PID addition to user profile | ||||
| ## 3PIDs Management | ||||
| See the [3PID session documents](../threepids/session) | ||||
|   | ||||
							
								
								
									
										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. | ||||
| @@ -6,8 +6,7 @@ | ||||
| 5. [Validate](#validate) | ||||
| 6. [Next steps](#next-steps) | ||||
|  | ||||
| Following these quick start instructions, you will have a basic setup that can perform recursive/federated lookups and | ||||
| talk to the central Matrix.org Identity server.   | ||||
| Following these quick start instructions, you will have a basic setup that can perform recursive/federated lookups.   | ||||
| This will be a good ground work for further integration with features and your existing Identity stores. | ||||
|  | ||||
| --- | ||||
| @@ -24,13 +23,17 @@ You will need: | ||||
| - Working Homeserver, ideally with working federation | ||||
| - Reverse proxy with regular TLS/SSL certificate (Let's encrypt) for your mxisd domain | ||||
|  | ||||
| As synapse requires an HTTPS connection when talking to an Identity service, **a reverse proxy is required** as mxisd does | ||||
| not support HTTPS listener at this time. | ||||
| If you use synapse: | ||||
| - It requires an HTTPS connection when talking to an Identity service, **a reverse proxy is required** as mxisd does | ||||
|   not support HTTPS listener at this time. | ||||
| - HTTPS is hardcoded when talking to the Identity server. If your Identity server URL in your client is `https://matrix.example.org/`, | ||||
|   then you need to ensure `https://matrix.example.org/_matrix/identity/api/v1/...` will reach mxisd if called from the synapse host. | ||||
|   In doubt, test with `curl` or similar.  | ||||
|  | ||||
| For maximum integration, it is best to have your Homeserver and mxisd reachable via the same hostname. | ||||
| For maximum integration, it is best to have your Homeserver and mxisd reachable via the same public hostname. | ||||
|  | ||||
| Be aware of a [NAT/Reverse proxy gotcha](https://github.com/kamax-matrix/mxisd/wiki/Gotchas#nating) if you use the same | ||||
| hostname. | ||||
| host. | ||||
|  | ||||
| The following Quick Start guide assumes you will host the Homeserver and mxisd under the same hostname.   | ||||
| If you would like a high-level view of the infrastructure and how each feature is integrated, see the | ||||
| @@ -51,17 +54,10 @@ See the [Latest release](https://github.com/kamax-matrix/mxisd/releases/latest) | ||||
|    | ||||
| > **NOTE**: Details about configuration syntax and format are described [here](configure.md) | ||||
|  | ||||
| Create/edit a minimal configuration (see installer doc for the location): | ||||
| ```yaml | ||||
| matrix: | ||||
|   domain: 'example.org' | ||||
| key: | ||||
|   path: '/path/to/signing.key.file' | ||||
| storage: | ||||
|   provider: | ||||
|     sqlite: | ||||
|       database: '/path/to/mxisd.db' | ||||
| ```   | ||||
| If you haven't created a configuration file yet, copy `mxisd.example.yaml` to where the configuration file is stored given | ||||
| your installation method and edit to your needs. | ||||
|  | ||||
| The following items must be at least configured: | ||||
| - `matrix.domain` should be set to your Homeserver domain (`server_name` in synapse configuration) | ||||
| - `key.path` will store the signing keys, which must be kept safe! If the file does not exist, keys will be generated for you. | ||||
| - `storage.provider.sqlite.database` is the location of the SQLite Database file which will hold state (invites, etc.) | ||||
| @@ -83,9 +79,9 @@ ProxyPass /_matrix/identity http://0.0.0.0:8090/_matrix/identity | ||||
| Typical configuration would look like: | ||||
| ```apache | ||||
| <VirtualHost *:443> | ||||
|     ServerName example.org | ||||
|     ServerName matrix.example.org | ||||
|      | ||||
|     ... | ||||
|     # ... | ||||
|      | ||||
|     ProxyPreserveHost on | ||||
|     ProxyPass /_matrix/identity http://localhost:8090/_matrix/identity | ||||
| @@ -107,9 +103,9 @@ Typical configuration would look like: | ||||
| ```nginx | ||||
| server { | ||||
|     listen 443 ssl; | ||||
|     server_name example.org; | ||||
|     server_name matrix.example.org; | ||||
|      | ||||
|     ... | ||||
|     # ... | ||||
|      | ||||
|     location /_matrix/identity { | ||||
|         proxy_pass http://localhost:8090/_matrix/identity; | ||||
| @@ -130,17 +126,17 @@ Add your mxisd domain into the `homeserver.yaml` at `trusted_third_party_id_serv | ||||
| In a typical configuration, you would end up with something similar to: | ||||
| ```yaml | ||||
| trusted_third_party_id_servers: | ||||
|     - example.org | ||||
|     - matrix.example.org | ||||
| ``` | ||||
| It is recommended to remove `matrix.org` and `vector.im` (or any other default entry) from your configuration so only | ||||
| your own Identity server is authoritative for your HS. | ||||
| It is **highly recommended** to remove `matrix.org` and `vector.im` (or any other default entry) from your configuration | ||||
| so only your own Identity server is authoritative for your HS. | ||||
|  | ||||
| ## Validate | ||||
| **NOTE:** In case your homeserver has no working federation, step 5 will not happen. If step 4 took place, consider | ||||
| your installation validated. | ||||
|  | ||||
| 1. Log in using your Matrix client and set `https://example.org` as your Identity server URL, replacing `example.org` by | ||||
| the relevant hostname which you configured in your reverse proxy. | ||||
| 1. Log in using your Matrix client and set `https://matrix.example.org` as your Identity server URL, replacing `matrix.example.org` | ||||
| by the relevant hostname which you configured in your reverse proxy. | ||||
| 2. Create a new empty room. All further actions will take place in this room. | ||||
| 3. Invite `mxisd-federation-test@kamax.io` | ||||
| 4. The 3PID invite should be turned into a Matrix invite to `@mxisd-lookup-test:kamax.io`. | ||||
| @@ -148,7 +144,8 @@ the relevant hostname which you configured in your reverse proxy. | ||||
| **NOTE:** You might not see a suggestion for the e-mail address, which is normal. Still proceed with the invite. | ||||
|    | ||||
| If it worked, it means you are up and running and can enjoy mxisd in its basic mode! Congratulations!   | ||||
| If it did not work, [get in touch](../README.md#support) and we'll do our best to get you started. | ||||
| If it did not work, read the basic [troubleshooting guide](troubleshooting.md), [get in touch](../README.md#support) and | ||||
| we'll do our best to get you started. | ||||
|  | ||||
| ## Next steps | ||||
| Once your mxisd server is up and running, there are several ways you can enhance and integrate further with your | ||||
| @@ -156,3 +153,5 @@ infrastructure: | ||||
|  | ||||
| - [Enable extra features](features/) | ||||
| - [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/) | ||||
|  | ||||
| ## 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. | ||||
| @@ -7,7 +7,7 @@ Follow the [build instructions](../build.md) then: | ||||
| # Create a dedicated user | ||||
| useradd -r mxisd | ||||
|  | ||||
| # Create config directory and set ownership | ||||
| # Create config directory | ||||
| mkdir -p /etc/mxisd | ||||
|  | ||||
| # Create data directory and set ownership | ||||
| @@ -26,7 +26,7 @@ ln -s /usr/lib/mxisd/mxisd /usr/bin/mxisd | ||||
| ``` | ||||
|  | ||||
| ### Prepare config file | ||||
| Copy the sample config file `./mxisd.example.yaml` to `/etc/mxisd/mxisd.yaml`, edit to your needs | ||||
| Copy the configuration file you've created following the build instructions to `/etc/mxisd/mxisd.yaml` | ||||
|  | ||||
| ### Prepare Systemd | ||||
| 1. Copy `src/systemd/mxisd.service` to `/etc/systemd/system/` and edit if needed | ||||
|   | ||||
							
								
								
									
										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. | ||||
| @@ -39,7 +39,7 @@ | ||||
| | [Authentication](../features/authentication.md) | Yes       | | ||||
| | [Directory](../features/directory.md)           | Yes       | | ||||
| | [Identity](../features/identity.md)             | Yes       | | ||||
| | [Profile](#profile)                             | Yes       | | ||||
| | [Profile](../features/profile.md)               | Yes       | | ||||
|  | ||||
| This Identity Store lets you run arbitrary commands to handle the various requests in each support feature.   | ||||
| It is the most versatile Identity store of mxisd, allowing you to connect any kind of logic with any executable/script. | ||||
| @@ -199,7 +199,7 @@ exec: | ||||
|     DOMAIN: '{domain}' | ||||
| ``` | ||||
| With Authentication enabled, run `/opt/mxisd-exec/auth.sh` when validating credentials, providing: | ||||
| - A single command-line argument to provide the `localoart` as username  | ||||
| - A single command-line argument to provide the `localpart` as username  | ||||
| - A plain text string with the password token for standard input, which will be replaced by the password to check | ||||
| - A single environment variable `DOMAIN` containing Matrix ID domain, if given | ||||
|  | ||||
| @@ -207,26 +207,34 @@ The command will use the default values for: | ||||
| - Success exit status of `0` | ||||
| - Failure exit status of `1` | ||||
| - Any other exit status considered as error | ||||
| - The standard output processing as not processed | ||||
| - Standard output will not be processed | ||||
|  | ||||
| #### Advanced | ||||
| Given the fictional `placeholder` feature: | ||||
| ```yaml | ||||
| exec.enabled: true | ||||
| exec.token.mxid: '{matrixId}' | ||||
|  | ||||
| exec.placeholder.token.localpart: '{username}' | ||||
| exec.placeholder.command: '/path/to/executable' | ||||
| exec.placeholder.args: | ||||
|   - '-u' | ||||
|   - '{username}' | ||||
| exec.placeholder.env: | ||||
|   MATRIX_DOMAIN: '{domain}' | ||||
|   MATRIX_USER_ID: '{matrixId}' | ||||
|    | ||||
| exec.placeholder.output.type: 'json' | ||||
| exec.placeholder.exit.success: [0, 128] | ||||
| exec.placeholder.exit.failure: [1, 129] | ||||
| exec: | ||||
|   enabled: true | ||||
|   token: | ||||
|     mxid: '{matrixId}' | ||||
|   auth: | ||||
|     token: | ||||
|       localpart: '{username}' | ||||
|     command: '/path/to/executable' | ||||
|     args: | ||||
|       - '-u' | ||||
|       - '{username}' | ||||
|     env: | ||||
|       MATRIX_DOMAIN: '{domain}' | ||||
|       MATRIX_USER_ID: '{matrixId}' | ||||
|     output: | ||||
|       type: 'json' | ||||
|     exit: | ||||
|       success: | ||||
|         - 0 | ||||
|         - 128 | ||||
|       failure: | ||||
|         - 1 | ||||
|         - 129 | ||||
| ``` | ||||
| With: | ||||
| - The Identity store enabled for all features | ||||
|   | ||||
| @@ -2,12 +2,12 @@ | ||||
| https://firebase.google.com/ | ||||
|  | ||||
| ## Features | ||||
| |      Name      | Supported? | | ||||
| |----------------|------------| | ||||
| | Authentication | Yes        | | ||||
| | Directory      | No         | | ||||
| | Identity       | Yes        | | ||||
| | Profile        | No         | | ||||
| |                       Name                      | Supported | | ||||
| |-------------------------------------------------|-----------| | ||||
| | [Authentication](../features/authentication.md) | Yes       | | ||||
| | [Directory](../features/directory.md)           | No        | | ||||
| | [Identity](../features/identity.md)             | Yes       | | ||||
| | [Profile](../features/profile.md)               | No        | | ||||
|  | ||||
| ## Requirements | ||||
| This backend requires a suitable Matrix client capable of performing Firebase authentication and passing the following | ||||
|   | ||||
| @@ -8,12 +8,12 @@ | ||||
| For NetIQ, replace all the `ldap` prefix in the configuration by `netiq`. | ||||
|  | ||||
| ## Features | ||||
| |      Name      | Supported? | | ||||
| |----------------|------------| | ||||
| | Authentication | Yes        | | ||||
| | Directory      | Yes        | | ||||
| | Identity       | Yes        | | ||||
| | Profile        | Yes        | | ||||
| |                       Name                      | Supported | | ||||
| |-------------------------------------------------|-----------| | ||||
| | [Authentication](../features/authentication.md) | Yes       | | ||||
| | [Directory](../features/directory.md)           | Yes       | | ||||
| | [Identity](../features/identity.md)             | Yes       | | ||||
| | [Profile](../features/profile.md)               | Yes       | | ||||
|  | ||||
| ## Getting started | ||||
| ### Base | ||||
| @@ -89,7 +89,7 @@ ldap: | ||||
| #### 3PIDs | ||||
| 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: | ||||
| ```yaml | ||||
| ldap: | ||||
| @@ -113,16 +113,18 @@ configuration item is needed to get started. | ||||
| - `ldap.identity.medium`: Namespace to overwrite generated queries from the list of attributes for each 3PID medium. | ||||
|  | ||||
| ### Authentication | ||||
| No further configuration is needed to use the Authentication feature with LDAP once globally enabled and configured. | ||||
| After you have configured and enabled the [feature itself](../features/authentication.md), no further configuration is | ||||
| needed with this identity store to make it work. | ||||
|  | ||||
| Profile auto-fill is enabled by default. It will use the `ldap.attribute.name` and `ldap.attribute.threepid` configuration | ||||
| options to get a lit of attributes to be used to build the user profile to pass on to synapse during authentication. | ||||
|  | ||||
| #### Configuration | ||||
| - `ldap.auth.filter`: Specific user filter applied during identity search. Global filter is used if blank/not set. | ||||
| - `ldap.auth.filter`: Specific user filter applied during username search. Global filter is used if blank/not set. | ||||
|  | ||||
| ### Directory | ||||
| No further configuration is needed to use the Directory feature with LDAP once globally enabled and configured. | ||||
| After you have configured and enabled the [feature itself](../features/directory.md), no further configuration is | ||||
| needed with this identity store to make it work. | ||||
|  | ||||
| #### Configuration | ||||
| To set a specific filter applied during directory search, use `ldap.directory.filter` | ||||
|   | ||||
| @@ -6,12 +6,12 @@ | ||||
| - SQLite | ||||
|  | ||||
| ## Features | ||||
| |      Name      | Supported? | | ||||
| |----------------|------------| | ||||
| | Authentication | No         | | ||||
| | Directory      | Yes        | | ||||
| | Identity       | Yes        | | ||||
| | Profile        | Yes        | | ||||
| |                       Name                      | Supported | | ||||
| |-------------------------------------------------|-----------| | ||||
| | [Authentication](../features/authentication.md) | No        | | ||||
| | [Directory](../features/directory.md)           | Yes       | | ||||
| | [Identity](../features/identity.md)             | Yes       | | ||||
| | [Profile](../features/profile.md)               | Yes       | | ||||
|  | ||||
| Due to the implementation complexity of supporting arbitrary hashing/encoding mechanisms or auth flow, Authentication | ||||
| will be out of scope of SQL Identity stores and should be done via one of the other identity stores, typically | ||||
| @@ -102,9 +102,41 @@ sql: | ||||
| ``` | ||||
|  | ||||
| ### 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 | ||||
| sql: | ||||
|   identity: | ||||
|     enabled: <boolean> | ||||
|     type: <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,15 +1,17 @@ | ||||
| # Synapse Identity Store | ||||
| Synapse's Database itself can be used as an Identity store. | ||||
| Synapse's Database itself can be used as an Identity store. This identity store is a regular SQL store with | ||||
| built-in default queries that matches Synapse DB. | ||||
|  | ||||
| ## Features | ||||
| |      Name      | Supported? | | ||||
| |----------------|------------| | ||||
| | Authentication | No         | | ||||
| | Directory      | Yes        | | ||||
| | Identity       | Yes        | | ||||
| | Profile        | Yes        | | ||||
| |                       Name                      | Supported | | ||||
| |-------------------------------------------------|-----------| | ||||
| | [Authentication](../features/authentication.md) | No        | | ||||
| | [Directory](../features/directory.md)           | Yes       | | ||||
| | [Identity](../features/identity.md)             | Yes       | | ||||
| | [Profile](../features/profile.md)               | Yes       | | ||||
|  | ||||
| Authentication is done by Synapse itself. | ||||
| - Authentication is done by Synapse itself. | ||||
| - Roles are mapped to communities. The Role name/ID uses the community ID in the form `+id:domain.tld` | ||||
|  | ||||
| ## Configuration | ||||
| ### Basic | ||||
|   | ||||
| @@ -5,12 +5,12 @@ Two types of connections are required for full support: | ||||
| - Direct SQL access | ||||
|  | ||||
| ## Features | ||||
| |      Name      | Supported? | | ||||
| |----------------|------------| | ||||
| | Authentication | Yes        | | ||||
| | Directory      | Yes        | | ||||
| | Identity       | Yes        | | ||||
| | Profile        | No         | | ||||
| |                       Name                      | Supported | | ||||
| |-------------------------------------------------|-----------| | ||||
| | [Authentication](../features/authentication.md) | Yes       | | ||||
| | [Directory](../features/directory.md)           | Yes       | | ||||
| | [Identity](../features/identity.md)             | Yes       | | ||||
| | [Profile](../features/profile.md)               | No        | | ||||
|  | ||||
| ## Requirements | ||||
| - [Wordpress](https://wordpress.org/download/) >= 4.4 | ||||
|   | ||||
| @@ -1,6 +1,4 @@ | ||||
| # Email notifications - SMTP connector | ||||
| Enabled by default. | ||||
|  | ||||
| Connector ID: `smtp` | ||||
|  | ||||
| ## Configuration | ||||
| @@ -14,8 +12,8 @@ threepid: | ||||
|       connectors: | ||||
|         smtp: | ||||
|           host: 'smtpHostname' | ||||
|           port: 587 | ||||
|           tls: 1 # 0 = no STARTLS, 1 = try, 2 = force | ||||
|           tls: 1 # 0 = no STARTLS, 1 = try, 2 = force, 3 = TLS/SSL | ||||
|           port: 587 # Set appropriate value depending on your TLS setting | ||||
|           login: 'smtpLogin' | ||||
|           password: 'smtpPassword' | ||||
| ``` | ||||
|   | ||||
| @@ -1,6 +1,4 @@ | ||||
| # SMS notifications - Twilio connector | ||||
| Enabled by default. | ||||
|  | ||||
| Connector ID: `twilio` | ||||
|  | ||||
| ## Configuration | ||||
| @@ -10,7 +8,7 @@ threepid: | ||||
|     msisdn: | ||||
|       connectors: | ||||
|         twilio: | ||||
|           accountSid: 'myAccountSid' | ||||
|           authToken: 'myAuthToken' | ||||
|           account_sid: 'myAccountSid' | ||||
|           auth_token: 'myAuthToken' | ||||
|           number: '+123456789' | ||||
| ``` | ||||
|   | ||||
| @@ -26,16 +26,10 @@ notification: | ||||
|             html: <Path to file containing the HTML part of the email. Do not set to not use one> | ||||
|         session: | ||||
|           validation: | ||||
|             local: | ||||
|               subject: <Subject of the email notification sent for local 3PID sessions> | ||||
|               body: | ||||
|                 text: <Path to file containing the raw text part of the email. Do not set to not use one> | ||||
|                 html: <Path to file containing the HTML part of the email. Do not set to not use one> | ||||
|             remote: | ||||
|               subject: <Subject of the email notification sent for remote 3PID sessions> | ||||
|               body: | ||||
|                 text: <Path to file containing the raw text part of the email. Do not set to not use one> | ||||
|                 html: <Path to file containing the HTML part of the email. Do not set to not use one> | ||||
|             subject: <Subject of the email notification sent for 3PID sessions> | ||||
|             body: | ||||
|               text: <Path to file containing the raw text part of the email. Do not set to not use one> | ||||
|               html: <Path to file containing the HTML part of the email. Do not set to not use one> | ||||
|           unbind: | ||||
|             fraudulent: | ||||
|               subject: <Subject of the email notification sent for potentially fraudulent 3PID unbinds> | ||||
|   | ||||
| @@ -1,77 +1,109 @@ | ||||
| # Notifications: Generate from templates | ||||
| To create notification content, you can use the `template` generator if supported for the 3PID medium which will read | ||||
| content from configured files. | ||||
| # Notifications: Template generator | ||||
| Most of the Identity actions will trigger a notification of some kind, informing the user of some confirmation, next step | ||||
| 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 | ||||
| the 3PID that was requested, the domain of your Identity server, etc. | ||||
| Those notifications are by default generated from templates and by replacing placeholder tokens in them with the relevant | ||||
| 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 | ||||
| placeholders and also have their own individual set of placeholders. | ||||
| Templates for the following events/actions are available: | ||||
| - [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 | ||||
| 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: | ||||
| ```yaml | ||||
| threepid: | ||||
|   medium: | ||||
|     <YOUR 3PID MEDIUM HERE>: | ||||
|     email: | ||||
|       generators: | ||||
|         template: | ||||
|           invite: '/path/to/invite-template.eml' | ||||
|           session: | ||||
|             validation: | ||||
|               local: '/path/to/validate-local-template.eml' | ||||
|               remote: '/path/to/validate-remote-template.eml' | ||||
|             validation: '/path/to/validate-template.eml' | ||||
|             unbind: | ||||
|               frandulent: '/path/to/unbind-fraudulent-template.eml' | ||||
|               fraudulent: '/path/to/unbind-fraudulent-template.eml' | ||||
|           generic: | ||||
|             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. | ||||
|  | ||||
| ##  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 | | ||||
|  | ||||
| ### Local 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 local session, in case the user cannot use the link | | ||||
|  | ||||
| ### Remote validation of 3PID Session | ||||
| This template is used when to user which added their 3PID address to their profile/settings and the session policy only | ||||
| allows remote sessions. | ||||
|  | ||||
| **NOTE:** 3PID session always require local validation of a token, even if a remote session is enforced.   | ||||
| One cannot bind a Matrix ID to the session until both local and remote sessions have been validated. | ||||
|  | ||||
| #### Placeholders | ||||
| | Placeholder          | Purpose                                                | | ||||
| |----------------------|--------------------------------------------------------| | ||||
| | `%VALIDATION_TOKEN%` | The token needed to validate the session               | | ||||
| | `%NEXT_URL%`         | URL to continue with remote validation of the session. | | ||||
| In this configuration, a custom template is used for each event and a static value for the `REGISTER_URL` is set. | ||||
|   | ||||
							
								
								
									
										58
									
								
								docs/troubleshooting.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								docs/troubleshooting.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| # Troubleshooting | ||||
| - [Purpose](#purpose) | ||||
| - [Logs](#logs) | ||||
|   - [Locations](#locations) | ||||
|   - [Reading Them](#reading-them) | ||||
|   - [Common issues](#common-issues) | ||||
| - [Submit an issue](#submit-an-issue) | ||||
|  | ||||
| ## Purpose | ||||
| This document describes basic troubleshooting steps for mxisd. | ||||
|  | ||||
| ## Logs | ||||
| ### Locations | ||||
| mxisd logs to `STDOUT` (Standard Output) and `STDERR` (Standard Error) only, which gets redirected | ||||
| to log file(s) depending on your system. | ||||
|  | ||||
| If you use the [Debian package](install/debian.md), this goes to `syslog`.   | ||||
| If you use the [Docker image](install/docker.md), this goes to the container logs.   | ||||
|  | ||||
| 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 | ||||
| Before reporting an issue, it is important to produce clean and complete logs so they can be understood. | ||||
|  | ||||
| It is usually useless to try to troubleshoot an issue based on a single log line. Any action or API request | ||||
| in mxisd would trigger more than one log lines, and those would be considered necessary context to | ||||
| understand what happened. | ||||
|  | ||||
| You may also find things called *stacktraces*. Those are important to pin-point bugs and the likes and should | ||||
| always be included in any report. They also tend to be very specific about the issue at hand. | ||||
|  | ||||
| Example of a stacktrace: | ||||
| ``` | ||||
| Exception in thread "main" java.lang.NullPointerException | ||||
|         at com.example.myproject.Book.getTitle(Book.java:16) | ||||
|         at com.example.myproject.Author.getBookTitles(Author.java:25) | ||||
|         at com.example.myproject.Bootstrap.main(Bootstrap.java:14) | ||||
| ``` | ||||
|  | ||||
| ### Common issues | ||||
| #### Internal Server Error | ||||
| `Contact your administrator with reference Transaction #123456789` | ||||
|  | ||||
| This is a generic message produced in case of an unknown error. The transaction reference allows to easily find | ||||
| the location in the logs to look for an error. | ||||
|  | ||||
| **IMPORTANT:** That line alone does not tell you anything about the error. You'll need the log lines before and after, | ||||
| usually including a stacktrace, to know what happened. Please take the time to read the surround output to get | ||||
| context about the issue at hand. | ||||
|  | ||||
| ## Submit an issue | ||||
| In case the logs do not allow you to understand the issue at hand, please submit clean and complete logs | ||||
| as explained [here](#reading-them) in a new issue on the repository, or [get in touch](../README.md#contact). | ||||
| @@ -1,6 +1,11 @@ | ||||
| # Sample configuration file explaining the minimum required keys to be set to run mxisd | ||||
| # | ||||
| # For a complete list of options, see https://github.com/kamax-matrix/mxisd/docs/README.md | ||||
| # | ||||
| # Please follow the Getting Started guide if this is your first time using/configuring mxisd | ||||
| # | ||||
| #  -- https://github.com/kamax-matrix/mxisd/blob/master/docs/getting-started.md#getting-started | ||||
| # | ||||
|  | ||||
| ####################### | ||||
| # Matrix config items # | ||||
| @@ -9,6 +14,11 @@ | ||||
| # NOTE: in Synapse Homeserver, the Matrix domain is defined as 'server_name' in configuration file. | ||||
| # | ||||
| # This is used to build the various identifiers in all the features. | ||||
| # | ||||
| # If the hostname of the public URL used to reach your Matrix services is different from your Matrix domain, | ||||
| # per example matrix.domain.tld vs domain.tld, then use the server.name configuration option. | ||||
| # See the "Configure" section of the Getting Started guide for more info. | ||||
| # | ||||
| matrix: | ||||
|   domain: '' | ||||
|  | ||||
| @@ -16,26 +26,27 @@ matrix: | ||||
| ################ | ||||
| # Signing keys # | ||||
| ################ | ||||
| # Absolute path for the Identity Server signing key. | ||||
| # This is **NOT** your homeserver key. | ||||
| # The signing key is auto-generated during execution time if not present. | ||||
| # Absolute path for the Identity Server signing keys database. | ||||
| # /!\ THIS MUST **NOT** BE YOUR HOMESERVER KEYS FILE /!\ | ||||
| # If this path does not exist, it will be auto-generated. | ||||
| # | ||||
| # During testing, /var/tmp/mxisd.key is a possible value | ||||
| # During testing, /var/tmp/mxisd/keys is a possible value | ||||
| # For production, recommended location shall be one of the following: | ||||
| #   - /var/opt/mxisd/sign.key | ||||
| #   - /var/local/mxisd/sign.key | ||||
| #   - /var/lib/mxisd/sign.key | ||||
| #   - /var/lib/mxisd/keys | ||||
| #   - /var/opt/mxisd/keys | ||||
| #   - /var/local/mxisd/keys | ||||
| # | ||||
| key: | ||||
|   path: '' | ||||
|  | ||||
|  | ||||
| # Path to the SQLite DB file for mxisd internal storage | ||||
| # /!\ THIS MUST **NOT** BE YOUR HOMESERVER DATABASE /!\ | ||||
| # | ||||
| # Examples: | ||||
| #  - /var/opt/mxisd/mxisd.db | ||||
| #  - /var/local/mxisd/mxisd.db | ||||
| #  - /var/lib/mxisd/mxisd.db | ||||
| #  - /var/opt/mxisd/store.db | ||||
| #  - /var/local/mxisd/store.db | ||||
| #  - /var/lib/mxisd/store.db | ||||
| # | ||||
| storage: | ||||
|   provider: | ||||
| @@ -43,48 +54,31 @@ storage: | ||||
|       database: '/path/to/mxisd.db' | ||||
|  | ||||
|  | ||||
| #################### | ||||
| # Fallback servers # | ||||
| #################### | ||||
| ################### | ||||
| # Identity Stores # | ||||
| ################### | ||||
| # If you are using synapse standalone and do not have an Identity store, | ||||
| # see https://github.com/kamax-matrix/mxisd/blob/master/docs/stores/synapse.md#synapse-identity-store | ||||
| # | ||||
| # Root/Central servers to be used as final fallback when performing lookups. | ||||
| # By default, for privacy reasons, matrix.org servers are not enabled. | ||||
| # See the following issue: https://github.com/kamax-matrix/mxisd/issues/76 | ||||
| # | ||||
| # If you would like to use them and trade away your privacy for convenience, uncomment the following option: | ||||
| # | ||||
| #forward: | ||||
| #  servers: ['matrix-org'] | ||||
|  | ||||
|  | ||||
| ################ | ||||
| # LDAP Backend # | ||||
| ################ | ||||
| # If you would like to integrate with your AD/Samba/LDAP server, | ||||
| # see https://github.com/kamax-matrix/mxisd/blob/master/docs/stores/ldap.md | ||||
|  | ||||
|  | ||||
| ############### | ||||
| # SQL Backend # | ||||
| ############### | ||||
| # If you would like to integrate with a MySQL/MariaDB/PostgreQL/SQLite DB, | ||||
| # see https://github.com/kamax-matrix/mxisd/blob/master/docs/stores/sql.md | ||||
|  | ||||
|  | ||||
| ################ | ||||
| # REST Backend # | ||||
| ################ | ||||
| # If you would like to integrate with an existing web service/webapp, | ||||
| # see https://github.com/kamax-matrix/mxisd/blob/master/docs/stores/rest.md | ||||
| # | ||||
| # For any other Identity store, or to simply discover them, | ||||
| # see https://github.com/kamax-matrix/mxisd/blob/master/docs/stores/README.md | ||||
|  | ||||
|  | ||||
| ################################################# | ||||
| # Notifications for invites/addition to profile # | ||||
| ################################################# | ||||
| # If you would like to change the content, | ||||
| # This is mandatory to deal with anything e-mail related. | ||||
| # | ||||
| # For an introduction to sessions, invites and 3PIDs in general, | ||||
| # see https://github.com/kamax-matrix/mxisd/blob/master/docs/threepids/session/session.md#3pid-sessions | ||||
| # | ||||
| # If you would like to change the content of the notifications, | ||||
| # see https://github.com/kamax-matrix/mxisd/blob/master/docs/threepids/notification/template-generator.md | ||||
| # | ||||
| #### E-mail invite sender | ||||
| #### E-mail connector | ||||
| threepid: | ||||
|   medium: | ||||
|     email: | ||||
| @@ -97,18 +91,19 @@ threepid: | ||||
|           # SMTP host | ||||
|           host: "smtp.example.org" | ||||
|  | ||||
|           # SMTP port | ||||
|           port: 587 | ||||
|  | ||||
|           # TLS mode for the connection. | ||||
|           # | ||||
|           # TLS mode for the connection | ||||
|           # Possible values: | ||||
|           #  0    Disable TLS entirely | ||||
|           #  1    Enable TLS if supported by server (default) | ||||
|           #  2    Force TLS and fail if not available | ||||
|           #  0    Disable any kind of TLS entirely | ||||
|           #  1    Enable STARTLS if supported by server (default) | ||||
|           #  2    Force STARTLS and fail if not available | ||||
|           #  3    Use full TLS/SSL instead of STARTLS | ||||
|           # | ||||
|           tls: 1 | ||||
|  | ||||
|           # SMTP port | ||||
|           # Be sure to adapt depending on your TLS choice, if changed from default | ||||
|           port: 587 | ||||
|  | ||||
|           # Login for SMTP | ||||
|           login: "matrix-identity@example.org" | ||||
|  | ||||
|   | ||||
| @@ -3,5 +3,7 @@ Maintainer: Kamax.io <foss@kamax.io> | ||||
| Homepage: https://github.com/kamax-matrix/mxisd | ||||
| Description: Federated Matrix Identity Server | ||||
| Architecture: all | ||||
| Section: net | ||||
| Priority: optional | ||||
| Depends: openjdk-8-jre | openjdk-8-jre-headless | openjdk-8-jdk | openjdk-8-jdk-headless | ||||
| Version: 0 | ||||
|   | ||||
| @@ -1,27 +1,26 @@ | ||||
| /* | ||||
| * The MIT License | ||||
| * | ||||
| * Copyright (c) 2013 Edin Dazdarevic (edin.dazdarevic@gmail.com) | ||||
|  * The MIT License | ||||
|  * | ||||
|  * Copyright (c) 2013 Edin Dazdarevic (edin.dazdarevic@gmail.com) | ||||
|  | ||||
| * Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| * of this software and associated documentation files (the "Software"), to deal | ||||
| * in the Software without restriction, including without limitation the rights | ||||
| * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
| * copies of the Software, and to permit persons to whom the Software is | ||||
| * furnished to do so, subject to the following conditions: | ||||
|  * Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
|  * of this software and associated documentation files (the "Software"), to deal | ||||
|  * in the Software without restriction, including without limitation the rights | ||||
|  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
|  * copies of the Software, and to permit persons to whom the Software is | ||||
|  * furnished to do so, subject to the following conditions: | ||||
|  | ||||
| * The above copyright notice and this permission notice shall be included in | ||||
| * all copies or substantial portions of the Software. | ||||
|  * The above copyright notice and this permission notice shall be included in | ||||
|  * all copies or substantial portions of the Software. | ||||
|  | ||||
| * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
| * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
| * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||||
| * THE SOFTWARE. | ||||
| * | ||||
| * */ | ||||
|  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
|  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
|  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
|  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
|  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
|  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||||
|  * THE SOFTWARE. | ||||
|  */ | ||||
|  | ||||
| package edazdarevic.commons.net; | ||||
|  | ||||
|   | ||||
| @@ -21,22 +21,30 @@ | ||||
| package io.kamax.mxisd; | ||||
|  | ||||
| import io.kamax.mxisd.config.MxisdConfig; | ||||
| import io.kamax.mxisd.http.undertow.handler.InternalInfoHandler; | ||||
| import io.kamax.mxisd.http.undertow.handler.OptionsHandler; | ||||
| import io.kamax.mxisd.http.undertow.handler.SaneHandler; | ||||
| import io.kamax.mxisd.http.undertow.handler.as.v1.AsNotFoundHandler; | ||||
| import io.kamax.mxisd.http.undertow.handler.as.v1.AsTransactionHandler; | ||||
| import io.kamax.mxisd.http.undertow.handler.as.v1.AsUserHandler; | ||||
| import io.kamax.mxisd.http.undertow.handler.auth.RestAuthHandler; | ||||
| import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginGetHandler; | ||||
| import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginHandler; | ||||
| import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginPostHandler; | ||||
| import io.kamax.mxisd.http.undertow.handler.directory.v1.UserDirectorySearchHandler; | ||||
| import io.kamax.mxisd.http.undertow.handler.identity.v1.*; | ||||
| import io.kamax.mxisd.http.undertow.handler.invite.v1.RoomInviteHandler; | ||||
| import io.kamax.mxisd.http.undertow.handler.profile.v1.InternalProfileHandler; | ||||
| import io.kamax.mxisd.http.undertow.handler.profile.v1.ProfileHandler; | ||||
| import io.kamax.mxisd.http.undertow.handler.register.v1.Register3pidRequestTokenHandler; | ||||
| import io.kamax.mxisd.http.undertow.handler.status.StatusHandler; | ||||
| import io.kamax.mxisd.http.undertow.handler.status.VersionHandler; | ||||
| import io.undertow.Handlers; | ||||
| import io.undertow.Undertow; | ||||
| import io.undertow.server.HttpHandler; | ||||
|  | ||||
| import java.util.Objects; | ||||
|  | ||||
| public class HttpMxisd { | ||||
|  | ||||
|     // Core | ||||
| @@ -45,6 +53,12 @@ public class HttpMxisd { | ||||
|     // I/O | ||||
|     private Undertow httpSrv; | ||||
|  | ||||
|     static { | ||||
|         // Used in XNIO package, dependency of Undertow | ||||
|         // We switch to slf4j | ||||
|         System.setProperty("org.jboss.logging.provider", "slf4j"); | ||||
|     } | ||||
|  | ||||
|     public HttpMxisd(MxisdConfig cfg) { | ||||
|         m = new Mxisd(cfg); | ||||
|     } | ||||
| @@ -52,15 +66,21 @@ public class HttpMxisd { | ||||
|     public void start() { | ||||
|         m.start(); | ||||
|  | ||||
|         HttpHandler asNotFoundHandler = SaneHandler.around(new AsNotFoundHandler(m.getAs())); | ||||
|         HttpHandler helloHandler = SaneHandler.around(new HelloHandler()); | ||||
|  | ||||
|         HttpHandler asUserHandler = SaneHandler.around(new AsUserHandler(m.getAs())); | ||||
|         HttpHandler asTxnHandler = SaneHandler.around(new AsTransactionHandler(m.getAs())); | ||||
|         HttpHandler storeInvHandler = SaneHandler.around(new StoreInviteHandler(m.getConfig().getServer(), m.getInvitationManager(), m.getKeyManager())); | ||||
|         HttpHandler sessValidateHandler = SaneHandler.around(new SessionValidateHandler(m.getSession(), m.getConfig().getServer(), m.getConfig().getView())); | ||||
|         HttpHandler asNotFoundHandler = SaneHandler.around(new AsNotFoundHandler(m.getAs())); | ||||
|  | ||||
|         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() | ||||
|  | ||||
|                 .add("OPTIONS", "/**", SaneHandler.around(new OptionsHandler())) | ||||
|  | ||||
|                 // Status endpoints | ||||
|                 .get(StatusHandler.Path, SaneHandler.around(new StatusHandler())) | ||||
|                 .get(VersionHandler.Path, SaneHandler.around(new VersionHandler())) | ||||
|  | ||||
|                 // Authentication endpoints | ||||
|                 .get(LoginHandler.Path, SaneHandler.around(new LoginGetHandler(m.getAuth(), m.getHttpClient()))) | ||||
| @@ -73,39 +93,52 @@ public class HttpMxisd { | ||||
|                 // Key endpoints | ||||
|                 .get(KeyGetHandler.Path, SaneHandler.around(new KeyGetHandler(m.getKeyManager()))) | ||||
|                 .get(RegularKeyIsValidHandler.Path, SaneHandler.around(new RegularKeyIsValidHandler(m.getKeyManager()))) | ||||
|                 .get(EphemeralKeyIsValidHandler.Path, SaneHandler.around(new EphemeralKeyIsValidHandler())) | ||||
|                 .get(EphemeralKeyIsValidHandler.Path, SaneHandler.around(new EphemeralKeyIsValidHandler(m.getKeyManager()))) | ||||
|  | ||||
|                 // Identity endpoints | ||||
|                 .get(HelloHandler.Path, SaneHandler.around(new HelloHandler())) | ||||
|                 .get(SingleLookupHandler.Path, SaneHandler.around(new SingleLookupHandler(m.getIdentity(), m.getSign()))) | ||||
|                 .get(HelloHandler.Path, helloHandler) | ||||
|                 .get(HelloHandler.Path + "/", helloHandler) // Be lax with possibly trailing slash | ||||
|                 .get(SingleLookupHandler.Path, SaneHandler.around(new SingleLookupHandler(m.getConfig(), m.getIdentity(), m.getSign()))) | ||||
|                 .post(BulkLookupHandler.Path, SaneHandler.around(new BulkLookupHandler(m.getIdentity()))) | ||||
|                 .post(StoreInviteHandler.Path, storeInvHandler) | ||||
|                 .post(SessionStartHandler.Path, SaneHandler.around(new SessionStartHandler(m.getSession()))) | ||||
|                 .get(SessionValidateHandler.Path, sessValidateHandler) | ||||
|                 .post(SessionValidateHandler.Path, sessValidateHandler) | ||||
|                 .get(SessionValidateHandler.Path, SaneHandler.around(new SessionValidationGetHandler(m.getSession(), m.getConfig()))) | ||||
|                 .post(SessionValidateHandler.Path, SaneHandler.around(new SessionValidationPostHandler(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(SignEd25519Handler.Path, SaneHandler.around(new SignEd25519Handler(m.getConfig(), m.getInvite(), m.getSign()))) | ||||
|  | ||||
|                 // Profile endpoints | ||||
|                 .get(ProfileHandler.Path, SaneHandler.around(new ProfileHandler(m.getProfile()))) | ||||
|                 .get(InternalProfileHandler.Path, SaneHandler.around(new InternalProfileHandler(m.getProfile()))) | ||||
|  | ||||
|                 // Registration endpoints | ||||
|                 .post(Register3pidRequestTokenHandler.Path, SaneHandler.around(new Register3pidRequestTokenHandler(m.getReg(), m.getClientDns(), m.getHttpClient()))) | ||||
|  | ||||
|                 // Invite endpoints | ||||
|                 .post(RoomInviteHandler.Path, SaneHandler.around(new RoomInviteHandler(m.getHttpClient(), m.getClientDns(), m.getInvite()))) | ||||
|  | ||||
|                 // Application Service endpoints | ||||
|                 .get("/_matrix/app/v1/users/**", asNotFoundHandler) | ||||
|                 .get("/users/**", asNotFoundHandler) // Legacy endpoint | ||||
|                 .get(AsUserHandler.Path, asUserHandler) | ||||
|                 .get("/_matrix/app/v1/rooms/**", asNotFoundHandler) | ||||
|                 .get("/rooms/**", asNotFoundHandler) // Legacy endpoint | ||||
|                 .put(AsTransactionHandler.Path, asTxnHandler) | ||||
|  | ||||
|                 .get("/users/{" + AsUserHandler.ID + "}", asUserHandler) // Legacy endpoint | ||||
|                 .get("/rooms/**", asNotFoundHandler) // Legacy endpoint | ||||
|                 .put("/transactions/{" + AsTransactionHandler.ID + "}", asTxnHandler) // Legacy endpoint | ||||
|  | ||||
|                 // Banned endpoints | ||||
|                 .get(InternalInfoHandler.Path, SaneHandler.around(new InternalInfoHandler())) | ||||
|  | ||||
|         ).build(); | ||||
|  | ||||
|         httpSrv.start(); | ||||
|     } | ||||
|  | ||||
|     public void stop() { | ||||
|         httpSrv.stop(); | ||||
|         // Because it might have never been initialized if an exception is thrown early | ||||
|         if (Objects.nonNull(httpSrv)) httpSrv.stop(); | ||||
|  | ||||
|         m.stop(); | ||||
|     } | ||||
|   | ||||
| @@ -20,8 +20,6 @@ | ||||
|  | ||||
| package io.kamax.mxisd; | ||||
|  | ||||
| import io.kamax.matrix.crypto.KeyManager; | ||||
| import io.kamax.matrix.crypto.SignatureManager; | ||||
| import io.kamax.mxisd.as.AppSvcManager; | ||||
| import io.kamax.mxisd.auth.AuthManager; | ||||
| import io.kamax.mxisd.auth.AuthProviders; | ||||
| @@ -29,6 +27,9 @@ import io.kamax.mxisd.backend.IdentityStoreSupplier; | ||||
| import io.kamax.mxisd.backend.sql.synapse.Synapse; | ||||
| import io.kamax.mxisd.config.MxisdConfig; | ||||
| import io.kamax.mxisd.crypto.CryptoFactory; | ||||
| import io.kamax.mxisd.crypto.KeyManager; | ||||
| import io.kamax.mxisd.crypto.SignatureManager; | ||||
| import io.kamax.mxisd.crypto.ed25519.Ed25519KeyManager; | ||||
| import io.kamax.mxisd.directory.DirectoryManager; | ||||
| import io.kamax.mxisd.directory.DirectoryProviders; | ||||
| import io.kamax.mxisd.dns.ClientDnsOverwrite; | ||||
| @@ -40,14 +41,18 @@ import io.kamax.mxisd.lookup.provider.BridgeFetcher; | ||||
| import io.kamax.mxisd.lookup.provider.RemoteIdentityServerFetcher; | ||||
| import io.kamax.mxisd.lookup.strategy.LookupStrategy; | ||||
| import io.kamax.mxisd.lookup.strategy.RecursivePriorityLookupStrategy; | ||||
| import io.kamax.mxisd.matrix.HomeserverFederationResolver; | ||||
| import io.kamax.mxisd.matrix.IdentityServerUtils; | ||||
| import io.kamax.mxisd.notification.NotificationHandlerSupplier; | ||||
| import io.kamax.mxisd.notification.NotificationHandlers; | ||||
| import io.kamax.mxisd.notification.NotificationManager; | ||||
| import io.kamax.mxisd.profile.ProfileManager; | ||||
| import io.kamax.mxisd.profile.ProfileProviders; | ||||
| import io.kamax.mxisd.registration.RegistrationManager; | ||||
| import io.kamax.mxisd.session.SessionManager; | ||||
| import io.kamax.mxisd.storage.IStorage; | ||||
| import io.kamax.mxisd.storage.ormlite.OrmLiteSqlStorage; | ||||
| import org.apache.commons.lang.StringUtils; | ||||
| import org.apache.http.impl.client.CloseableHttpClient; | ||||
| import org.apache.http.impl.client.HttpClients; | ||||
|  | ||||
| @@ -55,47 +60,58 @@ import java.util.ServiceLoader; | ||||
|  | ||||
| public class Mxisd { | ||||
|  | ||||
|     protected MxisdConfig cfg; | ||||
|     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; | ||||
|  | ||||
|     protected CloseableHttpClient httpClient; | ||||
|     protected IRemoteIdentityServerFetcher srvFetcher; | ||||
|     private MxisdConfig cfg; | ||||
|  | ||||
|     protected IStorage store; | ||||
|     private CloseableHttpClient httpClient; | ||||
|     private IRemoteIdentityServerFetcher srvFetcher; | ||||
|  | ||||
|     protected KeyManager keyMgr; | ||||
|     protected SignatureManager signMgr; | ||||
|     private IStorage store; | ||||
|  | ||||
|     private Ed25519KeyManager keyMgr; | ||||
|     private SignatureManager signMgr; | ||||
|     private ClientDnsOverwrite clientDns; | ||||
|  | ||||
|     // Features | ||||
|     protected AuthManager authMgr; | ||||
|     protected DirectoryManager dirMgr; | ||||
|     protected LookupStrategy idStrategy; | ||||
|     protected InvitationManager invMgr; | ||||
|     protected ProfileManager pMgr; | ||||
|     protected AppSvcManager asHander; | ||||
|     protected SessionManager sessMgr; | ||||
|     protected NotificationManager notifMgr; | ||||
|     private AuthManager authMgr; | ||||
|     private DirectoryManager dirMgr; | ||||
|     private LookupStrategy idStrategy; | ||||
|     private InvitationManager invMgr; | ||||
|     private ProfileManager pMgr; | ||||
|     private AppSvcManager asHander; | ||||
|     private SessionManager sessMgr; | ||||
|     private NotificationManager notifMgr; | ||||
|     private RegistrationManager regMgr; | ||||
|  | ||||
|     // HS-specific classes | ||||
|     private Synapse synapse; | ||||
|  | ||||
|     public Mxisd(MxisdConfig cfg) { | ||||
|         this.cfg = cfg.build(); | ||||
|     } | ||||
|  | ||||
|     protected void build() { | ||||
|     private void build() { | ||||
|         httpClient = HttpClients.custom() | ||||
|                 .setUserAgent("mxisd") | ||||
|                 .setUserAgent(Agent) | ||||
|                 .setMaxConnPerRoute(Integer.MAX_VALUE) | ||||
|                 .setMaxConnTotal(Integer.MAX_VALUE) | ||||
|                 .build(); | ||||
|  | ||||
|         FederationDnsOverwrite fedDns = new FederationDnsOverwrite(cfg.getDns().getOverwrite()); | ||||
|         HomeserverFederationResolver resolver = new HomeserverFederationResolver(fedDns, httpClient); | ||||
|         IdentityServerUtils.setHttpClient(httpClient); | ||||
|         srvFetcher = new RemoteIdentityServerFetcher(httpClient); | ||||
|  | ||||
|         store = new OrmLiteSqlStorage(cfg); | ||||
|         keyMgr = CryptoFactory.getKeyManager(cfg.getKey()); | ||||
|         signMgr = CryptoFactory.getSignatureManager(keyMgr, cfg.getServer()); | ||||
|         ClientDnsOverwrite 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); | ||||
|         signMgr = CryptoFactory.getSignatureManager(keyMgr); | ||||
|         clientDns = new ClientDnsOverwrite(cfg.getDns().getOverwrite()); | ||||
|  | ||||
|         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(NotificationHandlerSupplier.class).iterator().forEachRemaining(p -> p.accept(this)); | ||||
|  | ||||
| @@ -103,10 +119,11 @@ public class Mxisd { | ||||
|         pMgr = new ProfileManager(ProfileProviders.get(), clientDns, httpClient); | ||||
|         notifMgr = new NotificationManager(cfg.getNotification(), NotificationHandlers.get()); | ||||
|         sessMgr = new SessionManager(cfg.getSession(), cfg.getMatrix(), store, notifMgr, idStrategy, httpClient); | ||||
|         invMgr = new InvitationManager(cfg.getInvite(), store, idStrategy, signMgr, fedDns, notifMgr); | ||||
|         invMgr = new InvitationManager(cfg, store, idStrategy, keyMgr, signMgr, resolver, notifMgr, pMgr); | ||||
|         authMgr = new AuthManager(cfg, AuthProviders.get(), idStrategy, invMgr, clientDns, httpClient); | ||||
|         dirMgr = new DirectoryManager(cfg.getDirectory(), clientDns, httpClient, DirectoryProviders.get()); | ||||
|         asHander = new AppSvcManager(cfg, store, pMgr, notifMgr, synapse); | ||||
|         regMgr = new RegistrationManager(cfg.getRegister(), httpClient, clientDns, invMgr); | ||||
|         asHander = new AppSvcManager(this); | ||||
|     } | ||||
|  | ||||
|     public MxisdConfig getConfig() { | ||||
| @@ -117,6 +134,10 @@ public class Mxisd { | ||||
|         return httpClient; | ||||
|     } | ||||
|  | ||||
|     public ClientDnsOverwrite getClientDns() { | ||||
|         return clientDns; | ||||
|     } | ||||
|  | ||||
|     public IRemoteIdentityServerFetcher getServerFetcher() { | ||||
|         return srvFetcher; | ||||
|     } | ||||
| @@ -125,7 +146,7 @@ public class Mxisd { | ||||
|         return keyMgr; | ||||
|     } | ||||
|  | ||||
|     public InvitationManager getInvitationManager() { | ||||
|     public InvitationManager getInvite() { | ||||
|         return invMgr; | ||||
|     } | ||||
|  | ||||
| @@ -153,6 +174,10 @@ public class Mxisd { | ||||
|         return signMgr; | ||||
|     } | ||||
|  | ||||
|     public RegistrationManager getReg() { | ||||
|         return regMgr; | ||||
|     } | ||||
|  | ||||
|     public AppSvcManager getAs() { | ||||
|         return asHander; | ||||
|     } | ||||
| @@ -161,6 +186,14 @@ public class Mxisd { | ||||
|         return notifMgr; | ||||
|     } | ||||
|  | ||||
|     public IStorage getStore() { | ||||
|         return store; | ||||
|     } | ||||
|  | ||||
|     public Synapse getSynapse() { | ||||
|         return synapse; | ||||
|     } | ||||
|  | ||||
|     public void start() { | ||||
|         build(); | ||||
|     } | ||||
|   | ||||
| @@ -22,44 +22,75 @@ package io.kamax.mxisd; | ||||
|  | ||||
| import io.kamax.mxisd.config.MxisdConfig; | ||||
| import io.kamax.mxisd.config.YamlConfigLoader; | ||||
| import io.kamax.mxisd.exception.ConfigurationException; | ||||
| import org.apache.commons.lang3.StringUtils; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.util.Arrays; | ||||
| import java.util.Iterator; | ||||
| import java.util.Objects; | ||||
|  | ||||
| public class MxisdStandaloneExec { | ||||
|  | ||||
|     public static void main(String[] args) throws IOException { | ||||
|         MxisdConfig cfg = null; | ||||
|     private static final Logger log = LoggerFactory.getLogger("App"); | ||||
|  | ||||
|         Iterator<String> argsIt = Arrays.asList(args).iterator(); | ||||
|         while (argsIt.hasNext()) { | ||||
|             String arg = argsIt.next(); | ||||
|             if (StringUtils.equals("-c", arg)) { | ||||
|                 String cfgFile = argsIt.next(); | ||||
|                 cfg = YamlConfigLoader.loadFromFile(cfgFile); | ||||
|                 System.out.println("Loaded configuration from " + cfgFile); | ||||
|             } else { | ||||
|                 System.out.println("Invalid argument: " + arg); | ||||
|                 System.exit(1); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (Objects.isNull(cfg)) { | ||||
|             cfg = YamlConfigLoader.tryLoadFromFile("mxisd.yaml").orElseGet(MxisdConfig::new); | ||||
|     public static void main(String[] args) { | ||||
|         String logLevel = System.getenv("MXISD_LOG_LEVEL"); | ||||
|         if (StringUtils.isNotBlank(logLevel)) { | ||||
|             System.setProperty("org.slf4j.simpleLogger.log.io.kamax.mxisd", logLevel); | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             MxisdConfig cfg = null; | ||||
|             Iterator<String> argsIt = Arrays.asList(args).iterator(); | ||||
|             while (argsIt.hasNext()) { | ||||
|                 String arg = argsIt.next(); | ||||
|                 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(); | ||||
|                     cfg = YamlConfigLoader.loadFromFile(cfgFile); | ||||
|                 } else if (StringUtils.equals("--version", arg)) { | ||||
|                     System.out.println(Mxisd.Version); | ||||
|                     System.exit(0); | ||||
|                 } else { | ||||
|                     System.err.println("Invalid argument: " + arg); | ||||
|                     System.err.println("Try '--help' for available arguments"); | ||||
|                     System.exit(1); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (Objects.isNull(cfg)) { | ||||
|                 cfg = YamlConfigLoader.tryLoadFromFile("mxisd.yaml").orElseGet(MxisdConfig::new); | ||||
|             } | ||||
|  | ||||
|             log.info("mxisd starting"); | ||||
|             log.info("Version: {}", Mxisd.Version); | ||||
|  | ||||
|             HttpMxisd mxisd = new HttpMxisd(cfg); | ||||
|             Runtime.getRuntime().addShutdownHook(new Thread(() -> { | ||||
|                 mxisd.stop(); | ||||
|                 System.out.println("------------- mxisd stopped -------------"); | ||||
|                 log.info("mxisd stopped"); | ||||
|             })); | ||||
|             mxisd.start(); | ||||
|  | ||||
|             System.out.println("------------- mxisd started -------------"); | ||||
|             log.info("mxisd started"); | ||||
|         } catch (ConfigurationException e) { | ||||
|             log.error(e.getDetailedMessage()); | ||||
|             log.error(e.getMessage()); | ||||
|             System.exit(2); | ||||
|         } catch (Throwable t) { | ||||
|             t.printStackTrace(); | ||||
|             System.exit(1); | ||||
|   | ||||
| @@ -22,76 +22,183 @@ package io.kamax.mxisd.as; | ||||
|  | ||||
| import com.google.gson.JsonObject; | ||||
| import io.kamax.matrix.MatrixID; | ||||
| import io.kamax.matrix.ThreePidMedium; | ||||
| import io.kamax.matrix._MatrixID; | ||||
| import io.kamax.matrix._ThreePid; | ||||
| import io.kamax.matrix.client.MatrixClientContext; | ||||
| import io.kamax.matrix.client.as.MatrixApplicationServiceClient; | ||||
| import io.kamax.matrix.event.EventKey; | ||||
| import io.kamax.matrix.json.GsonUtil; | ||||
| import io.kamax.mxisd.backend.sql.synapse.Synapse; | ||||
| import io.kamax.mxisd.config.MatrixConfig; | ||||
| import io.kamax.mxisd.Mxisd; | ||||
| import io.kamax.mxisd.as.processor.event.EventTypeProcessor; | ||||
| import io.kamax.mxisd.as.processor.event.MembershipEventProcessor; | ||||
| import io.kamax.mxisd.as.processor.event.MessageEventProcessor; | ||||
| import io.kamax.mxisd.as.registration.SynapseRegistrationYaml; | ||||
| import io.kamax.mxisd.config.AppServiceConfig; | ||||
| import io.kamax.mxisd.config.MxisdConfig; | ||||
| import io.kamax.mxisd.exception.ConfigurationException; | ||||
| import io.kamax.mxisd.exception.HttpMatrixException; | ||||
| import io.kamax.mxisd.exception.NotAllowedException; | ||||
| import io.kamax.mxisd.notification.NotificationManager; | ||||
| import io.kamax.mxisd.profile.ProfileManager; | ||||
| import io.kamax.mxisd.storage.IStorage; | ||||
| import io.kamax.mxisd.storage.ormlite.dao.ASTransactionDao; | ||||
| import io.kamax.mxisd.util.GsonParser; | ||||
| import org.apache.commons.lang.StringUtils; | ||||
| import org.apache.commons.io.IOUtils; | ||||
| import org.apache.commons.lang3.ObjectUtils; | ||||
| import org.apache.commons.lang3.StringUtils; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| import org.yaml.snakeyaml.Yaml; | ||||
| import org.yaml.snakeyaml.introspector.BeanAccess; | ||||
| import org.yaml.snakeyaml.representer.Representer; | ||||
|  | ||||
| import java.io.FileOutputStream; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.nio.charset.StandardCharsets; | ||||
| import java.time.Instant; | ||||
| import java.util.*; | ||||
| import java.util.concurrent.CompletableFuture; | ||||
| import java.util.concurrent.ConcurrentHashMap; | ||||
| import java.util.stream.Collectors; | ||||
|  | ||||
| public class AppSvcManager { | ||||
|  | ||||
|     private transient final Logger log = LoggerFactory.getLogger(AppSvcManager.class); | ||||
|     private static final Logger log = LoggerFactory.getLogger(AppSvcManager.class); | ||||
|  | ||||
|     private final GsonParser parser; | ||||
|     private final AppServiceConfig cfg; | ||||
|     private final IStorage store; | ||||
|     private final GsonParser parser = new GsonParser(); | ||||
|  | ||||
|     private MatrixConfig cfg; | ||||
|     private IStorage store; | ||||
|     private ProfileManager profiler; | ||||
|     private NotificationManager notif; | ||||
|     private Synapse synapse; | ||||
|     private MatrixApplicationServiceClient client; | ||||
|     private Map<String, EventTypeProcessor> processors = new HashMap<>(); | ||||
|     private Map<String, CompletableFuture<String>> transactionsInProgress = new ConcurrentHashMap<>(); | ||||
|  | ||||
|     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(); | ||||
|         this.store = store; | ||||
|         this.profiler = profiler; | ||||
|         this.notif = notif; | ||||
|         this.synapse = synapse; | ||||
|         /* | ||||
|         We process the configuration to make sure all is fine and setting default values if needed | ||||
|          */ | ||||
|  | ||||
|         parser = new GsonParser(); | ||||
|         transactionsInProgress = new ConcurrentHashMap<>(); | ||||
|         // By default, the feature is enabled | ||||
|         cfg.setEnabled(ObjectUtils.defaultIfNull(cfg.isEnabled(), false)); | ||||
|  | ||||
|         if (!cfg.isEnabled()) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (Objects.isNull(cfg.getEndpoint().getToAS().getUrl())) { | ||||
|             throw new ConfigurationException("App Service: Endpoint: To AS: URL"); | ||||
|         } | ||||
|  | ||||
|         if (Objects.isNull(cfg.getEndpoint().getToAS().getToken())) { | ||||
|             throw new ConfigurationException("App Service: Endpoint: To AS: Token", "Must be set, even if to an empty string"); | ||||
|         } | ||||
|  | ||||
|         if (Objects.isNull(cfg.getEndpoint().getToHS().getUrl())) { | ||||
|             throw new ConfigurationException("App Service: Endpoint: To HS: URL"); | ||||
|         } | ||||
|  | ||||
|         if (Objects.isNull(cfg.getEndpoint().getToHS().getToken())) { | ||||
|             throw new ConfigurationException("App Service: Endpoint: To HS: Token", "Must be set, even if to an empty string"); | ||||
|         } | ||||
|  | ||||
|         // We set a default status for each feature individually | ||||
|         cfg.getFeature().getAdmin().setEnabled(ObjectUtils.defaultIfNull(cfg.getFeature().getAdmin().getEnabled(), cfg.isEnabled())); | ||||
|         cfg.getFeature().setCleanExpiredInvite(ObjectUtils.defaultIfNull(cfg.getFeature().getCleanExpiredInvite(), cfg.isEnabled())); | ||||
|         cfg.getFeature().setInviteById(ObjectUtils.defaultIfNull(cfg.getFeature().getInviteById(), false)); | ||||
|  | ||||
|         if (cfg.getFeature().getAdmin().getEnabled()) { | ||||
|             if (StringUtils.isBlank(cfg.getUser().getMain())) { | ||||
|                 throw new ConfigurationException("App Service admin feature is enabled, but no main user configured"); | ||||
|             } | ||||
|  | ||||
|             if (cfg.getUser().getMain().startsWith("@") || cfg.getUser().getMain().contains(":")) { | ||||
|                 throw new ConfigurationException("App Service: Users: Main ID: Is not a localpart"); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (cfg.getFeature().getCleanExpiredInvite()) { | ||||
|             if (StringUtils.isBlank(cfg.getUser().getInviteExpired())) { | ||||
|                 throw new ConfigurationException("App Service user for Expired Invite is not set"); | ||||
|             } | ||||
|  | ||||
|             if (cfg.getUser().getMain().startsWith("@") || cfg.getUser().getMain().contains(":")) { | ||||
|                 throw new ConfigurationException("App Service: Users: Expired Invite ID: Is not a localpart"); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         MatrixClientContext mxContext = new MatrixClientContext(); | ||||
|         mxContext.setDomain(m.getConfig().getMatrix().getDomain()); | ||||
|         mxContext.setToken(cfg.getEndpoint().getToHS().getToken()); | ||||
|         mxContext.setHsBaseUrl(cfg.getEndpoint().getToHS().getUrl()); | ||||
|         client = new MatrixApplicationServiceClient(mxContext); | ||||
|  | ||||
|         processors.put("m.room.member", new MembershipEventProcessor(client, m)); | ||||
|         processors.put("m.room.message", new MessageEventProcessor(m, client)); | ||||
|  | ||||
|         processSynapseConfig(m.getConfig()); | ||||
|     } | ||||
|  | ||||
|     private void processSynapseConfig(MxisdConfig cfg) { | ||||
|         String synapseRegFile = cfg.getAppsvc().getRegistration().getSynapse().getFile(); | ||||
|  | ||||
|         if (StringUtils.isBlank(synapseRegFile)) { | ||||
|             log.info("No synapse registration file path given - skipping generation..."); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         SynapseRegistrationYaml syncCfg = SynapseRegistrationYaml.parse(cfg.getAppsvc(), cfg.getMatrix().getDomain()); | ||||
|  | ||||
|         Representer rep = new Representer(); | ||||
|         rep.getPropertyUtils().setBeanAccess(BeanAccess.FIELD); | ||||
|         Yaml yaml = new Yaml(rep); | ||||
|  | ||||
|         // SnakeYAML set the type of object on the first line, which can fail to be parsed on synapse | ||||
|         // We therefore need to split the resulting string, remove the first line, and then write it | ||||
|         List<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) { | ||||
|         ensureEnabled(); | ||||
|  | ||||
|         if (StringUtils.isBlank(token)) { | ||||
|             throw new HttpMatrixException(401, "M_UNAUTHORIZED", "No HS token"); | ||||
|         } | ||||
|  | ||||
|         if (!StringUtils.equals(cfg.getListener().getToken().getHs(), token)) { | ||||
|         if (!StringUtils.equals(cfg.getEndpoint().getToAS().getToken(), token)) { | ||||
|             throw new NotAllowedException("Invalid HS token"); | ||||
|         } | ||||
|  | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     public void processUser(String userId) { | ||||
|         client.createUser(MatrixID.asAcceptable(userId).getLocalPart()); | ||||
|     } | ||||
|  | ||||
|     public CompletableFuture<String> processTransaction(String txnId, InputStream is) { | ||||
|         ensureEnabled(); | ||||
|  | ||||
|         if (StringUtils.isEmpty(txnId)) { | ||||
|             throw new IllegalArgumentException("Transaction ID cannot be empty"); | ||||
|         } | ||||
|  | ||||
|         synchronized (this) { | ||||
|             Optional<ASTransactionDao> dao = store.getTransactionResult(cfg.getListener().getLocalpart(), txnId); | ||||
|             Optional<ASTransactionDao> dao = store.getTransactionResult(cfg.getUser().getMain(), txnId); | ||||
|             if (dao.isPresent()) { | ||||
|                 log.info("AS Transaction {} already processed - returning computed result", txnId); | ||||
|                 return CompletableFuture.completedFuture(dao.get().getResult()); | ||||
| @@ -122,7 +229,7 @@ public class AppSvcManager { | ||||
|  | ||||
|             try { | ||||
|                 log.info("Saving transaction details to store"); | ||||
|                 store.insertTransactionResult(cfg.getListener().getLocalpart(), txnId, end, result); | ||||
|                 store.insertTransactionResult(cfg.getUser().getMain(), txnId, end, result); | ||||
|             } finally { | ||||
|                 log.debug("Removing CompletedFuture from transaction map"); | ||||
|                 transactionsInProgress.remove(txnId); | ||||
| @@ -139,7 +246,7 @@ public class AppSvcManager { | ||||
|         return future; | ||||
|     } | ||||
|  | ||||
|     public void processTransaction(List<JsonObject> eventsJson) { | ||||
|     private void processTransaction(List<JsonObject> eventsJson) { | ||||
|         log.info("Processing transaction events: start"); | ||||
|  | ||||
|         eventsJson.forEach(ev -> { | ||||
| @@ -165,54 +272,14 @@ public class AppSvcManager { | ||||
|             _MatrixID sender = MatrixID.asAcceptable(senderId); | ||||
|             log.debug("Sender: {}", senderId); | ||||
|  | ||||
|             if (!StringUtils.equals("m.room.member", GsonUtil.getStringOrNull(ev, "type"))) { | ||||
|                 log.debug("This is not a room membership event, skipping"); | ||||
|             String evType = StringUtils.defaultIfBlank(EventKey.Type.getStringOrNull(ev), "<EMPTY/MISSING>"); | ||||
|             EventTypeProcessor p = processors.get(evType); | ||||
|             if (Objects.isNull(p)) { | ||||
|                 log.debug("No event processor for type {}, skipping", evType); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (!StringUtils.equals("invite", GsonUtil.getStringOrNull(ev, "membership"))) { | ||||
|                 log.debug("This is not an invite event, skipping"); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             String inviteeId = EventKey.StateKey.getStringOrNull(ev); | ||||
|             if (StringUtils.isBlank(inviteeId)) { | ||||
|                 log.warn("Invalid event: No invitee ID, skipping"); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             _MatrixID invitee = MatrixID.asAcceptable(inviteeId); | ||||
|             if (!StringUtils.equals(invitee.getDomain(), cfg.getDomain())) { | ||||
|                 log.debug("Ignoring invite for {}: not a local user"); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             log.info("Got invite from {} to {}", senderId, inviteeId); | ||||
|  | ||||
|             boolean wasSent = false; | ||||
|             List<_ThreePid> tpids = profiler.getThreepids(invitee).stream() | ||||
|                     .filter(tpid -> ThreePidMedium.Email.is(tpid.getMedium())) | ||||
|                     .collect(Collectors.toList()); | ||||
|             log.info("Found {} email(s) in identity store for {}", tpids.size(), inviteeId); | ||||
|  | ||||
|             for (_ThreePid tpid : tpids) { | ||||
|                 log.info("Found Email to notify about room invitation: {}", tpid.getAddress()); | ||||
|                 Map<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); | ||||
|             p.process(ev, sender, roomId); | ||||
|  | ||||
|             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; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -44,6 +44,7 @@ public class ExecAuthStore extends ExecStore implements AuthenticatorProvider { | ||||
|     private ExecConfig.Auth cfg; | ||||
|  | ||||
|     public ExecAuthStore(ExecConfig cfg) { | ||||
|         super(cfg); | ||||
|         this.cfg = Objects.requireNonNull(cfg.getAuth()); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -36,11 +36,12 @@ public class ExecDirectoryStore extends ExecStore implements DirectoryProvider { | ||||
|     private MatrixConfig mxCfg; | ||||
|  | ||||
|     public ExecDirectoryStore(MxisdConfig cfg) { | ||||
|         this(cfg.getExec().getDirectory(), cfg.getMatrix()); | ||||
|         this(cfg.getExec(), cfg.getMatrix()); | ||||
|     } | ||||
|  | ||||
|     public ExecDirectoryStore(ExecConfig.Directory cfg, MatrixConfig mxCfg) { | ||||
|         this.cfg = cfg; | ||||
|     public ExecDirectoryStore(ExecConfig cfg, MatrixConfig mxCfg) { | ||||
|         super(cfg); | ||||
|         this.cfg = cfg.getDirectory(); | ||||
|         this.mxCfg = mxCfg; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -55,11 +55,8 @@ public class ExecIdentityStore extends ExecStore implements IThreePidProvider { | ||||
|     private final MatrixConfig mxCfg; | ||||
|  | ||||
|     public ExecIdentityStore(ExecConfig cfg, MatrixConfig mxCfg) { | ||||
|         this(cfg.getIdentity(), mxCfg); | ||||
|     } | ||||
|  | ||||
|     public ExecIdentityStore(ExecConfig.Identity cfg, MatrixConfig mxCfg) { | ||||
|         this.cfg = cfg; | ||||
|         super(cfg); | ||||
|         this.cfg = cfg.getIdentity(); | ||||
|         this.mxCfg = mxCfg; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -38,11 +38,8 @@ public class ExecProfileStore extends ExecStore implements ProfileProvider { | ||||
|     private ExecConfig.Profile cfg; | ||||
|  | ||||
|     public ExecProfileStore(ExecConfig cfg) { | ||||
|         this(cfg.getProfile()); | ||||
|     } | ||||
|  | ||||
|     public ExecProfileStore(ExecConfig.Profile cfg) { | ||||
|         this.cfg = cfg; | ||||
|         super(cfg); | ||||
|         this.cfg = cfg.getProfile(); | ||||
|     } | ||||
|  | ||||
|     private Optional<JsonProfileResult> getFull(_MatrixID userId, ExecConfig.Process cfg) { | ||||
|   | ||||
| @@ -43,14 +43,19 @@ public class ExecStore { | ||||
|     public static final String JsonType = "json"; | ||||
|     public static final String PlainType = "plain"; | ||||
|  | ||||
|     private static final Logger log = LoggerFactory.getLogger(ExecStore.class); | ||||
|  | ||||
|     protected static String toJson(Object o) { | ||||
|         return GsonUtil.get().toJson(o); | ||||
|     } | ||||
|  | ||||
|     private transient final Logger log = LoggerFactory.getLogger(ExecStore.class); | ||||
|  | ||||
|     private final ExecConfig cfg; | ||||
|     private Supplier<ProcessExecutor> executorSupplier = () -> new ProcessExecutor().readOutput(true); | ||||
|  | ||||
|     public ExecStore(ExecConfig cfg) { | ||||
|         this.cfg = cfg; | ||||
|     } | ||||
|  | ||||
|     public void setExecutorSupplier(Supplier<ProcessExecutor> supplier) { | ||||
|         executorSupplier = supplier; | ||||
|     } | ||||
| @@ -64,7 +69,7 @@ public class ExecStore { | ||||
|         private Function<String, String> inputUnknownTypeMapper; | ||||
|         private Map<String, Supplier<String>> inputTypeSuppliers; | ||||
|  | ||||
|         private Map<String, Function<ExecConfig.TokenOverride, String>> inputTypeTemplates; | ||||
|         private Map<String, Function<ExecConfig.Token, String>> inputTypeTemplates; | ||||
|         private Supplier<String> inputTypeNoTemplateHandler; | ||||
|         private Map<String, Supplier<String>> tokenMappers; | ||||
|         private Function<String, String> tokenHandler; | ||||
| @@ -156,11 +161,11 @@ public class ExecStore { | ||||
|             inputTypeSuppliers.put(type, handler); | ||||
|         } | ||||
|  | ||||
|         protected void addInputTemplate(String type, Function<ExecConfig.TokenOverride, String> template) { | ||||
|         protected void addInputTemplate(String type, Function<ExecConfig.Token, String> template) { | ||||
|             inputTypeTemplates.put(type, template); | ||||
|         } | ||||
|  | ||||
|         public void addJsonInputTemplate(Function<ExecConfig.TokenOverride, Object> template) { | ||||
|         public void addJsonInputTemplate(Function<ExecConfig.Token, Object> template) { | ||||
|             inputTypeTemplates.put(JsonType, token -> GsonUtil.get().toJson(template.apply(token))); | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -37,4 +37,5 @@ public class LookupSingleRequestJson { | ||||
|     public String getAddress() { | ||||
|         return address; | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -32,7 +32,7 @@ import io.kamax.mxisd.profile.JsonProfileRequest; | ||||
| import io.kamax.mxisd.profile.JsonProfileResult; | ||||
| import io.kamax.mxisd.profile.ProfileProvider; | ||||
| import org.apache.commons.io.IOUtils; | ||||
| 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.HttpPost; | ||||
| import org.apache.http.client.utils.URIBuilder; | ||||
| @@ -49,7 +49,7 @@ import java.util.function.Function; | ||||
|  | ||||
| public class RestProfileProvider extends RestProvider implements ProfileProvider { | ||||
|  | ||||
|     private transient final Logger log = LoggerFactory.getLogger(RestProfileProvider.class); | ||||
|     private static final Logger log = LoggerFactory.getLogger(RestProfileProvider.class); | ||||
|  | ||||
|     public RestProfileProvider(RestBackendConfig cfg) { | ||||
|         super(cfg); | ||||
| @@ -60,64 +60,71 @@ public class RestProfileProvider extends RestProvider implements ProfileProvider | ||||
|             Function<RestBackendConfig.ProfileEndpoints, Optional<String>> endpoint, | ||||
|             Function<JsonProfileResult, Optional<T>> value | ||||
|     ) { | ||||
|         return cfg.getEndpoints().getProfile() | ||||
|                 // We get the endpoint | ||||
|                 .flatMap(endpoint) | ||||
|                 // We only continue if there is a value | ||||
|                 .filter(StringUtils::isNotBlank) | ||||
|                 // We use the endpoint | ||||
|                 .flatMap(url -> { | ||||
|                     try { | ||||
|                         URIBuilder builder = new URIBuilder(url); | ||||
|                         HttpPost req = new HttpPost(builder.build()); | ||||
|                         req.setEntity(new StringEntity(GsonUtil.get().toJson(new JsonProfileRequest(userId)), ContentType.APPLICATION_JSON)); | ||||
|                         try (CloseableHttpResponse res = client.execute(req)) { | ||||
|                             int sc = res.getStatusLine().getStatusCode(); | ||||
|                             if (sc == 404) { | ||||
|                                 log.info("Got 404 - No result found"); | ||||
|                                 return Optional.empty(); | ||||
|                             } | ||||
|         Optional<String> url = endpoint.apply(cfg.getEndpoints().getProfile()); | ||||
|         if (!url.isPresent()) { | ||||
|             return Optional.empty(); | ||||
|         } | ||||
|  | ||||
|                             if (sc != 200) { | ||||
|                                 throw new InternalServerError("Unexpected backed status code: " + sc); | ||||
|                             } | ||||
|         try { | ||||
|             URIBuilder builder = new URIBuilder(url.get()); | ||||
|             HttpPost req = new HttpPost(builder.build()); | ||||
|             req.setEntity(new StringEntity(GsonUtil.get().toJson(new JsonProfileRequest(userId)), ContentType.APPLICATION_JSON)); | ||||
|             try (CloseableHttpResponse res = client.execute(req)) { | ||||
|                 int sc = res.getStatusLine().getStatusCode(); | ||||
|                 if (sc == 404) { | ||||
|                     log.info("Got 404 - No result found"); | ||||
|                     return Optional.empty(); | ||||
|                 } | ||||
|  | ||||
|                             String body = IOUtils.toString(res.getEntity().getContent(), StandardCharsets.UTF_8); | ||||
|                             if (StringUtils.isBlank(body)) { | ||||
|                                 log.warn("Backend response body is empty/blank, expected JSON object with profile key"); | ||||
|                                 return Optional.empty(); | ||||
|                             } | ||||
|                 if (sc != 200) { | ||||
|                     throw new InternalServerError("Unexpected backed status code: " + sc); | ||||
|                 } | ||||
|  | ||||
|                             Optional<JsonObject> pJson = GsonUtil.findObj(GsonUtil.parseObj(body), "profile"); | ||||
|                             if (!pJson.isPresent()) { | ||||
|                                 log.warn("Backend response body is invalid, expected JSON object with profile key"); | ||||
|                                 return Optional.empty(); | ||||
|                             } | ||||
|                 String body = IOUtils.toString(res.getEntity().getContent(), StandardCharsets.UTF_8); | ||||
|                 if (StringUtils.isBlank(body)) { | ||||
|                     log.warn("Backend response body is empty/blank, expected JSON object with profile key"); | ||||
|                     return Optional.empty(); | ||||
|                 } | ||||
|  | ||||
|                             JsonProfileResult profile = gson.fromJson(pJson.get(), JsonProfileResult.class); | ||||
|                             return value.apply(profile); | ||||
|                         } | ||||
|                     } catch (JsonSyntaxException | InvalidJsonException e) { | ||||
|                         log.error("Unable to parse backend response as JSON", e); | ||||
|                         throw new InternalServerError(e); | ||||
|                     } catch (URISyntaxException e) { | ||||
|                         log.error("Unable to build a valid request URL", e); | ||||
|                         throw new InternalServerError(e); | ||||
|                     } catch (IOException e) { | ||||
|                         log.error("I/O Error during backend request", e); | ||||
|                         throw new InternalServerError(); | ||||
|                     } | ||||
|                 }); | ||||
|                 Optional<JsonObject> pJson = GsonUtil.findObj(GsonUtil.parseObj(body), "profile"); | ||||
|                 if (!pJson.isPresent()) { | ||||
|                     log.warn("Backend response body is invalid, expected JSON object with profile key"); | ||||
|                     return Optional.empty(); | ||||
|                 } | ||||
|  | ||||
|                 JsonProfileResult profile = gson.fromJson(pJson.get(), JsonProfileResult.class); | ||||
|                 return value.apply(profile); | ||||
|             } | ||||
|         } catch (JsonSyntaxException | InvalidJsonException e) { | ||||
|             log.error("Unable to parse backend response as JSON", e); | ||||
|             throw new InternalServerError(e); | ||||
|         } catch (URISyntaxException e) { | ||||
|             log.error("Unable to build a valid request URL", e); | ||||
|             throw new InternalServerError(e); | ||||
|         } catch (IOException e) { | ||||
|             log.error("I/O Error during backend request", e); | ||||
|             throw new InternalServerError(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public Optional<String> getDisplayName(_MatrixID userId) { | ||||
|         return doRequest(userId, p -> Optional.ofNullable(p.getDisplayName()), profile -> Optional.ofNullable(profile.getDisplayName())); | ||||
|         return doRequest(userId, p -> { | ||||
|             if (StringUtils.isBlank(p.getDisplayName())) { | ||||
|                 return Optional.empty(); | ||||
|             } | ||||
|             return Optional.ofNullable(p.getDisplayName()); | ||||
|         }, profile -> Optional.ofNullable(profile.getDisplayName())); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public List<_ThreePid> getThreepids(_MatrixID userId) { | ||||
|         return doRequest(userId, p -> Optional.ofNullable(p.getThreepids()), profile -> { | ||||
|         return doRequest(userId, p -> { | ||||
|             if (StringUtils.isBlank(p.getThreepids())) { | ||||
|                 return Optional.empty(); | ||||
|             } | ||||
|             return Optional.ofNullable(p.getThreepids()); | ||||
|         }, profile -> { | ||||
|             List<_ThreePid> t = new ArrayList<>(); | ||||
|             if (Objects.nonNull(profile.getThreepids())) { | ||||
|                 t.addAll(profile.getThreepids()); | ||||
| @@ -128,7 +135,12 @@ public class RestProfileProvider extends RestProvider implements ProfileProvider | ||||
|  | ||||
|     @Override | ||||
|     public List<String> getRoles(_MatrixID userId) { | ||||
|         return doRequest(userId, p -> Optional.ofNullable(p.getRoles()), profile -> { | ||||
|         return doRequest(userId, p -> { | ||||
|             if (StringUtils.isBlank(p.getRoles())) { | ||||
|                 return Optional.empty(); | ||||
|             } | ||||
|             return Optional.ofNullable(p.getRoles()); | ||||
|         }, profile -> { | ||||
|             List<String> t = new ArrayList<>(); | ||||
|             if (Objects.nonNull(profile.getRoles())) { | ||||
|                 t.addAll(profile.getRoles()); | ||||
|   | ||||
| @@ -23,7 +23,9 @@ package io.kamax.mxisd.backend.sql; | ||||
| import io.kamax.matrix.ThreePid; | ||||
| import io.kamax.matrix._MatrixID; | ||||
| import io.kamax.matrix._ThreePid; | ||||
| import io.kamax.mxisd.UserIdType; | ||||
| import io.kamax.mxisd.config.sql.SqlConfig; | ||||
| import io.kamax.mxisd.exception.InternalServerError; | ||||
| import io.kamax.mxisd.profile.ProfileProvider; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| @@ -33,16 +35,14 @@ import java.sql.PreparedStatement; | ||||
| import java.sql.ResultSet; | ||||
| import java.sql.SQLException; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| import java.util.Optional; | ||||
|  | ||||
| public abstract class SqlProfileProvider implements ProfileProvider { | ||||
|  | ||||
|     private transient final Logger log = LoggerFactory.getLogger(SqlProfileProvider.class); | ||||
|     private static final Logger log = LoggerFactory.getLogger(SqlProfileProvider.class); | ||||
|  | ||||
|     private SqlConfig.Profile cfg; | ||||
|  | ||||
|     private SqlConnectionPool pool; | ||||
|  | ||||
|     public SqlProfileProvider(SqlConfig cfg) { | ||||
| @@ -50,6 +50,12 @@ public abstract class SqlProfileProvider implements ProfileProvider { | ||||
|         this.pool = new SqlConnectionPool(cfg); | ||||
|     } | ||||
|  | ||||
|     private void setParameters(PreparedStatement stmt, String value) throws SQLException { | ||||
|         for (int i = 1; i <= stmt.getParameterMetaData().getParameterCount(); i++) { | ||||
|             stmt.setString(i, value); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public Optional<String> getDisplayName(_MatrixID user) { | ||||
|         String stmtSql = cfg.getDisplayName().getQuery(); | ||||
| @@ -94,7 +100,33 @@ public abstract class SqlProfileProvider implements ProfileProvider { | ||||
|  | ||||
|     @Override | ||||
|     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 | ||||
|     public void accept(Mxisd mxisd) { | ||||
|         if (mxisd.getConfig().getSql().getAuth().isEnabled()) { | ||||
|             AuthProviders.register(() -> new GenericSqlAuthProvider(mxisd.getConfig().getSql(), mxisd.getInvitationManager())); | ||||
|             AuthProviders.register(() -> new GenericSqlAuthProvider(mxisd.getConfig().getSql(), mxisd.getInvite())); | ||||
|         } | ||||
|  | ||||
|         if (mxisd.getConfig().getSql().getDirectory().isEnabled()) { | ||||
|   | ||||
| @@ -43,6 +43,10 @@ public class SynapseQueries { | ||||
|         return "SELECT medium, address FROM user_threepids WHERE user_id = ?"; | ||||
|     } | ||||
|  | ||||
|     public static String getRoles() { | ||||
|         return "SELECT DISTINCT(group_id) FROM group_users WHERE user_id = ?"; | ||||
|     } | ||||
|  | ||||
|     public static String findByDisplayName(String type, String domain) { | ||||
|         if (StringUtils.equals("sqlite", type)) { | ||||
|             return "select " + getUserId(type, domain) + ", displayname from profiles p where displayname like ?"; | ||||
|   | ||||
| @@ -26,9 +26,13 @@ import io.kamax.mxisd.config.MxisdConfig; | ||||
| import io.kamax.mxisd.directory.DirectoryProviders; | ||||
| import io.kamax.mxisd.lookup.ThreePidProviders; | ||||
| import io.kamax.mxisd.profile.ProfileProviders; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
|  | ||||
| public class SynapseSqlStoreSupplier implements IdentityStoreSupplier { | ||||
|  | ||||
|     private static final Logger log = LoggerFactory.getLogger(SynapseSqlStoreSupplier.class); | ||||
|  | ||||
|     @Override | ||||
|     public void accept(Mxisd mxisd) { | ||||
|         accept(mxisd.getConfig()); | ||||
| @@ -44,6 +48,7 @@ public class SynapseSqlStoreSupplier implements IdentityStoreSupplier { | ||||
|         } | ||||
|  | ||||
|         if (cfg.getSynapseSql().getProfile().isEnabled()) { | ||||
|             log.debug("Profile is enabled, registering provider"); | ||||
|             ProfileProviders.register(() -> new SynapseSqlProfileProvider(cfg.getSynapseSql())); | ||||
|         } | ||||
|     } | ||||
|   | ||||
							
								
								
									
										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(); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -36,9 +36,8 @@ public class DirectoryConfig { | ||||
|             return homeserver; | ||||
|         } | ||||
|  | ||||
|         public Exclude setHomeserver(boolean homeserver) { | ||||
|         public void setHomeserver(boolean homeserver) { | ||||
|             this.homeserver = homeserver; | ||||
|             return this; | ||||
|         } | ||||
|  | ||||
|         public boolean getThreepid() { | ||||
| @@ -64,8 +63,8 @@ public class DirectoryConfig { | ||||
|     public void build() { | ||||
|         log.info("--- Directory config ---"); | ||||
|         log.info("Exclude:"); | ||||
|         log.info("\tHomeserver: {}", getExclude().getHomeserver()); | ||||
|         log.info("\t3PID: {}", getExclude().getThreepid()); | ||||
|         log.info("  Homeserver: {}", getExclude().getHomeserver()); | ||||
|         log.info("  3PID: {}", getExclude().getThreepid()); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -20,13 +20,11 @@ | ||||
|  | ||||
| package io.kamax.mxisd.config; | ||||
|  | ||||
| import org.apache.commons.lang3.StringUtils; | ||||
|  | ||||
| import java.util.*; | ||||
|  | ||||
| public class ExecConfig { | ||||
|  | ||||
|     public class IO { | ||||
|     public static class IO { | ||||
|  | ||||
|         private String type; | ||||
|         private String template; | ||||
| @@ -49,7 +47,7 @@ public class ExecConfig { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     public class Exit { | ||||
|     public static class Exit { | ||||
|  | ||||
|         private List<Integer> success = Collections.singletonList(0); | ||||
|         private List<Integer> failure = Collections.singletonList(1); | ||||
| @@ -72,84 +70,7 @@ public class ExecConfig { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     public class TokenOverride { | ||||
|  | ||||
|         private String localpart; | ||||
|         private String domain; | ||||
|         private String mxid; | ||||
|         private String password; | ||||
|         private String medium; | ||||
|         private String address; | ||||
|         private String type; | ||||
|         private String query; | ||||
|  | ||||
|         public String getLocalpart() { | ||||
|             return StringUtils.defaultIfEmpty(localpart, getToken().getLocalpart()); | ||||
|         } | ||||
|  | ||||
|         public void setLocalpart(String localpart) { | ||||
|             this.localpart = localpart; | ||||
|         } | ||||
|  | ||||
|         public String getDomain() { | ||||
|             return StringUtils.defaultIfEmpty(domain, getToken().getDomain()); | ||||
|         } | ||||
|  | ||||
|         public void setDomain(String domain) { | ||||
|             this.domain = domain; | ||||
|         } | ||||
|  | ||||
|         public String getMxid() { | ||||
|             return StringUtils.defaultIfEmpty(mxid, getToken().getMxid()); | ||||
|         } | ||||
|  | ||||
|         public void setMxid(String mxid) { | ||||
|             this.mxid = mxid; | ||||
|         } | ||||
|  | ||||
|         public String getPassword() { | ||||
|             return StringUtils.defaultIfEmpty(password, getToken().getPassword()); | ||||
|         } | ||||
|  | ||||
|         public void setPassword(String password) { | ||||
|             this.password = password; | ||||
|         } | ||||
|  | ||||
|         public String getMedium() { | ||||
|             return StringUtils.defaultIfEmpty(medium, getToken().getMedium()); | ||||
|         } | ||||
|  | ||||
|         public void setMedium(String medium) { | ||||
|             this.medium = medium; | ||||
|         } | ||||
|  | ||||
|         public String getAddress() { | ||||
|             return StringUtils.defaultIfEmpty(address, getToken().getAddress()); | ||||
|         } | ||||
|  | ||||
|         public void setAddress(String address) { | ||||
|             this.address = address; | ||||
|         } | ||||
|  | ||||
|         public String getType() { | ||||
|             return StringUtils.defaultIfEmpty(type, getToken().getType()); | ||||
|         } | ||||
|  | ||||
|         public void setType(String type) { | ||||
|             this.type = type; | ||||
|         } | ||||
|  | ||||
|         public String getQuery() { | ||||
|             return StringUtils.defaultIfEmpty(query, getToken().getQuery()); | ||||
|         } | ||||
|  | ||||
|         public void setQuery(String query) { | ||||
|             this.query = query; | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     public class Token { | ||||
|     public static class Token { | ||||
|  | ||||
|         private String localpart = "{localpart}"; | ||||
|         private String domain = "{domain}"; | ||||
| @@ -226,9 +147,9 @@ public class ExecConfig { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     public class Process { | ||||
|     public static class Process { | ||||
|  | ||||
|         private TokenOverride token = new TokenOverride(); | ||||
|         private Token token = new Token(); | ||||
|         private String command; | ||||
|  | ||||
|         private List<String> args = new ArrayList<>(); | ||||
| @@ -238,11 +159,11 @@ public class ExecConfig { | ||||
|         private Exit exit = new Exit(); | ||||
|         private IO output = new IO(); | ||||
|  | ||||
|         public TokenOverride getToken() { | ||||
|         public Token getToken() { | ||||
|             return token; | ||||
|         } | ||||
|  | ||||
|         public void setToken(TokenOverride token) { | ||||
|         public void setToken(Token token) { | ||||
|             this.token = token; | ||||
|         } | ||||
|  | ||||
| @@ -300,7 +221,7 @@ public class ExecConfig { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     public class Auth extends Process { | ||||
|     public static class Auth extends Process { | ||||
|  | ||||
|         private Boolean enabled; | ||||
|  | ||||
| @@ -314,9 +235,9 @@ public class ExecConfig { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     public class Directory { | ||||
|     public static class Directory { | ||||
|  | ||||
|         public class Search { | ||||
|         public static class Search { | ||||
|  | ||||
|             private Process byName = new Process(); | ||||
|             private Process byThreepid = new Process(); | ||||
| @@ -360,7 +281,7 @@ public class ExecConfig { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     public class Lookup { | ||||
|     public static class Lookup { | ||||
|  | ||||
|         private Process single = new Process(); | ||||
|         private Process bulk = new Process(); | ||||
| @@ -383,7 +304,7 @@ public class ExecConfig { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     public class Identity { | ||||
|     public static class Identity { | ||||
|  | ||||
|         private Boolean enabled; | ||||
|         private int priority; | ||||
| @@ -415,7 +336,7 @@ public class ExecConfig { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     public class Profile { | ||||
|     public static class Profile { | ||||
|  | ||||
|         private Boolean enabled; | ||||
|         private Process displayName = new Process(); | ||||
|   | ||||
| @@ -20,18 +20,53 @@ | ||||
|  | ||||
| package io.kamax.mxisd.config; | ||||
|  | ||||
| import io.kamax.mxisd.util.GsonUtil; | ||||
| import io.kamax.matrix.json.GsonUtil; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
|  | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
|  | ||||
| public class InvitationConfig { | ||||
|  | ||||
|     private transient final Logger log = LoggerFactory.getLogger(InvitationConfig.class); | ||||
|     private static final Logger log = LoggerFactory.getLogger(InvitationConfig.class); | ||||
|  | ||||
|     public static class Expiration { | ||||
|  | ||||
|         private Boolean enabled; | ||||
|         private long after = 60 * 24 * 7; // One calendar week (60min/1h * 24 = 1d * 7 = 1w) | ||||
|         private String resolveTo; | ||||
|  | ||||
|         public Boolean isEnabled() { | ||||
|             return enabled; | ||||
|         } | ||||
|  | ||||
|         public void setEnabled(boolean enabled) { | ||||
|             this.enabled = enabled; | ||||
|         } | ||||
|  | ||||
|         public long getAfter() { | ||||
|             return after; | ||||
|         } | ||||
|  | ||||
|         public void setAfter(long after) { | ||||
|             this.after = after; | ||||
|         } | ||||
|  | ||||
|         public String getResolveTo() { | ||||
|             return resolveTo; | ||||
|         } | ||||
|  | ||||
|         public void setResolveTo(String resolveTo) { | ||||
|             this.resolveTo = resolveTo; | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     public static class Resolution { | ||||
|  | ||||
|         private boolean recursive = true; | ||||
|         private long timer = 1; | ||||
|         private long timer = 5; | ||||
|  | ||||
|         public boolean isRecursive() { | ||||
|             return recursive; | ||||
| @@ -51,7 +86,43 @@ public class InvitationConfig { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     public static class SenderPolicy { | ||||
|  | ||||
|         private List<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 Policies policy = new Policies(); | ||||
|  | ||||
|     public Expiration getExpiration() { | ||||
|         return expiration; | ||||
|     } | ||||
|  | ||||
|     public void setExpiration(Expiration expiration) { | ||||
|         this.expiration = expiration; | ||||
|     } | ||||
|  | ||||
|     public Resolution getResolution() { | ||||
|         return resolution; | ||||
| @@ -61,9 +132,19 @@ public class InvitationConfig { | ||||
|         this.resolution = resolution; | ||||
|     } | ||||
|  | ||||
|     public Policies getPolicy() { | ||||
|         return policy; | ||||
|     } | ||||
|  | ||||
|     public void setPolicy(Policies policy) { | ||||
|         this.policy = policy; | ||||
|     } | ||||
|  | ||||
|     public void build() { | ||||
|         log.info("--- Invite config ---"); | ||||
|         log.info("Resolution: {}", GsonUtil.build().toJson(resolution)); | ||||
|         log.info("Expiration: {}", GsonUtil.get().toJson(getExpiration())); | ||||
|         log.info("Resolution: {}", GsonUtil.get().toJson(getResolution())); | ||||
|         log.info("Policies: {}", GsonUtil.get().toJson(getPolicy())); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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 Identity identity = new Identity(); | ||||
|     private ListenerConfig listener = new ListenerConfig(); | ||||
|  | ||||
|     public String getDomain() { | ||||
|         return domain; | ||||
| @@ -81,14 +80,6 @@ public class MatrixConfig { | ||||
|         this.identity = identity; | ||||
|     } | ||||
|  | ||||
|     public ListenerConfig getListener() { | ||||
|         return listener; | ||||
|     } | ||||
|  | ||||
|     public void setListener(ListenerConfig listener) { | ||||
|         this.listener = listener; | ||||
|     } | ||||
|  | ||||
|     public void build() { | ||||
|         log.info("--- Matrix config ---"); | ||||
|  | ||||
| @@ -99,8 +90,6 @@ public class MatrixConfig { | ||||
|         log.info("Domain: {}", getDomain()); | ||||
|         log.info("Identity:"); | ||||
|         log.info("\tServers: {}", GsonUtil.get().toJson(identity.getServers())); | ||||
|  | ||||
|         listener.build(); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -83,6 +83,7 @@ public class MxisdConfig { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     private AppServiceConfig appsvc = new AppServiceConfig(); | ||||
|     private AuthenticationConfig auth = new AuthenticationConfig(); | ||||
|     private DirectoryConfig directory = new DirectoryConfig(); | ||||
|     private Dns dns = new Dns(); | ||||
| @@ -97,6 +98,7 @@ public class MxisdConfig { | ||||
|     private MemoryStoreConfig memory = new MemoryStoreConfig(); | ||||
|     private NotificationConfig notification = new NotificationConfig(); | ||||
|     private NetIqLdapConfig netiq = new NetIqLdapConfig(); | ||||
|     private RegisterConfig register = new RegisterConfig(); | ||||
|     private ServerConfig server = new ServerConfig(); | ||||
|     private SessionConfig session = new SessionConfig(); | ||||
|     private StorageConfig storage = new StorageConfig(); | ||||
| @@ -107,6 +109,14 @@ public class MxisdConfig { | ||||
|     private ViewConfig view = new ViewConfig(); | ||||
|     private WordpressConfig wordpress = new WordpressConfig(); | ||||
|  | ||||
|     public AppServiceConfig getAppsvc() { | ||||
|         return appsvc; | ||||
|     } | ||||
|  | ||||
|     public void setAppsvc(AppServiceConfig appsvc) { | ||||
|         this.appsvc = appsvc; | ||||
|     } | ||||
|  | ||||
|     public AuthenticationConfig getAuth() { | ||||
|         return auth; | ||||
|     } | ||||
| @@ -219,6 +229,14 @@ public class MxisdConfig { | ||||
|         this.netiq = netiq; | ||||
|     } | ||||
|  | ||||
|     public RegisterConfig getRegister() { | ||||
|         return register; | ||||
|     } | ||||
|  | ||||
|     public void setRegister(RegisterConfig register) { | ||||
|         this.register = register; | ||||
|     } | ||||
|  | ||||
|     public ServerConfig getServer() { | ||||
|         return server; | ||||
|     } | ||||
| @@ -297,6 +315,7 @@ public class MxisdConfig { | ||||
|             log.debug("server.name is empty, using matrix.domain"); | ||||
|         } | ||||
|  | ||||
|         getAppsvc().build(); | ||||
|         getAuth().build(); | ||||
|         getDirectory().build(); | ||||
|         getExec().build(); | ||||
| @@ -310,6 +329,7 @@ public class MxisdConfig { | ||||
|         getMemory().build(); | ||||
|         getNetiq().build(); | ||||
|         getNotification().build(); | ||||
|         getRegister().build(); | ||||
|         getRest().build(); | ||||
|         getSession().build(); | ||||
|         getServer().build(); | ||||
|   | ||||
							
								
								
									
										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)); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -21,10 +21,16 @@ | ||||
| package io.kamax.mxisd.config; | ||||
|  | ||||
| import io.kamax.matrix.json.GsonUtil; | ||||
| import io.kamax.mxisd.exception.ConfigurationException; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| import org.yaml.snakeyaml.Yaml; | ||||
| import org.yaml.snakeyaml.constructor.Constructor; | ||||
| import org.yaml.snakeyaml.introspector.BeanAccess; | ||||
| import org.yaml.snakeyaml.parser.ParserException; | ||||
| import org.yaml.snakeyaml.representer.Representer; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.FileInputStream; | ||||
| import java.io.FileNotFoundException; | ||||
| import java.io.IOException; | ||||
| @@ -32,21 +38,38 @@ import java.util.Optional; | ||||
|  | ||||
| public class YamlConfigLoader { | ||||
|  | ||||
|     private static final Logger log = LoggerFactory.getLogger(YamlConfigLoader.class); | ||||
|  | ||||
|     public static MxisdConfig loadFromFile(String path) throws IOException { | ||||
|         File f = new File(path).getAbsoluteFile(); | ||||
|         log.info("Reading config from {}", f.toString()); | ||||
|         Representer rep = new Representer(); | ||||
|         rep.getPropertyUtils().setBeanAccess(BeanAccess.FIELD); | ||||
|         rep.getPropertyUtils().setAllowReadOnlyProperties(true); | ||||
|         rep.getPropertyUtils().setSkipMissingProperties(true); | ||||
|         Yaml yaml = new Yaml(new Constructor(MxisdConfig.class), rep); | ||||
|         try (FileInputStream is = new FileInputStream(path)) { | ||||
|             Object o = yaml.load(is); | ||||
|             return GsonUtil.get().fromJson(GsonUtil.get().toJson(o), MxisdConfig.class); | ||||
|         try (FileInputStream is = new FileInputStream(f)) { | ||||
|             MxisdConfig raw = yaml.load(is); | ||||
|             log.debug("Read config in memory from {}", path); | ||||
|  | ||||
|             // SnakeYaml set objects to null when there is no value set in the config, even a full sub-tree. | ||||
|             // This is problematic for default config values and objects, to avoid NPEs. | ||||
|             // Therefore, we'll use Gson to re-parse the data in a way that avoids us checking the whole config for nulls. | ||||
|             MxisdConfig cfg = GsonUtil.get().fromJson(GsonUtil.get().toJson(raw), MxisdConfig.class); | ||||
|  | ||||
|             log.info("Loaded config from {}", path); | ||||
|             return cfg; | ||||
|         } catch (ParserException t) { | ||||
|             throw new ConfigurationException(t.getMessage(), "Could not parse YAML config file - Please check indentation and that the configuration options exist"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public static Optional<MxisdConfig> tryLoadFromFile(String path) { | ||||
|         log.debug("Attempting to read config from {}", path); | ||||
|         try { | ||||
|             return Optional.of(loadFromFile(path)); | ||||
|         } catch (FileNotFoundException e) { | ||||
|             log.info("No config file at {}", path); | ||||
|             return Optional.empty(); | ||||
|         } catch (IOException e) { | ||||
|             throw new RuntimeException(e); | ||||
|   | ||||
| @@ -421,9 +421,9 @@ public abstract class LdapConfig { | ||||
|         log.info("Port: {}", connection.getPort()); | ||||
|         log.info("TLS: {}", connection.isTls()); | ||||
|         log.info("Bind DN: {}", connection.getBindDn()); | ||||
|         log.info("Base DNs: {}"); | ||||
|         log.info("Base DNs:"); | ||||
|         for (String baseDN : connection.getBaseDNs()) { | ||||
|             log.info("\t- {}", baseDN); | ||||
|             log.info("  - {}", baseDN); | ||||
|         } | ||||
|  | ||||
|         log.info("Attribute: {}", GsonUtil.get().toJson(attribute)); | ||||
|   | ||||
| @@ -28,7 +28,6 @@ import org.slf4j.LoggerFactory; | ||||
| import java.net.MalformedURLException; | ||||
| import java.net.URL; | ||||
| import java.util.Objects; | ||||
| import java.util.Optional; | ||||
|  | ||||
| public class RestBackendConfig { | ||||
|  | ||||
| @@ -118,8 +117,8 @@ public class RestBackendConfig { | ||||
|             this.identity = identity; | ||||
|         } | ||||
|  | ||||
|         public Optional<ProfileEndpoints> getProfile() { | ||||
|             return Optional.ofNullable(profile); | ||||
|         public ProfileEndpoints getProfile() { | ||||
|             return profile; | ||||
|         } | ||||
|  | ||||
|         public void setProfile(ProfileEndpoints profile) { | ||||
| @@ -128,7 +127,7 @@ public class RestBackendConfig { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     private transient final Logger log = LoggerFactory.getLogger(RestBackendConfig.class); | ||||
|     private static final Logger log = LoggerFactory.getLogger(RestBackendConfig.class); | ||||
|  | ||||
|     private boolean enabled; | ||||
|     private String host; | ||||
| @@ -197,6 +196,11 @@ public class RestBackendConfig { | ||||
|             log.info("Directory endpoint: {}", endpoints.getDirectory()); | ||||
|             log.info("Identity Single endpoint: {}", endpoints.identity.getSingle()); | ||||
|             log.info("Identity Bulk endpoint: {}", endpoints.identity.getBulk()); | ||||
|  | ||||
|             log.info("Profile endpoints:"); | ||||
|             log.info("  - Display name: {}", getEndpoints().getProfile().getDisplayName()); | ||||
|             log.info("  - 3PIDs: {}", getEndpoints().getProfile().getThreepids()); | ||||
|             log.info("  - Roles: {}", getEndpoints().getProfile().getRoles()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -21,6 +21,7 @@ | ||||
| package io.kamax.mxisd.config.sql; | ||||
|  | ||||
| import io.kamax.mxisd.util.GsonUtil; | ||||
| import org.apache.commons.lang3.StringUtils; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
|  | ||||
| @@ -192,11 +193,35 @@ public abstract class SqlConfig { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     public static class ProfileRoles { | ||||
|  | ||||
|         private String type; | ||||
|         private String query; | ||||
|  | ||||
|         public String getType() { | ||||
|             return type; | ||||
|         } | ||||
|  | ||||
|         public void setType(String type) { | ||||
|             this.type = type; | ||||
|         } | ||||
|  | ||||
|         public String getQuery() { | ||||
|             return query; | ||||
|         } | ||||
|  | ||||
|         public void setQuery(String query) { | ||||
|             this.query = query; | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     public static class Profile { | ||||
|  | ||||
|         private Boolean enabled; | ||||
|         private ProfileDisplayName displayName = new ProfileDisplayName(); | ||||
|         private ProfileThreepids threepid = new ProfileThreepids(); | ||||
|         private ProfileRoles role = new ProfileRoles(); | ||||
|  | ||||
|         public Boolean isEnabled() { | ||||
|             return enabled; | ||||
| @@ -222,6 +247,14 @@ public abstract class SqlConfig { | ||||
|             this.threepid = threepid; | ||||
|         } | ||||
|  | ||||
|         public ProfileRoles getRole() { | ||||
|             return role; | ||||
|         } | ||||
|  | ||||
|         public void setRole(ProfileRoles role) { | ||||
|             this.role = role; | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     private boolean enabled; | ||||
| @@ -314,17 +347,19 @@ public abstract class SqlConfig { | ||||
|         log.info("Enabled: {}", isEnabled()); | ||||
|         if (isEnabled()) { | ||||
|             log.info("Type: {}", getType()); | ||||
|             log.info("Connection: {}", getConnection()); | ||||
|             log.info("Has connection info? {}", !StringUtils.isEmpty(getConnection())); | ||||
|             log.debug("Connection: {}", getConnection()); | ||||
|             log.info("Auth enabled: {}", getAuth().isEnabled()); | ||||
|             log.info("Directory queries: {}", GsonUtil.build().toJson(getDirectory().getQuery())); | ||||
|             log.info("Identity type: {}", getIdentity().getType()); | ||||
|             log.info("3PID mapping query: {}", getIdentity().getQuery()); | ||||
|             log.info("Identity medium queries: {}", GsonUtil.build().toJson(getIdentity().getMedium())); | ||||
|             log.info("Profile:"); | ||||
|             log.info("\tEnabled: {}", getProfile().isEnabled()); | ||||
|             log.info("  Enabled: {}", getProfile().isEnabled()); | ||||
|             if (getProfile().isEnabled()) { | ||||
|                 log.info("\tDisplay name query: {}", getProfile().getDisplayName().getQuery()); | ||||
|                 log.info("\tProfile 3PID query: {}", getProfile().getThreepid().getQuery()); | ||||
|                 log.info("  Display name query: {}", getProfile().getDisplayName().getQuery()); | ||||
|                 log.info("  Profile 3PID query: {}", getProfile().getThreepid().getQuery()); | ||||
|                 log.info("  Role query: {}", getProfile().getRole().getQuery()); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -20,6 +20,7 @@ | ||||
|  | ||||
| package io.kamax.mxisd.config.sql.synapse; | ||||
|  | ||||
| import io.kamax.mxisd.UserIdType; | ||||
| import io.kamax.mxisd.backend.sql.synapse.SynapseQueries; | ||||
| import io.kamax.mxisd.config.sql.SqlConfig; | ||||
| import org.apache.commons.lang.StringUtils; | ||||
| @@ -48,9 +49,17 @@ public class SynapseSqlProviderConfig extends SqlConfig { | ||||
|             if (StringUtils.isBlank(getProfile().getDisplayName().getQuery())) { | ||||
|                 getProfile().getDisplayName().setQuery(SynapseQueries.getDisplayName()); | ||||
|             } | ||||
|  | ||||
|             if (StringUtils.isBlank(getProfile().getThreepid().getQuery())) { | ||||
|                 getProfile().getThreepid().setQuery(SynapseQueries.getThreepids()); | ||||
|             } | ||||
|  | ||||
|             if (StringUtils.isBlank(getProfile().getRole().getType())) { | ||||
|                 getProfile().getRole().setType(UserIdType.MatrixID.getId()); | ||||
|             } | ||||
|             if (StringUtils.isBlank(getProfile().getRole().getQuery())) { | ||||
|                 getProfile().getRole().setQuery(SynapseQueries.getRoles()); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         printConfig(); | ||||
|   | ||||
| @@ -115,28 +115,6 @@ public class EmailSendGridConfig { | ||||
|  | ||||
|     public static class Templates { | ||||
|  | ||||
|         public static class TemplateSessionValidation { | ||||
|  | ||||
|             private EmailTemplate local = new EmailTemplate(); | ||||
|             private EmailTemplate remote = new EmailTemplate(); | ||||
|  | ||||
|             public EmailTemplate getLocal() { | ||||
|                 return local; | ||||
|             } | ||||
|  | ||||
|             public void setLocal(EmailTemplate local) { | ||||
|                 this.local = local; | ||||
|             } | ||||
|  | ||||
|             public EmailTemplate getRemote() { | ||||
|                 return remote; | ||||
|             } | ||||
|  | ||||
|             public void setRemote(EmailTemplate remote) { | ||||
|                 this.remote = remote; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public static class TemplateSessionUnbind { | ||||
|  | ||||
|             private EmailTemplate fraudulent = new EmailTemplate(); | ||||
| @@ -153,14 +131,14 @@ public class EmailSendGridConfig { | ||||
|  | ||||
|         public static class TemplateSession { | ||||
|  | ||||
|             private TemplateSessionValidation validation = new TemplateSessionValidation(); | ||||
|             private EmailTemplate validation = new EmailTemplate(); | ||||
|             private TemplateSessionUnbind unbind = new TemplateSessionUnbind(); | ||||
|  | ||||
|             public TemplateSessionValidation getValidation() { | ||||
|             public EmailTemplate getValidation() { | ||||
|                 return validation; | ||||
|             } | ||||
|  | ||||
|             public void setValidation(TemplateSessionValidation validation) { | ||||
|             public void setValidation(EmailTemplate validation) { | ||||
|                 this.validation = validation; | ||||
|             } | ||||
|  | ||||
|   | ||||
| @@ -30,17 +30,17 @@ public class EmailTemplateConfig extends GenericTemplateConfig { | ||||
|     public EmailTemplateConfig() { | ||||
|         setInvite("classpath:/threepids/email/invite-template.eml"); | ||||
|         getGeneric().put("matrixId", "classpath:/threepids/email/mxid-template.eml"); | ||||
|         getSession().getValidation().setLocal("classpath:/threepids/email/validate-local-template.eml"); | ||||
|         getSession().getValidation().setRemote("classpath:/threepids/email/validate-remote-template.eml"); | ||||
|         getSession().setValidation("classpath:/threepids/email/validate-template.eml"); | ||||
|         getSession().getUnbind().setFraudulent("classpath:/threepids/email/unbind-fraudulent.eml"); | ||||
|     } | ||||
|  | ||||
|     public EmailTemplateConfig build() { | ||||
|         log.info("--- E-mail Generator templates config ---"); | ||||
|         log.info("Invite: {}", getName(getInvite())); | ||||
|         log.info("Session validation:"); | ||||
|         log.info("\tLocal: {}", getName(getSession().getValidation().getLocal())); | ||||
|         log.info("\tRemote: {}", getName(getSession().getValidation().getRemote())); | ||||
|         log.info("Session:"); | ||||
|         log.info("  Validation: {}", getSession().getValidation()); | ||||
|         log.info("  Unbind:"); | ||||
|         log.info("    Fraudulent: {}", getSession().getUnbind().getFraudulent()); | ||||
|  | ||||
|         return this; | ||||
|     } | ||||
|   | ||||
| @@ -39,29 +39,6 @@ public class GenericTemplateConfig { | ||||
|  | ||||
|     public static class Session { | ||||
|  | ||||
|         public static class SessionValidation { | ||||
|  | ||||
|             private String local; | ||||
|             private String remote; | ||||
|  | ||||
|             public String getLocal() { | ||||
|                 return local; | ||||
|             } | ||||
|  | ||||
|             public void setLocal(String local) { | ||||
|                 this.local = local; | ||||
|             } | ||||
|  | ||||
|             public String getRemote() { | ||||
|                 return remote; | ||||
|             } | ||||
|  | ||||
|             public void setRemote(String remote) { | ||||
|                 this.remote = remote; | ||||
|             } | ||||
|  | ||||
|         } | ||||
|  | ||||
|         public static class SessionUnbind { | ||||
|  | ||||
|             private String fraudulent; | ||||
| @@ -76,14 +53,14 @@ public class GenericTemplateConfig { | ||||
|  | ||||
|         } | ||||
|  | ||||
|         private SessionValidation validation = new SessionValidation(); | ||||
|         private String validation; | ||||
|         private SessionUnbind unbind = new SessionUnbind(); | ||||
|  | ||||
|         public SessionValidation getValidation() { | ||||
|         public String getValidation() { | ||||
|             return validation; | ||||
|         } | ||||
|  | ||||
|         public void setValidation(SessionValidation validation) { | ||||
|         public void setValidation(String validation) { | ||||
|             this.validation = validation; | ||||
|         } | ||||
|  | ||||
| @@ -100,6 +77,7 @@ public class GenericTemplateConfig { | ||||
|     private String invite; | ||||
|     private Session session = new Session(); | ||||
|     private Map<String, String> generic = new HashMap<>(); | ||||
|     private Map<String, String> placeholder = new HashMap<>(); | ||||
|  | ||||
|     public String getInvite() { | ||||
|         return invite; | ||||
| @@ -121,4 +99,12 @@ public class GenericTemplateConfig { | ||||
|         this.generic = generic; | ||||
|     } | ||||
|  | ||||
|     public Map<String, String> getPlaceholder() { | ||||
|         return placeholder; | ||||
|     } | ||||
|  | ||||
|     public void setPlaceholder(Map<String, String> placeholder) { | ||||
|         this.placeholder = placeholder; | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -29,18 +29,17 @@ public class PhoneSmsTemplateConfig extends GenericTemplateConfig { | ||||
|  | ||||
|     public PhoneSmsTemplateConfig() { | ||||
|         setInvite("classpath:/threepids/sms/invite-template.txt"); | ||||
|         getGeneric().put("matrixId", "classpath:/threepids/email/mxid-template.eml"); | ||||
|         getSession().getValidation().setLocal("classpath:/threepids/sms/validate-local-template.txt"); | ||||
|         getSession().getValidation().setRemote("classpath:/threepids/sms/validate-remote-template.txt"); | ||||
|         getSession().setValidation("classpath:/threepids/sms/validate-template.txt"); | ||||
|         getSession().getUnbind().setFraudulent("classpath:/threepids/sms/unbind-fraudulent.txt"); | ||||
|     } | ||||
|  | ||||
|     public PhoneSmsTemplateConfig build() { | ||||
|         log.info("--- SMS Generator templates config ---"); | ||||
|         log.info("Invite: {}", getName(getInvite())); | ||||
|         log.info("Session validation:"); | ||||
|         log.info("\tLocal: {}", getName(getSession().getValidation().getLocal())); | ||||
|         log.info("\tRemote: {}", getName(getSession().getValidation().getRemote())); | ||||
|         log.info("Session:"); | ||||
|         log.info("  Validation: {}", getSession().getValidation()); | ||||
|         log.info("  Unbind:"); | ||||
|         log.info("    Fraudulent: {}", getSession().getUnbind().getFraudulent()); | ||||
|  | ||||
|         return this; | ||||
|     } | ||||
|   | ||||
| @@ -61,7 +61,7 @@ public class NotificationConfig { | ||||
|     public void build() { | ||||
|         log.info("--- Notification config ---"); | ||||
|         log.info("Handlers:"); | ||||
|         handler.forEach((k, v) -> log.info("\t{}: {}", k, v)); | ||||
|         handler.forEach((k, v) -> log.info("  {}: {}", k, v)); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -20,9 +20,12 @@ | ||||
|  | ||||
| package io.kamax.mxisd.crypto; | ||||
|  | ||||
| import io.kamax.matrix.crypto.*; | ||||
| import io.kamax.mxisd.config.KeyConfig; | ||||
| import io.kamax.mxisd.config.ServerConfig; | ||||
| import io.kamax.mxisd.crypto.ed25519.Ed25519KeyManager; | ||||
| import io.kamax.mxisd.crypto.ed25519.Ed25519SignatureManager; | ||||
| import io.kamax.mxisd.storage.crypto.FileKeyStore; | ||||
| import io.kamax.mxisd.storage.crypto.KeyStore; | ||||
| import io.kamax.mxisd.storage.crypto.MemoryKeyStore; | ||||
| import org.apache.commons.io.FileUtils; | ||||
| import org.apache.commons.lang3.StringUtils; | ||||
|  | ||||
| @@ -31,10 +34,10 @@ import java.io.IOException; | ||||
|  | ||||
| public class CryptoFactory { | ||||
|  | ||||
|     public static KeyManager getKeyManager(KeyConfig keyCfg) { | ||||
|         _KeyStore store; | ||||
|     public static Ed25519KeyManager getKeyManager(KeyConfig keyCfg) { | ||||
|         KeyStore store; | ||||
|         if (StringUtils.equals(":memory:", keyCfg.getPath())) { | ||||
|             store = new KeyMemoryStore(); | ||||
|             store = new MemoryKeyStore(); | ||||
|         } else { | ||||
|             File keyStore = new File(keyCfg.getPath()); | ||||
|             if (!keyStore.exists()) { | ||||
| @@ -45,14 +48,14 @@ public class CryptoFactory { | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             store = new KeyFileStore(keyCfg.getPath()); | ||||
|             store = new FileKeyStore(keyCfg.getPath()); | ||||
|         } | ||||
|  | ||||
|         return new KeyManager(store); | ||||
|         return new Ed25519KeyManager(store); | ||||
|     } | ||||
|  | ||||
|     public static SignatureManager getSignatureManager(KeyManager keyMgr, ServerConfig cfg) { | ||||
|         return new SignatureManager(keyMgr, cfg.getName()); | ||||
|     public static SignatureManager getSignatureManager(Ed25519KeyManager keyMgr) { | ||||
|         return new Ed25519SignatureManager(keyMgr); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
							
								
								
									
										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(); | ||||
|  | ||||
| } | ||||
							
								
								
									
										64
									
								
								src/main/java/io/kamax/mxisd/crypto/SignatureManager.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/main/java/io/kamax/mxisd/crypto/SignatureManager.java
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| /* | ||||
|  * mxisd - Matrix Identity Server Daemon | ||||
|  * Copyright (C) 2019 Kamax Sàrl | ||||
|  * | ||||
|  * https://www.kamax.io/ | ||||
|  * | ||||
|  * This program is free software: you can redistribute it and/or modify | ||||
|  * it under the terms of the GNU Affero General Public License as | ||||
|  * published by the Free Software Foundation, either version 3 of the | ||||
|  * License, or (at your option) any later version. | ||||
|  * | ||||
|  * This program is distributed in the hope that it will be useful, | ||||
|  * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|  * GNU Affero General Public License for more details. | ||||
|  * | ||||
|  * You should have received a copy of the GNU Affero General Public License | ||||
|  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
|  | ||||
| package io.kamax.mxisd.crypto; | ||||
|  | ||||
| import com.google.gson.JsonObject; | ||||
|  | ||||
| import java.nio.charset.StandardCharsets; | ||||
|  | ||||
| public interface SignatureManager { | ||||
|  | ||||
|     /** | ||||
|      * 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); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -62,7 +62,7 @@ public class DirectoryManager { | ||||
|         this.providers = new ArrayList<>(providers); | ||||
|  | ||||
|         log.info("Directory providers:"); | ||||
|         this.providers.forEach(p -> log.info("\t- {}", p.getClass().getName())); | ||||
|         this.providers.forEach(p -> log.info("  - {}", p.getClass().getName())); | ||||
|     } | ||||
|  | ||||
|     public UserDirectorySearchResult search(URI target, String accessToken, String query) { | ||||
|   | ||||
| @@ -20,11 +20,8 @@ | ||||
|  | ||||
| package io.kamax.mxisd.exception; | ||||
|  | ||||
| import java.util.Optional; | ||||
|  | ||||
| public class ConfigurationException extends RuntimeException { | ||||
|  | ||||
|     private String key; | ||||
|     private String detailedMsg; | ||||
|  | ||||
|     public ConfigurationException(String key) { | ||||
| @@ -40,8 +37,8 @@ public class ConfigurationException extends RuntimeException { | ||||
|         this.detailedMsg = detailedMsg; | ||||
|     } | ||||
|  | ||||
|     public Optional<String> getDetailedMessage() { | ||||
|         return Optional.ofNullable(detailedMsg); | ||||
|     public String getDetailedMessage() { | ||||
|         return detailedMsg; | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -25,8 +25,14 @@ import org.apache.http.HttpStatus; | ||||
|  | ||||
| public class NotAllowedException extends HttpMatrixException { | ||||
|  | ||||
|     public static final String ErrCode = "M_FORBIDDEN"; | ||||
|  | ||||
|     public NotAllowedException(int code, String s) { | ||||
|         super(code, ErrCode, s); | ||||
|     } | ||||
|  | ||||
|     public NotAllowedException(String s) { | ||||
|         super(HttpStatus.SC_FORBIDDEN, "M_FORBIDDEN", s); | ||||
|         super(HttpStatus.SC_FORBIDDEN, ErrCode, s); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -22,8 +22,12 @@ package io.kamax.mxisd.exception; | ||||
|  | ||||
| public class ObjectNotFoundException extends RuntimeException { | ||||
|  | ||||
|     public ObjectNotFoundException(String message) { | ||||
|         super(message); | ||||
|     } | ||||
|  | ||||
|     public ObjectNotFoundException(String type, String id) { | ||||
|         super(type + " with ID " + id + " does not exist"); | ||||
|         this(type + " with ID " + id + " does not exist"); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -22,6 +22,9 @@ package io.kamax.mxisd.http.io.identity; | ||||
|  | ||||
| import io.kamax.mxisd.lookup.SingleLookupReply; | ||||
|  | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
|  | ||||
| public class SingeLookupReplyJson { | ||||
|  | ||||
|     private String address; | ||||
| @@ -30,6 +33,7 @@ public class SingeLookupReplyJson { | ||||
|     private long not_after; | ||||
|     private long not_before; | ||||
|     private long ts; | ||||
|     private Map<String, Map<String, String>> signatures = new HashMap<>(); | ||||
|  | ||||
|     public SingeLookupReplyJson(SingleLookupReply reply) { | ||||
|         this.address = reply.getRequest().getThreePid(); | ||||
| @@ -64,4 +68,8 @@ public class SingeLookupReplyJson { | ||||
|         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.JsonObject; | ||||
| import io.kamax.matrix.json.GsonUtil; | ||||
| import io.kamax.mxisd.dns.ClientDnsOverwrite; | ||||
| import io.kamax.mxisd.exception.AccessTokenNotFoundException; | ||||
| import io.kamax.mxisd.exception.HttpMatrixException; | ||||
| import io.kamax.mxisd.exception.InternalServerError; | ||||
| import io.kamax.mxisd.proxy.Response; | ||||
| import io.kamax.mxisd.util.RestClientUtils; | ||||
| import io.undertow.server.HttpHandler; | ||||
| import io.undertow.server.HttpServerExchange; | ||||
| import io.undertow.server.handlers.form.FormData; | ||||
| import io.undertow.util.HttpString; | ||||
| import org.apache.commons.io.IOUtils; | ||||
| import org.apache.commons.lang.StringUtils; | ||||
| import org.apache.commons.lang3.StringUtils; | ||||
| import org.apache.http.Header; | ||||
| import org.apache.http.HeaderElement; | ||||
| import org.apache.http.client.methods.CloseableHttpResponse; | ||||
| import org.apache.http.client.methods.HttpPost; | ||||
| import org.apache.http.impl.client.CloseableHttpClient; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.io.UnsupportedEncodingException; | ||||
| import java.net.InetSocketAddress; | ||||
| import java.net.URI; | ||||
| import java.net.URLDecoder; | ||||
| import java.nio.charset.StandardCharsets; | ||||
| import java.util.Deque; | ||||
| import java.util.LinkedList; | ||||
| import java.util.Map; | ||||
| import java.util.Optional; | ||||
| import java.util.*; | ||||
|  | ||||
| public abstract class BasicHttpHandler implements HttpHandler { | ||||
|  | ||||
|     private transient final Logger log = LoggerFactory.getLogger(BasicHttpHandler.class); | ||||
|     private static final Logger log = LoggerFactory.getLogger(BasicHttpHandler.class); | ||||
|  | ||||
|     protected String getAccessToken(HttpServerExchange exchange) { | ||||
|         return Optional.ofNullable(exchange.getRequestHeaders().getFirst("Authorization")) | ||||
|                 .flatMap(v -> { | ||||
|                     if (!v.startsWith("Bearer ")) { | ||||
|                         return Optional.empty(); | ||||
|                     } | ||||
|  | ||||
|                     return Optional.of(v.substring("Bearer ".length())); | ||||
|                 }).filter(StringUtils::isNotEmpty) | ||||
|                 .orElseThrow(AccessTokenNotFoundException::new); | ||||
|     } | ||||
|  | ||||
|     protected String getRemoteHostAddress(HttpServerExchange exchange) { | ||||
|         return ((InetSocketAddress) exchange.getConnection().getPeerAddress()).getAddress().getHostAddress(); | ||||
| @@ -101,6 +120,24 @@ public abstract class BasicHttpHandler implements HttpHandler { | ||||
|         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) { | ||||
|         ex.getResponseHeaders().put(HttpString.tryFromString(name), value); | ||||
|     } | ||||
|  | ||||
|     protected void respond(HttpServerExchange ex, int statusCode, JsonElement bodyJson) { | ||||
|         respondJson(ex, statusCode, GsonUtil.get().toJson(bodyJson)); | ||||
|     } | ||||
| @@ -145,4 +182,34 @@ public abstract class BasicHttpHandler implements HttpHandler { | ||||
|         upstream.getHeaders().forEach((key, value) -> exchange.getResponseHeaders().addAll(HttpString.tryFromString(key), value)); | ||||
|         writeBodyAsUtf8(exchange, upstream.getBody()); | ||||
|     } | ||||
|  | ||||
|     protected void proxyPost(HttpServerExchange exchange, JsonObject body, CloseableHttpClient client, ClientDnsOverwrite dns) { | ||||
|         String target = dns.transform(URI.create(exchange.getRequestURL())).toString(); | ||||
|         log.info("Requesting remote: {}", target); | ||||
|         HttpPost req = RestClientUtils.post(target, GsonUtil.get(), body); | ||||
|  | ||||
|         exchange.getRequestHeaders().forEach(header -> { | ||||
|             header.forEach(v -> { | ||||
|                 String name = header.getHeaderName().toString(); | ||||
|                 if (!StringUtils.startsWithIgnoreCase(name, "content-")) { | ||||
|                     req.addHeader(name, v); | ||||
|                 } | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         try (CloseableHttpResponse res = client.execute(req)) { | ||||
|             exchange.setStatusCode(res.getStatusLine().getStatusCode()); | ||||
|             for (Header h : res.getAllHeaders()) { | ||||
|                 for (HeaderElement el : h.getElements()) { | ||||
|                     exchange.getResponseHeaders().add(HttpString.tryFromString(h.getName()), el.getValue()); | ||||
|                 } | ||||
|             } | ||||
|             res.getEntity().writeTo(exchange.getOutputStream()); | ||||
|             exchange.endExchange(); | ||||
|         } catch (IOException e) { | ||||
|             log.warn("Unable to make proxy call: {}", e.getMessage(), e); | ||||
|             throw new InternalServerError(e); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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,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.http.undertow.handler; | ||||
|  | ||||
| import io.undertow.server.HttpServerExchange; | ||||
|  | ||||
| public class OptionsHandler extends BasicHttpHandler { | ||||
|  | ||||
|     @Override | ||||
|     public void handleRequest(HttpServerExchange exchange) { | ||||
|         // no-op | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -27,7 +27,7 @@ import io.kamax.matrix.json.InvalidJsonException; | ||||
| import io.kamax.mxisd.exception.*; | ||||
| import io.undertow.server.HttpHandler; | ||||
| import io.undertow.server.HttpServerExchange; | ||||
| import org.apache.commons.lang.StringUtils; | ||||
| import org.apache.commons.lang3.StringUtils; | ||||
| import org.apache.http.HttpStatus; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| @@ -36,15 +36,22 @@ import java.time.Instant; | ||||
|  | ||||
| public class SaneHandler extends BasicHttpHandler { | ||||
|  | ||||
|     private static final Logger log = LoggerFactory.getLogger(SaneHandler.class); | ||||
|  | ||||
|     private static final String CorsOriginName = "Access-Control-Allow-Origin"; | ||||
|     private static final String CorsOriginValue = "*"; | ||||
|     private static final String CorsMethodsName = "Access-Control-Allow-Methods"; | ||||
|     private static final String CorsMethodsValue = "GET, POST, PUT, DELETE, OPTIONS"; | ||||
|     private static final String CorsHeadersName = "Access-Control-Allow-Headers"; | ||||
|     private static final String CorsHeadersValue = "Origin, X-Requested-With, Content-Type, Accept, Authorization"; | ||||
|  | ||||
|     public static SaneHandler around(HttpHandler h) { | ||||
|         return new SaneHandler(h); | ||||
|     } | ||||
|  | ||||
|     private transient final Logger log = LoggerFactory.getLogger(SaneHandler.class); | ||||
|     private final HttpHandler child; | ||||
|  | ||||
|     private HttpHandler child; | ||||
|  | ||||
|     public SaneHandler(HttpHandler child) { | ||||
|     private SaneHandler(HttpHandler child) { | ||||
|         this.child = child; | ||||
|     } | ||||
|  | ||||
| @@ -56,6 +63,11 @@ public class SaneHandler extends BasicHttpHandler { | ||||
|             exchange.dispatch(this); | ||||
|         } else { | ||||
|             try { | ||||
|                 // CORS headers as per spec | ||||
|                 putHeader(exchange, CorsOriginName, CorsOriginValue); | ||||
|                 putHeader(exchange, CorsMethodsName, CorsMethodsValue); | ||||
|                 putHeader(exchange, CorsHeadersName, CorsHeadersValue); | ||||
|  | ||||
|                 child.handleRequest(exchange); | ||||
|             } catch (IllegalArgumentException e) { | ||||
|                 respond(exchange, HttpStatus.SC_BAD_REQUEST, GsonUtil.makeObj("error", e.getMessage())); | ||||
| @@ -83,9 +95,9 @@ public class SaneHandler extends BasicHttpHandler { | ||||
|                 handleException(exchange, e); | ||||
|             } catch (InternalServerError e) { | ||||
|                 if (StringUtils.isNotBlank(e.getInternalReason())) { | ||||
|                     log.error("Reference #{} - {}", e.getReference(), e.getInternalReason()); | ||||
|                     log.error("Transaction #{} - {}", e.getReference(), e.getInternalReason()); | ||||
|                 } else { | ||||
|                     log.error("Reference #{}", e); | ||||
|                     log.error("Transaction #{}", e); | ||||
|                 } | ||||
|  | ||||
|                 handleException(exchange, e); | ||||
| @@ -99,14 +111,11 @@ public class SaneHandler extends BasicHttpHandler { | ||||
|                 respond(exchange, e.getStatus(), buildErrorBody(exchange, e.getErrorCode(), e.getError())); | ||||
|             } catch (RuntimeException e) { | ||||
|                 log.error("Unknown error when handling {}", exchange.getRequestURL(), e); | ||||
|                 respond(exchange, HttpStatus.SC_INTERNAL_SERVER_ERROR, buildErrorBody(exchange, | ||||
|                         "M_UNKNOWN", | ||||
|                         StringUtils.defaultIfBlank( | ||||
|                                 e.getMessage(), | ||||
|                                 "An internal server error occurred. If this error persists, please contact support with reference #" + | ||||
|                                         Instant.now().toEpochMilli() | ||||
|                         ) | ||||
|                 )); | ||||
|                 String message = e.getMessage(); | ||||
|                 if (StringUtils.isBlank(message)) { | ||||
|                     message = "An internal server error occurred. Contact your administrator with reference Transaction #" + Instant.now().toEpochMilli(); | ||||
|                 } | ||||
|                 respond(exchange, HttpStatus.SC_INTERNAL_SERVER_ERROR, buildErrorBody(exchange, "M_UNKNOWN", message)); | ||||
|             } finally { | ||||
|                 exchange.endExchange(); | ||||
|             } | ||||
|   | ||||
| @@ -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; | ||||
|  | ||||
| import io.kamax.mxisd.crypto.KeyManager; | ||||
| import io.kamax.mxisd.crypto.KeyType; | ||||
| import io.kamax.mxisd.http.IsAPIv1; | ||||
| import io.undertow.server.HttpServerExchange; | ||||
| import org.slf4j.Logger; | ||||
| @@ -31,11 +33,19 @@ public class EphemeralKeyIsValidHandler extends KeyIsValidHandler { | ||||
|  | ||||
|     private transient final Logger log = LoggerFactory.getLogger(EphemeralKeyIsValidHandler.class); | ||||
|  | ||||
|     private KeyManager mgr; | ||||
|  | ||||
|     public EphemeralKeyIsValidHandler(KeyManager mgr) { | ||||
|         this.mgr = mgr; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void handleRequest(HttpServerExchange exchange) { | ||||
|         log.warn("Ephemeral key was requested but no ephemeral key are generated, replying not valid"); | ||||
|         // FIXME process + correctly in query parameter handling | ||||
|         String pubKey = getQueryParameter(exchange, "public_key").replace(" ", "+"); | ||||
|         log.info("Validating ephemeral public key {}", pubKey); | ||||
|  | ||||
|         respondJson(exchange, invalidKey); | ||||
|         respondJson(exchange, mgr.isValid(KeyType.Ephemeral, pubKey) ? validKey : invalidKey); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -21,18 +21,20 @@ | ||||
| package io.kamax.mxisd.http.undertow.handler.identity.v1; | ||||
|  | ||||
| import com.google.gson.JsonObject; | ||||
| import io.kamax.matrix.crypto.KeyManager; | ||||
| import io.kamax.mxisd.exception.BadRequestException; | ||||
| import io.kamax.mxisd.crypto.GenericKeyIdentifier; | ||||
| import io.kamax.mxisd.crypto.KeyManager; | ||||
| import io.kamax.mxisd.crypto.KeyType; | ||||
| import io.kamax.mxisd.http.IsAPIv1; | ||||
| import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; | ||||
| import io.undertow.server.HttpServerExchange; | ||||
| import org.apache.commons.lang3.StringUtils; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
|  | ||||
| public class KeyGetHandler extends BasicHttpHandler { | ||||
|  | ||||
|     public static final String Key = "key"; | ||||
|     public static final String Path = IsAPIv1.Base + "/pubkey/{key}"; | ||||
|     public static final String Path = IsAPIv1.Base + "/pubkey/{" + Key + "}"; | ||||
|  | ||||
|     private transient final Logger log = LoggerFactory.getLogger(KeyGetHandler.class); | ||||
|  | ||||
| @@ -45,17 +47,17 @@ public class KeyGetHandler extends BasicHttpHandler { | ||||
|     @Override | ||||
|     public void handleRequest(HttpServerExchange exchange) { | ||||
|         String key = getQueryParameter(exchange, Key); | ||||
|         String[] v = key.split(":", 2); | ||||
|         String keyType = v[0]; | ||||
|         int keyId = Integer.parseInt(v[1]); | ||||
|  | ||||
|         if (!"ed25519".contentEquals(keyType)) { | ||||
|             throw new BadRequestException("Invalid algorithm: " + keyType); | ||||
|         if (StringUtils.isBlank(key)) { | ||||
|             throw new IllegalArgumentException("Key ID cannot be empty or blank"); | ||||
|         } | ||||
|  | ||||
|         log.info("Key {}:{} was requested", keyType, keyId); | ||||
|         String[] v = key.split(":", 2); // Maybe use regex? | ||||
|         String keyAlgo = v[0]; | ||||
|         String keyId = v[1]; | ||||
|  | ||||
|         log.info("Key {}:{} was requested", keyAlgo, keyId); | ||||
|         JsonObject obj = new JsonObject(); | ||||
|         obj.addProperty("public_key", mgr.getPublicKeyBase64(keyId)); | ||||
|         obj.addProperty("public_key", mgr.getPublicKeyBase64(new GenericKeyIdentifier(KeyType.Regular, keyAlgo, keyId))); | ||||
|         respond(exchange, obj); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -20,10 +20,10 @@ | ||||
|  | ||||
| package io.kamax.mxisd.http.undertow.handler.identity.v1; | ||||
|  | ||||
| import io.kamax.matrix.crypto.KeyManager; | ||||
| import io.kamax.mxisd.crypto.KeyManager; | ||||
| import io.kamax.mxisd.crypto.KeyType; | ||||
| import io.kamax.mxisd.http.IsAPIv1; | ||||
| import io.undertow.server.HttpServerExchange; | ||||
| import org.apache.commons.lang.StringUtils; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
|  | ||||
| @@ -41,12 +41,11 @@ public class RegularKeyIsValidHandler extends KeyIsValidHandler { | ||||
|  | ||||
|     @Override | ||||
|     public void handleRequest(HttpServerExchange exchange) { | ||||
|         String pubKey = getQueryParameter(exchange, "public_key"); | ||||
|         // FIXME process + correctly in query parameter handling | ||||
|         String pubKey = getQueryParameter(exchange, "public_key").replace(" ", "+"); | ||||
|         log.info("Validating public key {}", pubKey); | ||||
|  | ||||
|         // TODO do in manager | ||||
|         boolean valid = StringUtils.equals(pubKey, mgr.getPublicKeyBase64(mgr.getCurrentIndex())); | ||||
|         respondJson(exchange, valid ? validKey : invalidKey); | ||||
|         respondJson(exchange, mgr.isValid(KeyType.Regular, pubKey) ? validKey : invalidKey); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -20,94 +20,36 @@ | ||||
|  | ||||
| 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.io.identity.SuccessStatusJson; | ||||
| import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; | ||||
| 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.apache.commons.lang.StringUtils; | ||||
| import org.apache.commons.lang3.StringUtils; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.net.MalformedURLException; | ||||
| import java.net.URL; | ||||
|  | ||||
| public class SessionValidateHandler extends BasicHttpHandler { | ||||
| public abstract class SessionValidateHandler extends BasicHttpHandler { | ||||
|  | ||||
|     public static final String Path = IsAPIv1.Base + "/validate/{medium}/submitToken"; | ||||
|  | ||||
|     private transient final Logger log = LoggerFactory.getLogger(SessionValidateHandler.class); | ||||
|  | ||||
|     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.srvCfg = srvCfg; | ||||
|         this.viewCfg = viewCfg; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void handleRequest(HttpServerExchange exchange) { | ||||
|         String medium = getQueryParameter(exchange, "medium"); | ||||
|         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; | ||||
|             } | ||||
|     protected ValidationResult handleRequest(String sid, String secret, String token) { | ||||
|         if (StringUtils.isEmpty(sid)) { | ||||
|             throw new IllegalArgumentException("sid is not set or is empty"); | ||||
|         } | ||||
|  | ||||
|         if (isHtmlRequest) { | ||||
|             handleHtmlRequest(exchange, medium, sid, secret, token); | ||||
|         } else { | ||||
|             handleJsonRequest(exchange, sid, secret, token); | ||||
|         if (StringUtils.isEmpty(secret)) { | ||||
|             throw new IllegalArgumentException("client secret is not set or is empty"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void handleHtmlRequest(HttpServerExchange exchange, String medium, String sid, String secret, String 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)); | ||||
|         return mgr.validate(sid, secret, token); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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); | ||||
|     } | ||||
|  | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user