Compare commits

...

59 Commits

Author SHA1 Message Date
Max Dor
ee21f051fb Merge branch 'to-v1.4' 2019-04-09 14:50:45 +02:00
Max Dor
6cc17abf2c Further document new features 2019-04-09 12:06:13 +02:00
Max Dor
a7b5accd75 Adapt AS doc to new format and capabilities 2019-04-09 02:50:58 +02:00
Max Dor
6bb0c93f57 Fix typo 2019-04-05 21:56:05 +02:00
Max Dor
9abdcc15ba Clarify specifics about synapse identity store 2019-04-03 00:50:48 +02:00
Max Dor
eb903bf226 Document new 3PID invite expiration feature 2019-04-03 00:44:30 +02:00
Max Dor
1cbb0a135b Add doc about new registration control feature 2019-04-02 11:56:48 +02:00
Joshua M. Boniface
1587103c0a Add Section and Priority Debian control fields (#150) 2019-04-01 03:01:10 +02:00
Max Dor
838d79ae15 Remove mention to the community Identity room 2019-03-11 19:46:23 +01:00
Max Dor
96c47ecf76 Merge pull request #143 from c7hm4r/patch-1
Fix typo in example configuration
2019-03-07 21:11:40 +01:00
Christoph Müller
c5cea933a4 Fix typo in example configuration 2019-03-07 21:07:40 +01:00
Max Dor
57c7e4a91d Show signatures into admin lookup queries 2019-03-04 02:12:55 +01:00
Max Dor
1dce59a02e Add lookup and invite commands to the admin AS interface 2019-03-04 00:02:13 +01:00
Max Dor
de840b9d00 Skeleton for modular AS admin command processing 2019-03-03 16:39:58 +01:00
Max Dor
53c85d2248 Package/Class refactoring (no-op) 2019-03-03 03:44:38 +01:00
Max Dor
254dc5684f Add mechanisms for 3PID invite expiration and AS integration
- Integration with AS and a fallback user to decline expired invites (#120)
- Rework of the AS feature to make it more independent/re-usable
- Skeleton for admin interface via bot to manage invites (#138)
2019-03-02 03:21:29 +01:00
Max Dor
de92e98f7d Save work in progress 2019-03-01 17:51:33 +01:00
Max Dor
d5f9137056 split into app svc processor 2019-03-01 15:58:37 +01:00
Max Dor
1307e3aa43 Add missing javadoc 2019-03-01 15:18:47 +01:00
Max Dor
dfedde0df6 Improve crypto
- Re-organize packages to be consistent
- Add Key store tests
2019-03-01 15:16:19 +01:00
Max Dor
93bd7354c2 Improve Authentication doc 2019-03-01 12:42:13 +01:00
Max Dor
c302789898 Add mechanism for 3PID invites expiration (#120) 2019-03-01 06:51:18 +01:00
Max Dor
96155c1876 Improving logging 2019-03-01 01:12:02 +01:00
Max Dor
95ee328281 Block custom internal endpoint that should never be called
- Is not spec'd
- Will not be spec'd
- Is 100% internal as per its authors
2019-02-25 14:06:32 +01:00
Max Dor
72a1794cc3 Skeleton for 3PID registration policies (#130) 2019-02-18 23:08:50 +01:00
Max Dor
37ddd0e588 Talk about server.name in the example config 2019-02-17 03:22:48 +01:00
Max Dor
4d63bba251 Add version in jar
- Cli argument
- In HTTP client
- /version endpoint
2019-02-17 02:08:50 +01:00
Max Dor
aadfae2965 Skeleton for invitation policies (#130) 2019-02-17 02:08:50 +01:00
Max Dor
2f7e5e4025 Fix migration in case of empty dir 2019-02-17 02:08:50 +01:00
Max Dor
77dc75d383 Basic check for pending invite when requesting token on registration 2019-02-17 02:08:50 +01:00
Max Dor
f3b528d1ba Store ephemeral key in invite and add support for /sign-ed25519 2019-02-17 02:08:50 +01:00
Max Dor
91e5e08e70 Support for all key types 2019-02-17 02:08:50 +01:00
Max Dor
acd8c7d7c5 Skeleton for full support of all key types 2019-02-17 02:08:50 +01:00
Max Dor
249cc0ea92 Improve troubleshooting doc/flows
- Use better wording for unknown server error
- Add basic troubleshooting doc
2019-02-17 02:06:13 +01:00
Max Dor
99697d7c75 Various doc fixes and improvements 2019-02-14 00:39:33 +01:00
Max Dor
e133e120d7 Fix Exec store breakage following change to new config format 2019-02-13 21:08:56 +01:00
Max Dor
e39d6bfa10 Better handling of YAML->Java object config processing 2019-02-13 21:08:35 +01:00
Max Dor
217bc423ed Fix edge case of error when parsing valid config for directory 2019-02-13 20:19:26 +01:00
Max Dor
8f0654c34e Fix oversight in potentially printing credentials to log 2019-02-13 12:40:01 +01:00
Max Dor
8afdb3ed83 Improve feedback in case of parsing error in config file 2019-02-11 03:18:50 +01:00
Max Dor
bd4ccbc5e5 Fix some edge cases configuration parsing
- Optional in getter but not in setter seems problematic
- Document config parsing better
- Properly handle empty values in REST Profile so no HTTP call is made
- Possibly related to #113
2019-02-11 02:56:02 +01:00
Max Dor
6d1c6ed109 Last cosmetic changes for v1.3.0 2019-02-10 20:41:40 +01:00
Max Dor
1619f5311c Add email verification notification test (/requestToken) 2019-02-09 15:18:06 +01:00
Max Dor
6fa36ea092 Add missing header 2019-02-07 01:39:10 +01:00
Max Dor
471e06536b Improve logging 2019-02-07 01:35:43 +01:00
Max Dor
3a6b75996c Use a proper HTTP client when discovering federated IS to avoid 4xx's 2019-02-06 23:23:40 +01:00
Max Dor
566e4f3137 Correctly handle 3PID notification revamping (forgotten code) 2019-02-06 22:27:42 +01:00
Max Dor
a4c18dee5d Handle possibly trailing slashes for older versions of mxisd 2019-02-06 19:55:22 +01:00
Max Dor
8d6850d346 Link to targeted setups in main README 2019-02-06 04:03:33 +01:00
Max Dor
67bc18af7d Improve docs 2019-02-06 03:53:42 +01:00
Max Dor
5c660fdcaf Add forgotten CORS headers from Spring port 2019-02-05 19:09:47 +01:00
Max Dor
fbbafeb769 Cache processing of bulk lookups and de-dup concurrent requests 2019-02-04 06:04:39 +01:00
Max Dor
559f6a7401 Fix docs 2019-02-04 06:03:15 +01:00
Max Dor
3bebb33147 Revamp 3PID sessions
- Fix #93
- Fix #98
2019-02-04 05:26:33 +01:00
Max Dor
3e240fe34d Improve fraudulent unbind notification 2019-02-01 15:41:44 +01:00
Max Dor
635f6fdbe7 Implementation for blocking fraudulent 3PID /unbind attempts 2019-02-01 02:34:52 +01:00
Max Dor
4237eeb3b6 Skeleton for blocking fraudulent 3PID /unbind attempts 2019-01-30 00:29:51 +01:00
Max Dor
a0e91e7896 Use proper return codes for session errors 2019-01-30 00:28:55 +01:00
Max Dor
aab0b86646 Talk about projects using mxisd under the hood 2019-01-23 18:50:00 +01:00
173 changed files with 6243 additions and 2281 deletions

View File

@@ -8,18 +8,20 @@ mxisd - Federated Matrix Identity Server
- [Getting Started](#getting-started)
- [Support](#support)
- [Contribute](#contribute)
- [Powered by mxisd](#powered-by-mxisd)
- [FAQ](#faq)
- [Contact](#contact)
# 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.
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:
@@ -32,15 +34,15 @@ users. 3PIDs can be anything that uniquely and globally identify a user, like:
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,
@@ -51,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,
@@ -66,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:
@@ -96,6 +102,11 @@ You can contribute as an organisation/corporation by:
maintained regularly and you get direct access to the support team.
- Sponsoring new features or bug fixes. [Get in touch](#contact) so we can discuss it further.
# Powered by mxisd
The following projects use mxisd under the hood for some or all their features. Check them out!
- [matrix-docker-ansible-deploy](https://github.com/spantaleev/matrix-docker-ansible-deploy)
- [matrix-register-bot](https://github.com/krombel/matrix-register-bot)
# FAQ
See the [dedicated document](docs/faq.md)

View File

@@ -146,12 +146,23 @@ 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'
testCompile 'com.unboundid:unboundid-ldapsdk:4.0.9'
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 {

View File

@@ -38,12 +38,12 @@ matrix:
- 'https://other1.example.org'
- 'https://other2.example.org'
```
Create a list under the label `root` containing a single Identity server, `https://matrix.org`
Create a list under the label `myOtherServers` containing two Identity servers: `https://other1.example.org` and `https://other2.example.org`.
## Server
- `server.name`: Public hostname of mxisd, if different from the Matrix domain.
- `server.port`: HTTP port to listen on (unencrypted)
- `server.publicUrl`: Defaults to `https://${server.name}`
- `server.publicUrl`: Defaults to `https://{server.name}`
## Storage
### SQLite

View File

@@ -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.

View File

@@ -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

View File

@@ -1,65 +1,65 @@
# 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 | `true` | Globally enable/disable the feature |
| `user.main` | string | No | `mxisd` | Localpart for the main appservice user |
| `endpoint.toHS.url` | string | Yes | *None* | Base URL to the Homeserver |
| `endpoint.toHS.token` | string | Yes | *None* | Token to use when sending requests to the Homeserver |
| `endpoint.toAS.url` | string | Yes | *None* | Base URL to mxisd from the Homeserver |
| `endpoint.toAS.token` | string | Yes | *None* | Token for the Homeserver to use when sending requests to mxisd |
#### Example
```yaml
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:
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 +68,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*

View File

@@ -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.

View File

@@ -1,7 +1,12 @@
# Identity
**WARNING**: This document is incomplete and can be missleading.
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)

View 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.

View File

@@ -6,22 +6,34 @@
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.
---
If you would like a more fully integrated setup out of the box, the [matrix-docker-ansible-deploy](https://github.com/spantaleev/matrix-docker-ansible-deploy)
project provides a turn-key full-stack solution, including LDAP and the various mxisd features enabled and ready.
We work closely with the project owner so the latest mxisd version is always supported.
If you choose to use it, this Getting Started guide is not applicable - See the project documentation. You may then
directly go to the [Next steps](#next-steps).
## Preparation
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
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
@@ -42,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.)
@@ -74,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
@@ -98,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;
@@ -121,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`.
@@ -139,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

View File

@@ -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

View File

@@ -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:
exec:
enabled: true
token:
mxid: '{matrixId}'
auth:
token:
localpart: '{username}'
command: '/path/to/executable'
args:
- '-u'
- '{username}'
exec.placeholder.env:
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]
output:
type: 'json'
exit:
success:
- 0
- 128
failure:
- 1
- 129
```
With:
- The Identity store enabled for all features

View File

@@ -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

View File

@@ -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
@@ -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`

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,6 +1,4 @@
# Email notifications - SMTP connector
Enabled by default.
Connector ID: `smtp`
## Configuration

View File

@@ -1,6 +1,4 @@
# SMS notifications - Twilio connector
Enabled by default.
Connector ID: `twilio`
## Configuration

View File

@@ -26,14 +26,14 @@ 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>
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>
remote:
subject: <Subject of the email notification sent for remote 3PID sessions>
unbind:
fraudulent:
subject: <Subject of the email notification sent for potentially fraudulent 3PID unbinds>
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>
html: <Path to file containing the raw text part of the email. Do not set to not use one>
```

View File

@@ -18,9 +18,9 @@ threepid:
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:
fraudulent: '/path/to/unbind-fraudulent-template.eml'
generic:
matrixId: '/path/to/mxid-invite-template.eml'
```
@@ -51,7 +51,7 @@ This template is used when someone is invited into a room using an email address
| `%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
### 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.
@@ -59,17 +59,5 @@ allows at least local sessions.
| 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. |
| `%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. |

View File

@@ -8,81 +8,27 @@ Pseudo-configuration to illustrate the structure:
# DO NOT COPY/PASTE THIS IN YOUR CONFIGURATION
view:
session:
local:
onTokenSubmit:
success: '/path/to/session/local/tokenSubmitSuccess-page.html'
failure: '/path/to/session/local/tokenSubmitFailure-page.html'
localRemote:
onTokenSubmit:
success: '/path/to/session/localRemote/tokenSubmitSuccess-page.html'
failure: '/path/to/session/local/tokenSubmitFailure-page.html'
remote:
onRequest:
success: '/path/to/session/remote/requestSuccess-page.html'
failure: '/path/to/session/remote/requestFailure-page.html'
onCheck:
success: '/path/to/session/remote/checkSuccess-page.html'
failure: '/path/to/session/remote/checkFailure-page.html'
success: '/path/to/session/tokenSubmitSuccess-page.html'
failure: '/path/to/session/tokenSubmitFailure-page.html'
# CONFIGURATION EXAMPLE
# DO NOT COPY/PASTE THIS IN YOUR CONFIGURATION
```
3PID session are divided into three config sections:
- `local` for local-only 3PID sessions
- `localRemote` for local 3PID sessions that can also be turned into remote sessions, if the user so desires
- `remote` for remote-only 3PID sessions
Each section contains a sub-key per support event. Finally, a `success` and `failure` key is available depending on the
outcome of the request.
## Local
### onTokenSubmit
`view.session`:
This is triggered when a user submit a validation token for a 3PID session. It is typically visited when clicking the
link in a validation email.
The template should typically inform the user that the validation was successful and to go back in their Matrix client
to finish the validation process.
to finish the validation process, or that the validation failed.
#### Placeholders
Two configuration keys are available that accept paths to HTML templates:
- `success`
- `failure`
## Placeholders
### Success
No object/placeholder are currently available.
## Local & Remote
### onTokenSubmit
This is triggered when a user submit a validation token for a 3PID session. It is typically visited when clicking the
link in a validation email.
The template should typically inform the user that their 3PID address will not yet be publicly/globally usable. In case
they want to make it, they should start a Remote 3PID session with a given link or that they can go back to their Matrix
client if they do not wish to proceed any further.
#### Placeholders
##### Success
`<a href="${remoteSessionLink}">text</a>` can be used to display the link to start a Remote 3PID session.
##### Failure
No object/placeholder are currently available.
## Remote
### onRequest
This is triggered when a user starts a Remote 3PID session, usually from a link produced in the `local.onTokenSubmit`
view or in a remote-only 3PID notification.
The template should typically inform the user that the remote creation was successful, followed the instructions sent by
the remote Identity server and, once that is done, click a link to validate the session.
#### Placeholders
##### Success
`<a href="${checkLink}">text</a>` can be used to display the link to validate the Remote 3PID session.
##### Failure
No object/placeholder are currently available.
### onCheck
This is triggered when a user attempts to inform the Identity server that the Remote 3PID session has been validated
with the remote Identity server.
The template should typically inform the user that the validation was successful and to go back in their Matrix client
to finish the validation process.
#### Placeholders
### Failure
No object/placeholder are currently available.

View File

@@ -1,9 +1,8 @@
# 3PID Sessions
- [Overview](#overview)
- [Purpose](#purpose)
- [Restrictions](#restrictions)
- [Bindings](#bindings)
- [Federation](#federation)
- [3PID scope](#3pid-scope)
- [Session scope](#session-scope)
- [Notifications](#notifications)
- [Email](#email)
- [Phone numbers](#msisdn-(phone-numbers))
@@ -11,28 +10,39 @@
- [Configuration](#configuration)
- [Web views](#web-views)
- [Scenarios](#scenarios)
- [Default](#default)
- [Local sessions only](#local-sessions-only)
- [Remote sessions only](#remote-sessions-only)
- [Sessions disabled](#sessions-disabled)
## Overview
When adding an email, a phone number or any other kind of 3PID (Third-Party Identifier) in a Matrix client,
the identity server is contacted to validate the 3PID.
To validate the 3PID the identity server sends a message to the 3PID (e.g. an
email) with a hyperlink back to a web-page managed by the identity server to
confirm ownership of the 3PID.
To validate the 3PID, the identity server creates a session associated with a secret token. That token is sent via a message
to the 3PID (e.g. an email) with a the necessary info so the user can submit them to the Identity Server, confirm ownership
of the 3PID.
Once this 3PID is validated, the Homeserver will publish the user Matrix ID on the Identity Server and
add this 3PID to the Matrix account which initiated the request.
Once this 3PID is validated, the Homeserver will request that the Identity Server links the provided user Matrix ID with
the 3PID session and finally add the 3PID to its own data store.
This serves two purposes:
- Add the 3PID as an administrative/login info for the Homeserver directly
- Publish, or *Bind*, the 3PID so it can be queried from Homeservers and clients when inviting someone in a room
- Links, called *Bind*, the 3PID so it can be queried from Homeservers and clients when inviting someone in a room
by a 3PID, allowing it to be resolved to a Matrix ID.
## Federation
## Restrictions
### Bindings
mxisd does not store bindings directly. While a user can see its email, phone number or any other 3PID in its
settings/profile, it does **NOT** mean it is published/saved anywhere or can be used to invite/search the user.
Identity stores are the ones holding such data, irrelevant if a user added a 3PID to their profile. When queried for
bindings, mxisd will query Identity stores which are responsible to store this kind of information.
Therefore, by default, any 3PID added to a user profile which is NOT within a configured and enabled Identity backend
will simply not be usable for search or invites, **even on the same Homeserver!**
To have such 3PID bindings available for search and invite queries on synapse, use its dedicated
[Identity store](../../stores/synapse.md).
### Federation
In a federated set up, identity servers must cooperate to find the Matrix ID associated with a 3PID.
Federation is based on the principle that each server is responsible for its own (dns) domain.
@@ -43,61 +53,15 @@ Example: a user from Homeserver `example.org` adds an email `john@example.com`.
Federated identity servers would try to find the identity server at `example.com` and ask it for the Matrix ID of associated with `john@example.com`.
Nevertheless, Matrix users might add 3PIDs that are not associated to a domain, for example telephone numbers.
Or they might even add 3PIDs associated to a different domain (such as an email address hosted by gmail).
Such 3PIDs cannot be resolved in a federated way.
Or they might even add 3PIDs associated to a different domain (such as an email address hosted by Gmail).
Such 3PIDs cannot be resolved in a federated way and will not be found from other servers.
Example: a user from Homeserver `example.org` adds an email `john@gmail.com`.
If a federated lookup was performed, Identity servers would try to find the 3PID bind at the `gmail.com` server, and
not `example.org`.
In order to resolve such 3PIDs, i.e. 3PIDs that cannot be resolved in a Federated way, an identity server can be configured such that
- 3PIDs that cannot be resolved locally or using federation, are fowarded to another global identity server.
- registration of new 3PIDs that cannot be looked up in a federated fashion, is forwarded to another global identity server.
By forwarding a 3PIDs registration the identity creates a *Remote session* and *Remote bind*, effectively starting a new 3PID session with another Identity server on
behalf of the user.
To ensure lookup works consistency within the current Matrix network, the central Matrix.org Identity Server should be
used to store *remote* sessions and binds.
However, at the time of writing, the Matrix specification and the central Matrix.org servers do not allow to remote a 3PID bind.
This means that once a 3PID is published (email, phone number, etc.), it cannot be easily removed
and would require contacting the Matrix.org administrators for each bind individually.
This poses a privacy, control and security concern, especially for groups/corporations that want to keep a tight control
on where such identifiers can be made publicly visible.
To ensure full control, validation management relies on two concepts:
- The scope of 3PID being validated
- The scope of 3PID sessions that should be possible/offered
### 3PID scope
3PID can either be scoped as local or remote.
Local means that they can be looked up using federation and that such a federation call would end up on the local
Identity Server.
Remote means that they cannot be lookup using federation or that a federation call would not end up on the local
Identity Server.
Email addresses can either be local or remote 3PID, depending on the domain. If the address is one from the configured
domain in the Identity server, it will be scoped as local. If it is from another domain, it will be as remote.
Phone number can only be scoped as remote, since there is currently no way to perform DNS queries that would lead back
to the Identity server who validated the phone number.
### Session scope
Sessions can be scoped as:
- Local only - validate 3PIDs directly, do not allow the creation of 3PID sessions on a remote Identity server.
- Local and Remote - validate 3PIDs directly, offer users to option to also validate and bind 3PID on another server.
- Remote only - validate and bind 3PIDs on another server, no validation or bind done locally.
---
**IMPORTANT NOTE:** mxisd does not store bindings directly. While a user can see its email, phone number or any other
3PID in its settings/profile, it does **NOT** mean it is published anywhere and can be used to invite/search the user.
Identity stores are the ones holding such data.
If you still want added arbitrary 3PIDs to be discoverable on a synapse Homeserver, use the corresponding [Identity store](../../stores/synapse.md).
See the [Scenarios](#scenarios) for more info on how and why.
As mxisd is built for self-hosted use cases, mainly for orgs/corps, this is usually not a problem for emails.
Sadly, there is currently no mechanism to make this work for phone numbers.
## Notifications
3PIDs are validated by sending a pre-formatted message containing a token to that 3PID address, which must be given to the
@@ -126,47 +90,36 @@ Connectors:
## Usage
### Configuration
The following example of configuration (incomplete extract) shows which items are relevant for 3PID sessions.
The following example of configuration shows which items are relevant for 3PID sessions.
**IMPORTANT:** Most configuration items shown have default values and should not be included in your own configuration
file unless you want to specifically overwrite them.
```yaml
# CONFIGURATION EXAMPLE
# DO NOT COPY/PASTE THIS IN YOUR CONFIGURATION
# DO NOT COPY/PASTE AS-IS IN YOUR CONFIGURATION
session:
policy:
validation:
enabled: true
forLocal:
enabled: true
toLocal: true
toRemote:
enabled: true
server: 'configExample' # Not to be included in config! Already present in default config!
forRemote:
enabled: true
toLocal: true
toRemote:
enabled: true
server: 'configExample' # Not to be included in config! Already present in default config!
# DO NOT COPY/PASTE THIS IN YOUR CONFIGURATION
unbind:
fraudulent:
sendWarning: true
# DO NOT COPY/PASTE AS-IS IN YOUR CONFIGURATION
# CONFIGURATION EXAMPLE
```
`session.policy.validation` is the core configuration to control what users configured to use your Identity server
are allowed to do in terms of 3PID sessions.
are allowed to do in terms of 3PID sessions. The policy has a global on/off switch for 3PID sessions using `.enabled`
The policy has a global on/off switch for 3PID sessions using `.enabled`
It is also divided into two sections: `forLocal` and `forRemote` which refers to the 3PID scopes.
---
Each scope is divided into three parts:
- global on/off switch for 3PID sessions using `.enabled`
- `toLocal` allowing or not local 3PID session validations
- `toRemote` allowing or not remote 3PID session validations and to which server such sessions should be sent.
`.server` takes a Matrix Identity server list label. Only the first server in the list is currently used.
`unbind.fraudulent` controls warning notifications if an illegal/fraudulent 3PID removal is attempted on the Identity server.
This is directly related to synapse disregard for privacy and new GDPR laws in Europe in an attempt to inform users about
potential privacy leaks.
If both `toLocal` and `toRemote` are enabled, the user will be offered to initiate a remote session once their 3PID
locally validated.
For more information, see the corresponding [synapse issue](https://github.com/matrix-org/synapse/issues/4540).
### Web views
Once a user click on a validation link, it is taken to the Identity Server validation page where the token is submitted.
@@ -177,107 +130,13 @@ See [the dedicated document](session-views.md)
on how to configure/customize/brand those pages to your liking.
### Scenarios
It is important to keep in mind that mxisd does not create bindings, irrelevant if a user added a 3PID to their profile.
Instead, when queried for bindings, mxisd will query Identity stores which are responsible to store this kind of information.
This has the side effect that any 3PID added to a user profile which is NOT within a configured and enabled Identity backend
will simply not be usable for search or invites, **even on the same Homeserver!**
mxisd does not store binds on purpose, as one of its primary goal is to ensure maximum compatibility with federation
and the rest of the Matrix ecosystem is preserved.
Nonetheless, because mxisd also aims at offering support for tight control over identity data, it is possible to have
such 3PID bindings available for search and invite queries on synapse with the corresponding [Identity store](../../stores/synapse.md).
See the [Local sessions only](#local-sessions-only) use case for more information on how to configure.
#### Default
By default, mxisd allows the following:
| | Local Session | Remote Session |
|-----------------|-------------------|----------------|
| **Local 3PID** | Yes | Yes, offered |
| **Remote 3PID** | No, Remote forced | Yes |
This is usually what people expect and will feel natural to users and does not involve further integration.
This allows to stay in control for e-mail addresses which domain matches your Matrix environment, still making them
discoverable with federation but not recorded in a 3rd party Identity server which is not under your control.
Users still get the possibility to publish globally their address if needed.
Other e-mail addresses and phone number will be redirected to remote sessions to ensure full compatibility with the Matrix
ecosystem and other federated servers.
#### Local sessions only
**NOTE:** This does not affect 3PID lookups (queries to find Matrix IDs). See [Federation](../../features/federation.md)
to disable remote lookup for those.
This configuration ensures maximum confidentiality and privacy.
Typical use cases:
- Private Homeserver, not federated
- Internal Homeserver without direct Internet access
- Custom product based on Matrix which does not federate
No 3PID will be sent to a remote Identity server and all validation will be performed locally.
On the flip side, people with *Remote* 3PID scopes will not be found from other servers.
Use the following values:
```yaml
session:
policy:
validation:
enabled: true
forLocal:
enabled: true
toLocal: true
toRemote:
enabled: false
forRemote:
enabled: true
toLocal: true
toRemote:
enabled: false
```
**IMPORTANT**: When using local-only mode and if you are using synapse, you will also need to enable its dedicated Identity
store if you want user searches and invites to work. To do so, see the [dedicated document](../../stores/synapse.md).
#### Remote sessions only
This configuration ensures all 3PID are made public for maximum compatibility and reach within the Matrix ecosystem, at
the cost of confidentiality and privacy.
Typical use cases:
- Public Homeserver
- Homeserver with registration enabled
Use the following values:
```yaml
session:
policy:
validation:
enabled: true
forLocal:
enabled: true
toLocal: false
toRemote:
enabled: true
forRemote:
enabled: true
toLocal: false
toRemote:
enabled: true
```
#### Sessions disabled
This configuration would disable 3PID session altogether, preventing users from adding emails and/or phone numbers to
their profiles.
This configuration would disable 3PID sessions altogether, preventing users from validating emails and/or phone numbers
and any subsequent actions that requires them, like adding them to their profiles.
This would be used if mxisd is also performing authentication for the Homeserver, typically with synapse and the
[REST password provider](https://github.com/kamax-io/matrix-synapse-rest-auth).
**This mode comes with several important restrictions:**
- This does not prevent users from removing 3PID from their profile. They would be unable to add them back!
- This prevents users from initiating remote session to make their 3PID binds globally visible
It is therefore recommended to not fully disable sessions but instead restrict specific set of 3PID and Session scopes.
[REST password provider](https://github.com/kamax-matrix/matrix-synapse-rest-auth), where 3PID mappings would be
auto-populated.
Use the following values to enable this mode:
```yaml

53
docs/troubleshooting.md Normal file
View File

@@ -0,0 +1,53 @@
# 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.
### 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).

View File

@@ -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:
@@ -100,12 +94,13 @@ threepid:
# SMTP port
port: 587
# TLS mode for the connection.
# STARTLS mode for the connection.
# SSL/TLS is currently not supported. See https://github.com/kamax-matrix/mxisd/issues/125
#
# 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
#
tls: 1

View File

@@ -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

View File

@@ -20,8 +20,7 @@
* 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;

View File

@@ -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,22 @@ 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 asNotFoundHandler = SaneHandler.around(new AsNotFoundHandler(m.getAs()));
HttpHandler storeInvHandler = SaneHandler.around(new StoreInviteHandler(m.getConfig().getServer(), m.getInvite(), m.getKeyManager()));
HttpHandler sessValidateHandler = SaneHandler.around(new SessionValidateHandler(m.getSession(), m.getConfig().getServer(), m.getConfig().getView()));
httpSrv = Undertow.builder().addHttpListener(m.getConfig().getServer().getPort(), "0.0.0.0").setHandler(Handlers.routing()
.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,40 +94,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(SessionTpidGetValidatedHandler.Path, SaneHandler.around(new SessionTpidGetValidatedHandler(m.getSession())))
.post(SessionTpidBindHandler.Path, SaneHandler.around(new SessionTpidBindHandler(m.getSession(), m.getInvitationManager())))
.get(RemoteIdentityAPIv1.SESSION_REQUEST_TOKEN, SaneHandler.around(new RemoteSessionStartHandler(m.getSession(), m.getConfig().getView())))
.get(RemoteIdentityAPIv1.SESSION_CHECK, SaneHandler.around(new RemoteSessionCheckHandler(m.getSession(), m.getConfig().getView())))
.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();
}

View File

@@ -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,17 @@ 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.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.session.SessionMananger;
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,45 +59,55 @@ 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 SessionMananger 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();
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());
signMgr = CryptoFactory.getSignatureManager(keyMgr);
clientDns = new ClientDnsOverwrite(cfg.getDns().getOverwrite());
FederationDnsOverwrite fedDns = new FederationDnsOverwrite(cfg.getDns().getOverwrite());
Synapse synapse = new Synapse(cfg.getSynapseSql());
synapse = new Synapse(cfg.getSynapseSql());
BridgeFetcher bridgeFetcher = new BridgeFetcher(cfg.getLookup().getRecursive().getBridge(), srvFetcher);
ServiceLoader.load(IdentityStoreSupplier.class).iterator().forEachRemaining(p -> p.accept(this));
@@ -102,11 +116,12 @@ public class Mxisd {
idStrategy = new RecursivePriorityLookupStrategy(cfg.getLookup(), ThreePidProviders.get(), bridgeFetcher);
pMgr = new ProfileManager(ProfileProviders.get(), clientDns, httpClient);
notifMgr = new NotificationManager(cfg.getNotification(), NotificationHandlers.get());
sessMgr = new SessionMananger(cfg.getSession(), cfg.getMatrix(), store, notifMgr, httpClient);
invMgr = new InvitationManager(cfg.getInvite(), store, idStrategy, signMgr, fedDns, notifMgr);
sessMgr = new SessionManager(cfg.getSession(), cfg.getMatrix(), store, notifMgr, idStrategy, httpClient);
invMgr = new InvitationManager(cfg, store, idStrategy, keyMgr, signMgr, fedDns, notifMgr, pMgr);
authMgr = new AuthManager(cfg, AuthProviders.get(), idStrategy, invMgr, clientDns, httpClient);
dirMgr = new DirectoryManager(cfg.getDirectory(), clientDns, httpClient, DirectoryProviders.get());
asHander = new AppSvcManager(cfg, store, pMgr, notifMgr, synapse);
regMgr = new RegistrationManager(cfg.getRegister(), httpClient, clientDns, invMgr);
asHander = new AppSvcManager(this);
}
public MxisdConfig getConfig() {
@@ -117,6 +132,10 @@ public class Mxisd {
return httpClient;
}
public ClientDnsOverwrite getClientDns() {
return clientDns;
}
public IRemoteIdentityServerFetcher getServerFetcher() {
return srvFetcher;
}
@@ -125,7 +144,7 @@ public class Mxisd {
return keyMgr;
}
public InvitationManager getInvitationManager() {
public InvitationManager getInvite() {
return invMgr;
}
@@ -137,7 +156,7 @@ public class Mxisd {
return authMgr;
}
public SessionMananger getSession() {
public SessionManager getSession() {
return sessMgr;
}
@@ -153,6 +172,10 @@ public class Mxisd {
return signMgr;
}
public RegistrationManager getReg() {
return regMgr;
}
public AppSvcManager getAs() {
return asHander;
}
@@ -161,6 +184,14 @@ public class Mxisd {
return notifMgr;
}
public IStorage getStore() {
return store;
}
public Synapse getSynapse() {
return synapse;
}
public void start() {
build();
}

View File

@@ -22,44 +22,64 @@ 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");
public static void main(String[] args) {
try {
MxisdConfig cfg = null;
Iterator<String> argsIt = Arrays.asList(args).iterator();
while (argsIt.hasNext()) {
String arg = argsIt.next();
if (StringUtils.equals("-c", arg)) {
if (StringUtils.equalsAny(arg, "-h", "--help", "-?", "--usage")) {
System.out.println("Available arguments:" + System.lineSeparator());
System.out.println(" -h, --help Show this help message");
System.out.println(" --version Print the version then exit");
System.out.println(" -c, --config Set the configuration file location");
System.out.println(" ");
System.exit(0);
} else if (StringUtils.equalsAny(arg, "-c", "--config")) {
String cfgFile = argsIt.next();
cfg = YamlConfigLoader.loadFromFile(cfgFile);
System.out.println("Loaded configuration from " + cfgFile);
} else if (StringUtils.equals("--version", arg)) {
System.out.println(Mxisd.Version);
System.exit(0);
} else {
System.out.println("Invalid argument: " + arg);
System.err.println("Invalid argument: " + arg);
System.err.println("Try '--help' for available arguments");
System.exit(1);
}
}
log.info("mxisd starting");
log.info("Version: {}", Mxisd.Version);
if (Objects.isNull(cfg)) {
cfg = YamlConfigLoader.tryLoadFromFile("mxisd.yaml").orElseGet(MxisdConfig::new);
}
try {
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);

View File

@@ -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);
});

View File

@@ -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);
}

View File

@@ -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";
}
}

View File

@@ -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";
}
}

View File

@@ -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!");
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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)));
}

View File

@@ -37,4 +37,5 @@ public class LookupSingleRequestJson {
public String getAddress() {
return address;
}
}

View File

@@ -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,15 +60,13 @@ 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 -> {
Optional<String> url = endpoint.apply(cfg.getEndpoints().getProfile());
if (!url.isPresent()) {
return Optional.empty();
}
try {
URIBuilder builder = new URIBuilder(url);
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)) {
@@ -107,17 +105,26 @@ public class RestProfileProvider extends RestProvider implements ProfileProvider
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());

View File

@@ -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);
}
}
}

View File

@@ -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()) {

View File

@@ -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 ?";

View File

@@ -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()));
}
}

View 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();
}
}

View File

@@ -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());
}
}

View File

@@ -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();

View File

@@ -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()));
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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();

View 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.info("Building email policy");
return domains.stream().map(d -> {
if (StringUtils.startsWith(d, "*")) {
log.info("Found domain and subdomain policy");
d = "(.*)" + d.substring(1);
} else if (StringUtils.startsWith(d, ".")) {
log.info("Found subdomain-only policy");
d = "(.*)" + d;
} else {
log.info("Found domain-only policy");
}
return "([^@]+)@" + d.replace(".", "\\.");
}).collect(Collectors.toList());
}
@Override
public void build() {
if (Objects.isNull(getDomain())) {
return;
}
if (Objects.nonNull(getDomain().getBlacklist())) {
if (Objects.isNull(getPattern().getBlacklist())) {
getPattern().setBlacklist(new ArrayList<>());
}
List<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.info("--- Registration config ---");
log.info("Before Build");
log.info(GsonUtil.getPrettyForLog(this));
new HashMap<>(getPolicy().getThreepid()).forEach((medium, policy) -> {
if (ThreePidMedium.Email.is(medium)) {
EmailPolicy pPolicy = GsonUtil.get().fromJson(GsonUtil.get().toJson(policy), EmailPolicy.class);
pPolicy.build();
policy = GsonUtil.makeObj(pPolicy);
} else {
ThreepidPolicy pPolicy = GsonUtil.get().fromJson(GsonUtil.get().toJson(policy), ThreepidPolicy.class);
pPolicy.build();
policy = GsonUtil.makeObj(pPolicy);
}
getPolicy().getThreepid().put(medium, policy);
});
log.info("After Build");
log.info(GsonUtil.getPrettyForLog(this));
}
}

View File

@@ -32,12 +32,7 @@ public class SessionConfig {
public static class PolicyTemplate {
public static class PolicySource {
public static class PolicySourceRemote {
private boolean enabled;
private String server;
public boolean isEnabled() {
return enabled;
@@ -47,98 +42,42 @@ public class SessionConfig {
this.enabled = enabled;
}
public String getServer() {
return server;
}
public void setServer(String server) {
this.server = server;
public static class PolicyUnbind {
public static class PolicyUnbindFraudulent {
private boolean sendWarning = true;
public boolean getSendWarning() {
return sendWarning;
}
public void setSendWarning(boolean sendWarning) {
this.sendWarning = sendWarning;
}
}
private boolean enabled;
private boolean toLocal;
private PolicySourceRemote toRemote = new PolicySourceRemote();
public boolean isEnabled() {
return enabled;
private PolicyUnbindFraudulent fraudulent = new PolicyUnbindFraudulent();
public PolicyUnbindFraudulent getFraudulent() {
return fraudulent;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean toLocal() {
return toLocal;
}
public void setToLocal(boolean toLocal) {
this.toLocal = toLocal;
}
public boolean toRemote() {
return toRemote.isEnabled();
}
public PolicySourceRemote getToRemote() {
return toRemote;
}
public void setToRemote(PolicySourceRemote toRemote) {
this.toRemote = toRemote;
}
}
private boolean enabled;
private PolicySource forLocal = new PolicySource();
private PolicySource forRemote = new PolicySource();
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public PolicySource getForLocal() {
return forLocal;
}
public PolicySource forLocal() {
return forLocal;
}
public PolicySource getForRemote() {
return forRemote;
}
public PolicySource forRemote() {
return forRemote;
}
public PolicySource forIf(boolean isLocal) {
return isLocal ? forLocal : forRemote;
public void setFraudulent(PolicyUnbindFraudulent fraudulent) {
this.fraudulent = fraudulent;
}
}
public Policy() {
validation.enabled = true;
validation.forLocal.enabled = true;
validation.forLocal.toLocal = true;
validation.forLocal.toRemote.enabled = true;
validation.forLocal.toRemote.server = "matrix-org";
validation.forRemote.enabled = true;
validation.forRemote.toLocal = false;
validation.forRemote.toRemote.enabled = true;
validation.forRemote.toRemote.server = "matrix-org";
}
private PolicyTemplate validation = new PolicyTemplate();
private PolicyUnbind unbind = new PolicyUnbind();
public PolicyTemplate getValidation() {
return validation;
@@ -148,6 +87,14 @@ public class SessionConfig {
this.validation = validation;
}
public PolicyUnbind getUnbind() {
return unbind;
}
public void setUnbind(PolicyUnbind unbind) {
this.unbind = unbind;
}
}
private Policy policy = new Policy();

View File

@@ -21,12 +21,13 @@
package io.kamax.mxisd.config;
import io.kamax.matrix.json.GsonUtil;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ViewConfig {
private transient final Logger log = LoggerFactory.getLogger(ViewConfig.class);
private static final Logger log = LoggerFactory.getLogger(ViewConfig.class);
public static class Session {
@@ -67,45 +68,13 @@ public class ViewConfig {
}
public static class Remote {
private Paths onRequest = new Paths();
private Paths onCheck = new Paths();
public Paths getOnRequest() {
return onRequest;
}
public void setOnRequest(Paths onRequest) {
this.onRequest = onRequest;
}
public Paths getOnCheck() {
return onCheck;
}
public void setOnCheck(Paths onCheck) {
this.onCheck = onCheck;
}
}
// Legacy option
private Local local = new Local();
private Local localRemote = new Local();
private Remote remote = new Remote();
private Paths onTokenSubmit = new Paths();
public Session() {
local.onTokenSubmit.success = "classpath:/templates/session/local/tokenSubmitSuccess.html";
local.onTokenSubmit.failure = "classpath:/templates/session/local/tokenSubmitFailure.html";
localRemote.onTokenSubmit.success = "classpath:/templates/session/localRemote/tokenSubmitSuccess.html";
localRemote.onTokenSubmit.failure = "classpath:/templates/session/local/tokenSubmitFailure.html";
remote.onRequest.success = "classpath:/templates/session/remote/requestSuccess.html";
remote.onRequest.failure = "classpath:/templates/session/remote/requestFailure.html";
remote.onCheck.success = "classpath:/templates/session/remote/checkSuccess.html";
remote.onCheck.failure = "classpath:/templates/session/remote/checkFailure.html";
onTokenSubmit.success = "classpath:/templates/session/tokenSubmitSuccess.html";
onTokenSubmit.failure = "classpath:/templates/session/tokenSubmitFailure.html";
}
public Local getLocal() {
@@ -116,21 +85,14 @@ public class ViewConfig {
this.local = local;
}
public Local getLocalRemote() {
return localRemote;
public Paths getOnTokenSubmit() {
return onTokenSubmit;
}
public void setLocalRemote(Local localRemote) {
this.localRemote = localRemote;
public void setOnTokenSubmit(Paths onTokenSubmit) {
this.onTokenSubmit = onTokenSubmit;
}
public Remote getRemote() {
return remote;
}
public void setRemote(Remote remote) {
this.remote = remote;
}
}
private Session session = new Session();
@@ -144,6 +106,17 @@ public class ViewConfig {
}
public void build() {
if (StringUtils.isNotBlank(session.local.onTokenSubmit.success) && StringUtils.isBlank(session.onTokenSubmit.success)) {
log.warn("Legacy option session.local.onTokenSubmit.success in use, please switch to session.onTokenSubmit.success");
session.onTokenSubmit.success = session.local.onTokenSubmit.success;
}
if (StringUtils.isNotBlank(session.local.onTokenSubmit.failure) && StringUtils.isBlank(session.onTokenSubmit.failure)) {
log.warn("Legacy option session.local.onTokenSubmit.failure in use, please switch to session.onTokenSubmit.failure");
session.onTokenSubmit.failure = session.local.onTokenSubmit.failure;
}
log.info("--- View config ---");
log.info("Session: {}", GsonUtil.get().toJson(session));
}

View File

@@ -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);

View File

@@ -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));

View File

@@ -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());
}
}

View File

@@ -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());
}
}
}

View File

@@ -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();

View File

@@ -115,26 +115,41 @@ public class EmailSendGridConfig {
public static class Templates {
public static class TemplateSessionUnbind {
private EmailTemplate fraudulent = new EmailTemplate();
public EmailTemplate getFraudulent() {
return fraudulent;
}
public void setFraudulent(EmailTemplate fraudulent) {
this.fraudulent = fraudulent;
}
}
public static class TemplateSession {
private EmailTemplate local = new EmailTemplate();
private EmailTemplate remote = new EmailTemplate();
private EmailTemplate validation = new EmailTemplate();
private TemplateSessionUnbind unbind = new TemplateSessionUnbind();
public EmailTemplate getLocal() {
return local;
public EmailTemplate getValidation() {
return validation;
}
public void setLocal(EmailTemplate local) {
this.local = local;
public void setValidation(EmailTemplate validation) {
this.validation = validation;
}
public EmailTemplate getRemote() {
return remote;
public TemplateSessionUnbind getUnbind() {
return unbind;
}
public void setRemote(EmailTemplate remote) {
this.remote = remote;
public void setUnbind(TemplateSessionUnbind unbind) {
this.unbind = unbind;
}
}
private EmailTemplate invite = new EmailTemplate();

View File

@@ -30,16 +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;
}

View File

@@ -39,39 +39,39 @@ public class GenericTemplateConfig {
public static class Session {
public static class SessionValidation {
public static class SessionUnbind {
private String local;
private String remote;
private String fraudulent;
public String getLocal() {
return local;
public String getFraudulent() {
return fraudulent;
}
public void setLocal(String local) {
this.local = local;
}
public String getRemote() {
return remote;
}
public void setRemote(String remote) {
this.remote = remote;
public void setFraudulent(String fraudulent) {
this.fraudulent = fraudulent;
}
}
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;
}
public SessionUnbind getUnbind() {
return unbind;
}
public void setUnbind(SessionUnbind unbind) {
this.unbind = unbind;
}
}
private String invite;

View File

@@ -29,17 +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;
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -1,6 +1,6 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Kamax Sarl
* Copyright (C) 2019 Kamax Sàrl
*
* https://www.kamax.io/
*
@@ -18,20 +18,34 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.http.undertow.handler.identity.v1;
package io.kamax.mxisd.crypto;
public class RemoteIdentityAPIv1 {
public class GenericKey implements Key {
public static final String BASE = "/_matrix/identity/remote/api/v1";
public static final String SESSION_REQUEST_TOKEN = BASE + "/validate/requestToken";
public static final String SESSION_CHECK = BASE + "/validate/check";
private final KeyIdentifier id;
private final boolean isValid;
private final String privKey;
public static String getRequestToken(String id, String secret) {
return SESSION_REQUEST_TOKEN + "?sid=" + id + "&client_secret=" + secret;
public GenericKey(KeyIdentifier id, boolean isValid, String privKey) {
this.id = new GenericKeyIdentifier(id);
this.isValid = isValid;
this.privKey = privKey;
}
public static String getSessionCheck(String id, String secret) {
return SESSION_CHECK + "?sid=" + id + "&client_secret=" + secret;
@Override
public KeyIdentifier getId() {
return id;
}
@Override
public boolean isValid() {
return isValid;
}
@Override
public String getPrivateKeyBase64() {
return privKey;
}
}

View File

@@ -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);
}
}

View 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();
}

View 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";
}

View 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();
}
}

View 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);
}

View 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
}

View 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 class RegularKeyIdentifier extends GenericKeyIdentifier {
public RegularKeyIdentifier(String algo, String serial) {
super(KeyType.Regular, algo, serial);
}
}

View 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();
}

View 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);
}

View 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;
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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) {

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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");
}
}

View File

@@ -25,7 +25,7 @@ import org.apache.http.HttpStatus;
public class SessionNotValidatedException extends HttpMatrixException {
public SessionNotValidatedException() {
super(HttpStatus.SC_OK, "M_SESSION_NOT_VALIDATED", "This validation session has not yet been completed");
super(HttpStatus.SC_BAD_REQUEST, "M_SESSION_NOT_VALIDATED", "This validation session has not yet been completed");
}
}

View File

@@ -20,6 +20,8 @@
package io.kamax.mxisd.exception;
import org.apache.http.HttpStatus;
public class SessionUnknownException extends HttpMatrixException {
public SessionUnknownException() {
@@ -27,7 +29,7 @@ public class SessionUnknownException extends HttpMatrixException {
}
public SessionUnknownException(String error) {
super(200, "M_NO_VALID_SESSION", error);
super(HttpStatus.SC_NOT_FOUND, "M_NO_VALID_SESSION", error);
}
}

View File

@@ -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;
}
}

View File

@@ -23,20 +23,29 @@ package io.kamax.mxisd.http.undertow.handler;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.dns.ClientDnsOverwrite;
import io.kamax.mxisd.exception.AccessTokenNotFoundException;
import io.kamax.mxisd.exception.HttpMatrixException;
import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.proxy.Response;
import io.kamax.mxisd.util.RestClientUtils;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.HttpString;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Deque;
@@ -46,7 +55,19 @@ import java.util.Optional;
public abstract class BasicHttpHandler implements HttpHandler {
private transient final Logger log = LoggerFactory.getLogger(BasicHttpHandler.class);
private static final Logger log = LoggerFactory.getLogger(BasicHttpHandler.class);
protected String getAccessToken(HttpServerExchange exchange) {
return Optional.ofNullable(exchange.getRequestHeaders().getFirst("Authorization"))
.flatMap(v -> {
if (!v.startsWith("Bearer ")) {
return Optional.empty();
}
return Optional.of(v.substring("Bearer ".length()));
}).filter(StringUtils::isNotEmpty)
.orElseThrow(AccessTokenNotFoundException::new);
}
protected String getRemoteHostAddress(HttpServerExchange exchange) {
return ((InetSocketAddress) exchange.getConnection().getPeerAddress()).getAddress().getHostAddress();
@@ -98,11 +119,11 @@ public abstract class BasicHttpHandler implements HttpHandler {
}
protected JsonObject parseJsonObject(HttpServerExchange exchange) {
try {
return GsonUtil.parseObj(IOUtils.toString(exchange.getInputStream(), StandardCharsets.UTF_8));
} catch (IOException e) {
throw new RuntimeException(e);
return GsonUtil.parseObj(getBodyUtf8(exchange));
}
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) {
@@ -149,4 +170,34 @@ public abstract class BasicHttpHandler implements HttpHandler {
upstream.getHeaders().forEach((key, value) -> exchange.getResponseHeaders().addAll(HttpString.tryFromString(key), value));
writeBodyAsUtf8(exchange, upstream.getBody());
}
protected void proxyPost(HttpServerExchange exchange, JsonObject body, CloseableHttpClient client, ClientDnsOverwrite dns) {
String target = dns.transform(URI.create(exchange.getRequestURL())).toString();
log.info("Requesting remote: {}", target);
HttpPost req = RestClientUtils.post(target, GsonUtil.get(), body);
exchange.getRequestHeaders().forEach(header -> {
header.forEach(v -> {
String name = header.getHeaderName().toString();
if (!StringUtils.startsWithIgnoreCase(name, "content-")) {
req.addHeader(name, v);
}
});
});
try (CloseableHttpResponse res = client.execute(req)) {
exchange.setStatusCode(res.getStatusLine().getStatusCode());
for (Header h : res.getAllHeaders()) {
for (HeaderElement el : h.getElements()) {
exchange.getResponseHeaders().add(HttpString.tryFromString(h.getName()), el.getValue());
}
}
res.getEntity().writeTo(exchange.getOutputStream());
exchange.endExchange();
} catch (IOException e) {
log.warn("Unable to make proxy call: {}", e.getMessage(), e);
throw new InternalServerError(e);
}
}
}

View File

@@ -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.");
}
}

View File

@@ -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
}
}

View File

@@ -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();
}

View File

@@ -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, "{}");
}
}

View File

@@ -46,7 +46,7 @@ public class BulkLookupHandler extends LookupHandler {
}
@Override
public void handleRequest(HttpServerExchange exchange) {
public void handleRequest(HttpServerExchange exchange) throws Exception {
ClientBulkLookupRequest input = parseJsonTo(exchange, ClientBulkLookupRequest.class);
BulkLookupRequest lookupRequest = new BulkLookupRequest();
setRequesterInfo(lookupRequest, exchange);
@@ -63,7 +63,9 @@ public class BulkLookupHandler extends LookupHandler {
lookupRequest.setMappings(mappings);
ClientBulkLookupAnswer answer = new ClientBulkLookupAnswer();
answer.addAll(strategy.find(lookupRequest));
answer.addAll(strategy.find(lookupRequest).get());
log.info("Finished bulk lookup request from {}", lookupRequest.getRequester());
respondJson(exchange, answer);
}

View File

@@ -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);
}
}

View File

@@ -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);
}

Some files were not shown because too many files have changed in this diff Show More