Compare commits

...

19 Commits

Author SHA1 Message Date
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
90 changed files with 1983 additions and 709 deletions

View File

@@ -14,13 +14,14 @@ mxisd - Federated Matrix Identity Server
# Overview
mxisd is a Federated Matrix Identity server for self-hosted Matrix infrastructures with [enhanced features](#features).
As an enhanced Identity service, it implements the [Matrix Identity service API](https://kamax.io/matrix/api/identity_service/unstable.html)
As an enhanced Identity service, it implements the [Identity service API](https://matrix.org/docs/spec/identity_service/r0.1.0.html)
and several [extra features](#features) that greatly enhance user experience within Matrix.
It is the one stop shop for anything regarding Authentication, Directory and Identity management in Matrix built in a
single coherent product.
mxisd is specifically designed to connect to an existing on-premise Identity store (AD/Samba/LDAP, SQL Database,
Web services/app, etc.) and ease the integration of a Matrix infrastructure within an existing one.
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:
@@ -33,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,
@@ -67,10 +68,15 @@ 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/))

View File

@@ -101,7 +101,7 @@ dependencies {
compile 'com.j256.ormlite:ormlite-jdbc:5.0'
// ed25519 handling
compile 'net.i2p.crypto:eddsa:0.1.0'
compile 'net.i2p.crypto:eddsa:0.3.0'
// LDAP connector
compile 'org.apache.directory.api:api-all:1.0.0'
@@ -190,13 +190,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

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

@@ -26,7 +26,7 @@ synapseSql:
connection: '<DB CONNECTION URL>'
```
The `synapseSql` section is used to retrieve display names which are not directly accessible in this mode.
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.

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,5 @@
# Identity
**WARNING**: This document is incomplete and can be misleading.
Implementation of the [Unofficial Matrix Identity Service API](https://kamax.io/matrix/api/identity_service/unstable.html).
Implementation of the [Identity Service API r0.1.0](https://matrix.org/docs/spec/identity_service/r0.1.0.html).
## Lookups
If you would like to use the central matrix.org Identity server to ensure maximum discovery at the cost of potentially

View File

@@ -6,8 +6,7 @@
5. [Validate](#validate)
6. [Next steps](#next-steps)
Following these quick start instructions, you will have a basic setup that can perform recursive/federated lookups and
talk to the central Matrix.org Identity server.
Following these quick start instructions, you will have a basic setup that can perform recursive/federated lookups.
This will be a good ground work for further integration with features and your existing Identity stores.
---
@@ -24,13 +23,17 @@ You will need:
- Working Homeserver, ideally with working federation
- Reverse proxy with regular TLS/SSL certificate (Let's encrypt) for your mxisd domain
As synapse requires an HTTPS connection when talking to an Identity service, **a reverse proxy is required** as mxisd does
not support HTTPS listener at this time.
If you use synapse:
- It requires an HTTPS connection when talking to an Identity service, **a reverse proxy is required** as mxisd does
not support HTTPS listener at this time.
- HTTPS is hardcoded when talking to the Identity server. If your Identity server URL in your client is `https://matrix.example.org/`,
then you need to ensure `https://matrix.example.org/_matrix/identity/api/v1/...` will reach mxisd if called from the synapse host.
In doubt, test with `curl` or similar.
For maximum integration, it is best to have your Homeserver and mxisd reachable via the same hostname.
For maximum integration, it is best to have your Homeserver and mxisd reachable via the same public hostname.
Be aware of a [NAT/Reverse proxy gotcha](https://github.com/kamax-matrix/mxisd/wiki/Gotchas#nating) if you use the same
hostname.
host.
The following Quick Start guide assumes you will host the Homeserver and mxisd under the same hostname.
If you would like a high-level view of the infrastructure and how each feature is integrated, see the
@@ -51,17 +54,10 @@ See the [Latest release](https://github.com/kamax-matrix/mxisd/releases/latest)
> **NOTE**: Details about configuration syntax and format are described [here](configure.md)
Create/edit a minimal configuration (see installer doc for the location):
```yaml
matrix:
domain: 'example.org'
key:
path: '/path/to/signing.key.file'
storage:
provider:
sqlite:
database: '/path/to/mxisd.db'
```
If you haven't created a configuration file yet, copy `mxisd.example.yaml` to where the configuration file is stored given
your installation method and edit to your needs.
The following items must be at least configured:
- `matrix.domain` should be set to your Homeserver domain (`server_name` in synapse configuration)
- `key.path` will store the signing keys, which must be kept safe! If the file does not exist, keys will be generated for you.
- `storage.provider.sqlite.database` is the location of the SQLite Database file which will hold state (invites, etc.)
@@ -83,9 +79,9 @@ ProxyPass /_matrix/identity http://0.0.0.0:8090/_matrix/identity
Typical configuration would look like:
```apache
<VirtualHost *:443>
ServerName example.org
ServerName matrix.example.org
...
# ...
ProxyPreserveHost on
ProxyPass /_matrix/identity http://localhost:8090/_matrix/identity
@@ -107,9 +103,9 @@ Typical configuration would look like:
```nginx
server {
listen 443 ssl;
server_name example.org;
server_name matrix.example.org;
...
# ...
location /_matrix/identity {
proxy_pass http://localhost:8090/_matrix/identity;
@@ -130,17 +126,17 @@ Add your mxisd domain into the `homeserver.yaml` at `trusted_third_party_id_serv
In a typical configuration, you would end up with something similar to:
```yaml
trusted_third_party_id_servers:
- example.org
- matrix.example.org
```
It is recommended to remove `matrix.org` and `vector.im` (or any other default entry) from your configuration so only
your own Identity server is authoritative for your HS.
It is **highly recommended** to remove `matrix.org` and `vector.im` (or any other default entry) from your configuration
so only your own Identity server is authoritative for your HS.
## Validate
**NOTE:** In case your homeserver has no working federation, step 5 will not happen. If step 4 took place, consider
your installation validated.
1. Log in using your Matrix client and set `https://example.org` as your Identity server URL, replacing `example.org` by
the relevant hostname which you configured in your reverse proxy.
1. Log in using your Matrix client and set `https://matrix.example.org` as your Identity server URL, replacing `matrix.example.org`
by the relevant hostname which you configured in your reverse proxy.
2. Create a new empty room. All further actions will take place in this room.
3. Invite `mxisd-federation-test@kamax.io`
4. The 3PID invite should be turned into a Matrix invite to `@mxisd-lookup-test:kamax.io`.
@@ -148,7 +144,8 @@ the relevant hostname which you configured in your reverse proxy.
**NOTE:** You might not see a suggestion for the e-mail address, which is normal. Still proceed with the invite.
If it worked, it means you are up and running and can enjoy mxisd in its basic mode! Congratulations!
If it did not work, [get in touch](../README.md#support) and we'll do our best to get you started.
If it did not work, read the basic [troubleshooting guide](troubleshooting.md), [get in touch](../README.md#support) and
we'll do our best to get you started.
## Next steps
Once your mxisd server is up and running, there are several ways you can enhance and integrate further with your

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

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

@@ -2,12 +2,12 @@
Synapse's Database itself can be used as an Identity store.
## 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.

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

View File

@@ -18,9 +18,7 @@ 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:
frandulent: '/path/to/unbind-fraudulent-template.eml'
generic:
@@ -53,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.
@@ -61,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. |

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 #
@@ -16,26 +21,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 +49,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 +89,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

@@ -1,27 +1,26 @@
/*
* The MIT License
*
* Copyright (c) 2013 Edin Dazdarevic (edin.dazdarevic@gmail.com)
* The MIT License
*
* Copyright (c) 2013 Edin Dazdarevic (edin.dazdarevic@gmail.com)
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* */
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package edazdarevic.commons.net;

View File

@@ -21,6 +21,7 @@
package io.kamax.mxisd;
import io.kamax.mxisd.config.MxisdConfig;
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;
@@ -52,6 +53,7 @@ public class HttpMxisd {
public void start() {
m.start();
HttpHandler helloHandler = SaneHandler.around(new HelloHandler());
HttpHandler asNotFoundHandler = SaneHandler.around(new AsNotFoundHandler(m.getAs()));
HttpHandler asTxnHandler = SaneHandler.around(new AsTransactionHandler(m.getAs()));
HttpHandler storeInvHandler = SaneHandler.around(new StoreInviteHandler(m.getConfig().getServer(), m.getInvitationManager(), m.getKeyManager()));
@@ -59,6 +61,8 @@ public class HttpMxisd {
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()))
@@ -76,8 +80,9 @@ public class HttpMxisd {
.get(EphemeralKeyIsValidHandler.Path, SaneHandler.around(new EphemeralKeyIsValidHandler()))
// 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())))
@@ -106,7 +111,6 @@ public class HttpMxisd {
public void stop() {
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;
@@ -40,6 +38,7 @@ 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;
@@ -47,6 +46,9 @@ import io.kamax.mxisd.profile.ProfileManager;
import io.kamax.mxisd.profile.ProfileProviders;
import io.kamax.mxisd.session.SessionManager;
import io.kamax.mxisd.storage.IStorage;
import io.kamax.mxisd.storage.crypto.Ed25519KeyManager;
import io.kamax.mxisd.storage.crypto.KeyManager;
import io.kamax.mxisd.storage.crypto.SignatureManager;
import io.kamax.mxisd.storage.ormlite.OrmLiteSqlStorage;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
@@ -55,42 +57,43 @@ import java.util.ServiceLoader;
public class Mxisd {
protected MxisdConfig cfg;
private MxisdConfig cfg;
protected CloseableHttpClient httpClient;
protected IRemoteIdentityServerFetcher srvFetcher;
private CloseableHttpClient httpClient;
private IRemoteIdentityServerFetcher srvFetcher;
protected IStorage store;
private IStorage store;
protected KeyManager keyMgr;
protected SignatureManager signMgr;
private Ed25519KeyManager keyMgr;
private SignatureManager signMgr;
// Features
protected AuthManager authMgr;
protected DirectoryManager dirMgr;
protected LookupStrategy idStrategy;
protected InvitationManager invMgr;
protected ProfileManager pMgr;
protected AppSvcManager asHander;
protected SessionManager sessMgr;
protected NotificationManager notifMgr;
private AuthManager authMgr;
private DirectoryManager dirMgr;
private LookupStrategy idStrategy;
private InvitationManager invMgr;
private ProfileManager pMgr;
private AppSvcManager asHander;
private SessionManager sessMgr;
private NotificationManager notifMgr;
public Mxisd(MxisdConfig cfg) {
this.cfg = cfg.build();
}
protected void build() {
private void build() {
httpClient = HttpClients.custom()
.setUserAgent("mxisd")
.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());
signMgr = CryptoFactory.getSignatureManager(keyMgr);
ClientDnsOverwrite clientDns = new ClientDnsOverwrite(cfg.getDns().getOverwrite());
FederationDnsOverwrite fedDns = new FederationDnsOverwrite(cfg.getDns().getOverwrite());
Synapse synapse = new Synapse(cfg.getSynapseSql());
@@ -103,7 +106,7 @@ public class Mxisd {
pMgr = new ProfileManager(ProfileProviders.get(), clientDns, httpClient);
notifMgr = new NotificationManager(cfg.getNotification(), NotificationHandlers.get());
sessMgr = new SessionManager(cfg.getSession(), cfg.getMatrix(), store, notifMgr, idStrategy, httpClient);
invMgr = new InvitationManager(cfg.getInvite(), store, idStrategy, signMgr, fedDns, notifMgr);
invMgr = new InvitationManager(cfg, store, idStrategy, signMgr, fedDns, notifMgr);
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);

View File

@@ -22,44 +22,52 @@ 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;
Iterator<String> argsIt = Arrays.asList(args).iterator();
while (argsIt.hasNext()) {
String arg = argsIt.next();
if (StringUtils.equals("-c", arg)) {
String cfgFile = argsIt.next();
cfg = YamlConfigLoader.loadFromFile(cfgFile);
System.out.println("Loaded configuration from " + cfgFile);
} else {
System.out.println("Invalid argument: " + arg);
System.exit(1);
}
}
if (Objects.isNull(cfg)) {
cfg = YamlConfigLoader.tryLoadFromFile("mxisd.yaml").orElseGet(MxisdConfig::new);
}
private static final Logger log = LoggerFactory.getLogger("App");
public static void main(String[] args) {
try {
log.info("------------- mxisd starting -------------");
MxisdConfig cfg = null;
Iterator<String> argsIt = Arrays.asList(args).iterator();
while (argsIt.hasNext()) {
String arg = argsIt.next();
if (StringUtils.equals("-c", arg)) {
String cfgFile = argsIt.next();
cfg = YamlConfigLoader.loadFromFile(cfgFile);
} else {
log.info("Invalid argument: {}", arg);
System.exit(1);
}
}
if (Objects.isNull(cfg)) {
cfg = YamlConfigLoader.tryLoadFromFile("mxisd.yaml").orElseGet(MxisdConfig::new);
}
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

@@ -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,64 +60,71 @@ public class RestProfileProvider extends RestProvider implements ProfileProvider
Function<RestBackendConfig.ProfileEndpoints, Optional<String>> endpoint,
Function<JsonProfileResult, Optional<T>> value
) {
return cfg.getEndpoints().getProfile()
// We get the endpoint
.flatMap(endpoint)
// We only continue if there is a value
.filter(StringUtils::isNotBlank)
// We use the endpoint
.flatMap(url -> {
try {
URIBuilder builder = new URIBuilder(url);
HttpPost req = new HttpPost(builder.build());
req.setEntity(new StringEntity(GsonUtil.get().toJson(new JsonProfileRequest(userId)), ContentType.APPLICATION_JSON));
try (CloseableHttpResponse res = client.execute(req)) {
int sc = res.getStatusLine().getStatusCode();
if (sc == 404) {
log.info("Got 404 - No result found");
return Optional.empty();
}
Optional<String> url = endpoint.apply(cfg.getEndpoints().getProfile());
if (!url.isPresent()) {
return Optional.empty();
}
if (sc != 200) {
throw new InternalServerError("Unexpected backed status code: " + sc);
}
try {
URIBuilder builder = new URIBuilder(url.get());
HttpPost req = new HttpPost(builder.build());
req.setEntity(new StringEntity(GsonUtil.get().toJson(new JsonProfileRequest(userId)), ContentType.APPLICATION_JSON));
try (CloseableHttpResponse res = client.execute(req)) {
int sc = res.getStatusLine().getStatusCode();
if (sc == 404) {
log.info("Got 404 - No result found");
return Optional.empty();
}
String body = IOUtils.toString(res.getEntity().getContent(), StandardCharsets.UTF_8);
if (StringUtils.isBlank(body)) {
log.warn("Backend response body is empty/blank, expected JSON object with profile key");
return Optional.empty();
}
if (sc != 200) {
throw new InternalServerError("Unexpected backed status code: " + sc);
}
Optional<JsonObject> pJson = GsonUtil.findObj(GsonUtil.parseObj(body), "profile");
if (!pJson.isPresent()) {
log.warn("Backend response body is invalid, expected JSON object with profile key");
return Optional.empty();
}
String body = IOUtils.toString(res.getEntity().getContent(), StandardCharsets.UTF_8);
if (StringUtils.isBlank(body)) {
log.warn("Backend response body is empty/blank, expected JSON object with profile key");
return Optional.empty();
}
JsonProfileResult profile = gson.fromJson(pJson.get(), JsonProfileResult.class);
return value.apply(profile);
}
} catch (JsonSyntaxException | InvalidJsonException e) {
log.error("Unable to parse backend response as JSON", e);
throw new InternalServerError(e);
} catch (URISyntaxException e) {
log.error("Unable to build a valid request URL", e);
throw new InternalServerError(e);
} catch (IOException e) {
log.error("I/O Error during backend request", e);
throw new InternalServerError();
}
});
Optional<JsonObject> pJson = GsonUtil.findObj(GsonUtil.parseObj(body), "profile");
if (!pJson.isPresent()) {
log.warn("Backend response body is invalid, expected JSON object with profile key");
return Optional.empty();
}
JsonProfileResult profile = gson.fromJson(pJson.get(), JsonProfileResult.class);
return value.apply(profile);
}
} catch (JsonSyntaxException | InvalidJsonException e) {
log.error("Unable to parse backend response as JSON", e);
throw new InternalServerError(e);
} catch (URISyntaxException e) {
log.error("Unable to build a valid request URL", e);
throw new InternalServerError(e);
} catch (IOException e) {
log.error("I/O Error during backend request", e);
throw new InternalServerError();
}
}
@Override
public Optional<String> getDisplayName(_MatrixID userId) {
return doRequest(userId, p -> Optional.ofNullable(p.getDisplayName()), profile -> Optional.ofNullable(profile.getDisplayName()));
return doRequest(userId, p -> {
if (StringUtils.isBlank(p.getDisplayName())) {
return Optional.empty();
}
return Optional.ofNullable(p.getDisplayName());
}, profile -> Optional.ofNullable(profile.getDisplayName()));
}
@Override
public List<_ThreePid> getThreepids(_MatrixID userId) {
return doRequest(userId, p -> Optional.ofNullable(p.getThreepids()), profile -> {
return doRequest(userId, p -> {
if (StringUtils.isBlank(p.getThreepids())) {
return Optional.empty();
}
return Optional.ofNullable(p.getThreepids());
}, profile -> {
List<_ThreePid> t = new ArrayList<>();
if (Objects.nonNull(profile.getThreepids())) {
t.addAll(profile.getThreepids());
@@ -128,7 +135,12 @@ public class RestProfileProvider extends RestProvider implements ProfileProvider
@Override
public List<String> getRoles(_MatrixID userId) {
return doRequest(userId, p -> Optional.ofNullable(p.getRoles()), profile -> {
return doRequest(userId, p -> {
if (StringUtils.isBlank(p.getRoles())) {
return Optional.empty();
}
return Optional.ofNullable(p.getRoles());
}, profile -> {
List<String> t = new ArrayList<>();
if (Objects.nonNull(profile.getRoles())) {
t.addAll(profile.getRoles());

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

@@ -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;
@@ -314,7 +315,8 @@ 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());

View File

@@ -115,28 +115,6 @@ public class EmailSendGridConfig {
public static class Templates {
public static class TemplateSessionValidation {
private EmailTemplate local = new EmailTemplate();
private EmailTemplate remote = new EmailTemplate();
public EmailTemplate getLocal() {
return local;
}
public void setLocal(EmailTemplate local) {
this.local = local;
}
public EmailTemplate getRemote() {
return remote;
}
public void setRemote(EmailTemplate remote) {
this.remote = remote;
}
}
public static class TemplateSessionUnbind {
private EmailTemplate fraudulent = new EmailTemplate();
@@ -153,14 +131,14 @@ public class EmailSendGridConfig {
public static class TemplateSession {
private TemplateSessionValidation validation = new TemplateSessionValidation();
private EmailTemplate validation = new EmailTemplate();
private TemplateSessionUnbind unbind = new TemplateSessionUnbind();
public TemplateSessionValidation getValidation() {
public EmailTemplate getValidation() {
return validation;
}
public void setValidation(TemplateSessionValidation validation) {
public void setValidation(EmailTemplate validation) {
this.validation = validation;
}

View File

@@ -30,17 +30,17 @@ public class EmailTemplateConfig extends GenericTemplateConfig {
public EmailTemplateConfig() {
setInvite("classpath:/threepids/email/invite-template.eml");
getGeneric().put("matrixId", "classpath:/threepids/email/mxid-template.eml");
getSession().getValidation().setLocal("classpath:/threepids/email/validate-local-template.eml");
getSession().getValidation().setRemote("classpath:/threepids/email/validate-remote-template.eml");
getSession().setValidation("classpath:/threepids/email/validate-template.eml");
getSession().getUnbind().setFraudulent("classpath:/threepids/email/unbind-fraudulent.eml");
}
public EmailTemplateConfig build() {
log.info("--- E-mail Generator templates config ---");
log.info("Invite: {}", getName(getInvite()));
log.info("Session validation:");
log.info("\tLocal: {}", getName(getSession().getValidation().getLocal()));
log.info("\tRemote: {}", getName(getSession().getValidation().getRemote()));
log.info("Session:");
log.info(" Validation: {}", getSession().getValidation());
log.info(" Unbind:");
log.info(" Fraudulent: {}", getSession().getUnbind().getFraudulent());
return this;
}

View File

@@ -39,29 +39,6 @@ public class GenericTemplateConfig {
public static class Session {
public static class SessionValidation {
private String local;
private String remote;
public String getLocal() {
return local;
}
public void setLocal(String local) {
this.local = local;
}
public String getRemote() {
return remote;
}
public void setRemote(String remote) {
this.remote = remote;
}
}
public static class SessionUnbind {
private String fraudulent;
@@ -76,14 +53,14 @@ public class GenericTemplateConfig {
}
private SessionValidation validation = new SessionValidation();
private String validation;
private SessionUnbind unbind = new SessionUnbind();
public SessionValidation getValidation() {
public String getValidation() {
return validation;
}
public void setValidation(SessionValidation validation) {
public void setValidation(String validation) {
this.validation = validation;
}

View File

@@ -29,18 +29,17 @@ public class PhoneSmsTemplateConfig extends GenericTemplateConfig {
public PhoneSmsTemplateConfig() {
setInvite("classpath:/threepids/sms/invite-template.txt");
getGeneric().put("matrixId", "classpath:/threepids/email/mxid-template.eml");
getSession().getValidation().setLocal("classpath:/threepids/sms/validate-local-template.txt");
getSession().getValidation().setRemote("classpath:/threepids/sms/validate-remote-template.txt");
getSession().setValidation("classpath:/threepids/sms/validate-template.txt");
getSession().getUnbind().setFraudulent("classpath:/threepids/sms/unbind-fraudulent.txt");
}
public PhoneSmsTemplateConfig build() {
log.info("--- SMS Generator templates config ---");
log.info("Invite: {}", getName(getInvite()));
log.info("Session validation:");
log.info("\tLocal: {}", getName(getSession().getValidation().getLocal()));
log.info("\tRemote: {}", getName(getSession().getValidation().getRemote()));
log.info("Session:");
log.info(" Validation: {}", getSession().getValidation());
log.info(" Unbind:");
log.info(" Fraudulent: {}", getSession().getUnbind().getFraudulent());
return this;
}

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,8 @@
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.storage.crypto.*;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
@@ -31,10 +30,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 +44,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

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

@@ -101,6 +101,10 @@ public abstract class BasicHttpHandler implements HttpHandler {
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) {
respondJson(ex, statusCode, GsonUtil.get().toJson(bodyJson));
}

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

@@ -21,10 +21,11 @@
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.http.IsAPIv1;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.storage.crypto.GenericKeyIdentifier;
import io.kamax.mxisd.storage.crypto.KeyManager;
import io.kamax.mxisd.storage.crypto.KeyType;
import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -46,16 +47,12 @@ public class KeyGetHandler extends BasicHttpHandler {
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]);
String keyAlgo = v[0];
String keyId = v[1];
if (!"ed25519".contentEquals(keyType)) {
throw new BadRequestException("Invalid algorithm: " + keyType);
}
log.info("Key {}:{} was requested", keyType, keyId);
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);
}

View File

@@ -20,10 +20,10 @@
package io.kamax.mxisd.http.undertow.handler.identity.v1;
import io.kamax.matrix.crypto.KeyManager;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.storage.crypto.KeyManager;
import io.kamax.mxisd.storage.crypto.KeyType;
import io.undertow.server.HttpServerExchange;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -44,9 +44,7 @@ public class RegularKeyIsValidHandler extends KeyIsValidHandler {
String pubKey = getQueryParameter(exchange, "public_key");
log.info("Validating public key {}", pubKey);
// TODO do in manager
boolean valid = StringUtils.equals(pubKey, mgr.getPublicKeyBase64(mgr.getCurrentIndex()));
respondJson(exchange, valid ? validKey : invalidKey);
respondJson(exchange, mgr.isValid(KeyType.Regular, pubKey) ? validKey : invalidKey);
}
}

View File

@@ -21,15 +21,17 @@
package io.kamax.mxisd.http.undertow.handler.identity.v1;
import com.google.gson.JsonObject;
import io.kamax.matrix.crypto.SignatureManager;
import io.kamax.matrix.event.EventKey;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.matrix.json.MatrixJson;
import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.io.identity.SingeLookupReplyJson;
import io.kamax.mxisd.lookup.SingleLookupReply;
import io.kamax.mxisd.lookup.SingleLookupRequest;
import io.kamax.mxisd.lookup.strategy.LookupStrategy;
import io.kamax.mxisd.storage.crypto.SignatureManager;
import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -42,10 +44,12 @@ public class SingleLookupHandler extends LookupHandler {
private transient final Logger log = LoggerFactory.getLogger(SingleLookupHandler.class);
private ServerConfig cfg;
private LookupStrategy strategy;
private SignatureManager signMgr;
public SingleLookupHandler(LookupStrategy strategy, SignatureManager signMgr) {
public SingleLookupHandler(MxisdConfig cfg, LookupStrategy strategy, SignatureManager signMgr) {
this.cfg = cfg.getServer();
this.strategy = strategy;
this.signMgr = signMgr;
}
@@ -72,7 +76,7 @@ public class SingleLookupHandler extends LookupHandler {
// FIXME signing should be done in the business model, not in the controller
JsonObject obj = GsonUtil.makeObj(new SingeLookupReplyJson(lookup));
obj.add(EventKey.Signatures.get(), signMgr.signMessageGson(MatrixJson.encodeCanonical(obj)));
obj.add(EventKey.Signatures.get(), signMgr.signMessageGson(cfg.getName(), MatrixJson.encodeCanonical(obj)));
respondJson(exchange, obj);
}

View File

@@ -24,7 +24,6 @@ import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
import io.kamax.matrix.MatrixID;
import io.kamax.matrix._MatrixID;
import io.kamax.matrix.crypto.KeyManager;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.exception.BadRequestException;
@@ -36,6 +35,7 @@ import io.kamax.mxisd.invitation.IThreePidInvite;
import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.invitation.InvitationManager;
import io.kamax.mxisd.invitation.ThreePidInvite;
import io.kamax.mxisd.storage.crypto.KeyManager;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.QueryParameterUtils;
import org.apache.commons.lang3.StringUtils;
@@ -96,7 +96,8 @@ public class StoreInviteHandler extends BasicHttpHandler {
IThreePidInvite invite = new ThreePidInvite(sender, inv.getMedium(), inv.getAddress(), inv.getRoomId(), parameters);
IThreePidInviteReply reply = invMgr.storeInvite(invite);
respondJson(exchange, new ThreePidInviteReplyIO(reply, keyMgr.getPublicKeyBase64(keyMgr.getCurrentIndex()), cfg.getPublicUrl()));
// FIXME the key info must be set by the invitation manager in the reply object!
respondJson(exchange, new ThreePidInviteReplyIO(reply, keyMgr.getPublicKeyBase64(keyMgr.getServerSigningKey().getId()), cfg.getPublicUrl()));
}
}

View File

@@ -23,9 +23,10 @@ package io.kamax.mxisd.invitation;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import io.kamax.matrix.MatrixID;
import io.kamax.matrix.crypto.SignatureManager;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.config.InvitationConfig;
import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.dns.FederationDnsOverwrite;
import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.exception.MappingAlreadyExistsException;
@@ -34,6 +35,7 @@ import io.kamax.mxisd.lookup.ThreePidMapping;
import io.kamax.mxisd.lookup.strategy.LookupStrategy;
import io.kamax.mxisd.notification.NotificationManager;
import io.kamax.mxisd.storage.IStorage;
import io.kamax.mxisd.storage.crypto.SignatureManager;
import io.kamax.mxisd.storage.ormlite.dao.ThreePidInviteIO;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.RandomStringUtils;
@@ -67,6 +69,7 @@ public class InvitationManager {
private transient final Logger log = LoggerFactory.getLogger(InvitationManager.class);
private InvitationConfig cfg;
private ServerConfig srvCfg;
private IStorage storage;
private LookupStrategy lookupMgr;
private SignatureManager signMgr;
@@ -79,14 +82,15 @@ public class InvitationManager {
private Map<String, IThreePidInviteReply> invitations = new ConcurrentHashMap<>();
public InvitationManager(
InvitationConfig cfg,
MxisdConfig mxisdCfg,
IStorage storage,
LookupStrategy lookupMgr,
SignatureManager signMgr,
FederationDnsOverwrite dns,
NotificationManager notifMgr
) {
this.cfg = cfg;
this.cfg = mxisdCfg.getInvite();
this.srvCfg = mxisdCfg.getServer();
this.storage = storage;
this.lookupMgr = lookupMgr;
this.signMgr = signMgr;
@@ -280,7 +284,7 @@ public class InvitationManager {
JsonObject obj = new JsonObject();
obj.addProperty("mxid", mxid);
obj.addProperty("token", reply.getToken());
obj.add("signatures", signMgr.signMessageGson(obj.toString()));
obj.add("signatures", signMgr.signMessageGson(srvCfg.getName(), obj.toString()));
JsonObject objUp = new JsonObject();
objUp.addProperty("mxid", mxid);
@@ -298,7 +302,7 @@ public class InvitationManager {
content.addProperty("address", address);
content.addProperty("mxid", mxid);
content.add("signatures", signMgr.signMessageGson(content.toString()));
content.add("signatures", signMgr.signMessageGson(srvCfg.getName(), content.toString()));
StringEntity entity = new StringEntity(content.toString(), StandardCharsets.UTF_8);
entity.setContentType("application/json");

View File

@@ -25,6 +25,7 @@ import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.exception.InvalidResponseJsonException;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.io.identity.ClientBulkLookupRequest;
import io.kamax.mxisd.lookup.SingleLookupReply;
import io.kamax.mxisd.lookup.SingleLookupRequest;
@@ -73,7 +74,7 @@ public class RemoteIdentityServerFetcher implements IRemoteIdentityServerFetcher
try {
URIBuilder b = new URIBuilder(remote);
b.setPath("/_matrix/identity/api/v1/lookup");
b.setPath(IsAPIv1.Base + "/lookup");
b.addParameter("medium", request.getType());
b.addParameter("address", request.getThreePid());
HttpGet req = new HttpGet(b.build());
@@ -116,7 +117,7 @@ public class RemoteIdentityServerFetcher implements IRemoteIdentityServerFetcher
ClientBulkLookupRequest mappingRequest = new ClientBulkLookupRequest();
mappingRequest.setMappings(mappings);
String url = remote + "/_matrix/identity/api/v1/bulk_lookup";
String url = remote + IsAPIv1.Base + "/bulk_lookup";
try {
HttpPost request = RestClientUtils.post(url, mappingRequest);
try (CloseableHttpResponse response = client.execute(request)) {

View File

@@ -57,7 +57,7 @@ public class RecursivePriorityLookupStrategy implements LookupStrategy {
try {
log.info("Found {} providers", providers.size());
providers.forEach(p -> log.info("\t- {}", p.getClass().getName()));
providers.forEach(p -> log.info(" - {}", p.getClass().getName()));
providers.sort((o1, o2) -> Integer.compare(o2.getPriority(), o1.getPriority()));
log.info("Recursive lookup enabled: {}", cfg.getRecursive().isEnabled());

View File

@@ -1,17 +1,42 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Kamax Sarl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.matrix;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import io.kamax.mxisd.http.IsAPIv1;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xbill.DNS.*;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
@@ -20,31 +45,41 @@ import java.util.List;
import java.util.Optional;
// FIXME placeholder, this must go in matrix-java-sdk for 1.0
// FIXME this class is just a mistake and should never have happened. Make sure to get rid of for v2.x
public class IdentityServerUtils {
private static Logger log = LoggerFactory.getLogger(IdentityServerUtils.class);
private static JsonParser parser = new JsonParser();
private static CloseableHttpClient client;
public static void setHttpClient(CloseableHttpClient client) {
IdentityServerUtils.client = client;
}
public static boolean isUsable(String remote) {
if (StringUtils.isBlank(remote)) {
log.info("IS URL is blank, not usable");
return false;
}
try {
// FIXME use Apache HTTP client
HttpURLConnection rootSrvConn = (HttpURLConnection) new URL(remote + "/_matrix/identity/api/v1/").openConnection();
// TODO turn this into a configuration property
rootSrvConn.setConnectTimeout(2000);
HttpGet req = new HttpGet(URI.create(remote + IsAPIv1.Base));
req.setConfig(RequestConfig.custom()
.setConnectTimeout(2000)
.setConnectionRequestTimeout(2000)
.build()
);
int status = rootSrvConn.getResponseCode();
try (CloseableHttpResponse res = client.execute(req)) {
int status = res.getStatusLine().getStatusCode();
if (status != 200) {
log.info("Usability of {} as Identity server: answer status: {}", remote, status);
return false;
}
JsonElement el = parser.parse(IOUtils.toString(rootSrvConn.getInputStream(), StandardCharsets.UTF_8));
JsonElement el = parser.parse(IOUtils.toString(res.getEntity().getContent(), StandardCharsets.UTF_8));
if (!el.isJsonObject()) {
log.debug("IS {} did not send back a JSON object for single 3PID lookup");
log.debug("IS {} did not send back an empty JSON object as per spec, not a valid IS");
return false;
}

View File

@@ -37,8 +37,6 @@ public interface NotificationHandler {
void sendForValidation(IThreePidSession session);
void sendForRemoteValidation(IThreePidSession session);
void sendForFraudulentUnbind(ThreePid tpid);
}

View File

@@ -78,10 +78,6 @@ public class NotificationManager {
ensureMedium(session.getThreePid().getMedium()).sendForValidation(session);
}
public void sendForRemoteValidation(IThreePidSession session) {
ensureMedium(session.getThreePid().getMedium()).sendForRemoteValidation(session);
}
public void sendForFraudulentUnbind(ThreePid tpid) throws NotImplementedException {
ensureMedium(tpid.getMedium()).sendForFraudulentUnbind(tpid);
}

View File

@@ -58,7 +58,7 @@ public class ProfileManager {
this.providers = new ArrayList<>(providers);
log.info("Profile Providers:");
providers.forEach(p -> log.info("\t- {}", p.getClass().getSimpleName()));
providers.forEach(p -> log.info(" - {}", p.getClass().getSimpleName()));
}
public <T> List<T> getList(Function<ProfileProvider, List<T>> function) {

View File

@@ -178,7 +178,6 @@ public class SessionManager {
}
public void unbind(JsonObject reqData) {
// TODO also check for HS header to know which domain attempting the unbind
if (reqData.entrySet().size() == 2 && reqData.has("mxid") && reqData.has("threepid")) {
/* This is a HS request to remove a 3PID and is considered:
* - An attack on user privacy
@@ -218,11 +217,13 @@ public class SessionManager {
}
}
}
throw new NotAllowedException("You have attempted to alter 3PID bindings, which can only be done by the 3PID owner directly. " +
"We have informed the 3PID owner of your fraudulent attempt.");
}
log.info("Denying request");
throw new NotAllowedException("You have attempted to alter 3PID bindings, which can only be done by the 3PID owner directly. " +
"We have informed the 3PID owner of your fraudulent attempt.");
log.info("Denying unbind request as the endpoint is not defined in the spec.");
throw new NotAllowedException(499, "This endpoint does not exist in the spec and therefore is not supported.");
}
}

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.storage.crypto;
public class Ed2219RegularKeyIdentifier extends RegularKeyIdentifier {
public Ed2219RegularKeyIdentifier(String serial) {
super(KeyAlgorithm.Ed25519, serial);
}
}

View File

@@ -0,0 +1,53 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sàrl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.storage.crypto;
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,140 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sàrl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.storage.crypto;
import io.kamax.matrix.codec.MxBase64;
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;
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.ED_25519);
this.store = store;
if (!store.getCurrentKey().isPresent()) {
List<KeyIdentifier> keys = store.list(KeyType.Regular);
if (keys.isEmpty()) {
keys.add(generateKey(KeyType.Regular));
}
store.setCurrentKey(keys.get(0));
}
}
protected 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);
}
protected String getPrivateKeyBase64(EdDSAPrivateKey key) {
return MxBase64.encode(key.getSeed());
}
public 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);
}
public EdDSAPrivateKeySpec getPrivateKeySpecs(KeyIdentifier id) {
return new EdDSAPrivateKeySpec(java.util.Base64.getDecoder().decode(getKey(id).getPrivateKeyBase64()), keySpecs);
}
public EdDSAPrivateKey getPrivateKey(KeyIdentifier id) {
return new EdDSAPrivateKey(getPrivateKeySpecs(id));
}
public 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,85 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sàrl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.storage.crypto;
import com.google.gson.JsonObject;
import io.kamax.matrix.codec.MxBase64;
import io.kamax.matrix.json.MatrixJson;
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();
// FIXME should create a signing key object what would give this ed and index values
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

@@ -0,0 +1,78 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sàrl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.storage.crypto;
import io.kamax.mxisd.exception.ObjectNotFoundException;
import java.util.List;
import java.util.Optional;
public class FileKeyStore implements KeyStore {
public FileKeyStore(String path) {
}
@Override
public boolean has(KeyIdentifier id) {
return false;
}
@Override
public List<KeyIdentifier> list() {
return null;
}
@Override
public List<KeyIdentifier> list(KeyType type) {
return null;
}
@Override
public Key get(KeyIdentifier id) throws ObjectNotFoundException {
return null;
}
@Override
public void add(Key key) throws IllegalStateException {
}
@Override
public void update(Key key) throws ObjectNotFoundException {
}
@Override
public void delete(KeyIdentifier id) throws ObjectNotFoundException {
}
@Override
public void setCurrentKey(KeyIdentifier id) throws IllegalArgumentException {
}
@Override
public Optional<KeyIdentifier> getCurrentKey() {
return Optional.empty();
}
}

View File

@@ -0,0 +1,51 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sàrl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.storage.crypto;
public class GenericKey implements Key {
private final KeyIdentifier id;
private final boolean isValid;
private final String privKey;
public GenericKey(KeyIdentifier id, boolean isValid, String privKey) {
this.id = new GenericKeyIdentifier(id);
this.isValid = isValid;
this.privKey = privKey;
}
@Override
public KeyIdentifier getId() {
return id;
}
@Override
public boolean isValid() {
return isValid;
}
@Override
public String getPrivateKeyBase64() {
return privKey;
}
}

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.storage.crypto;
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) {
this.type = 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;
}
}

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.storage.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.storage.crypto;
public interface KeyAlgorithm {
String Ed25519 = "ed25519";
}

View File

@@ -0,0 +1,50 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sàrl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.storage.crypto;
/**
* 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();
}

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.storage.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,98 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sàrl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.storage.crypto;
import io.kamax.mxisd.exception.ObjectNotFoundException;
import java.util.List;
import java.util.Optional;
/**
* Store to persist signing keys and the identifier for the current long-term signing key
*/
public interface KeyStore {
/**
* If a given key is currently stored
*
* @param id The Identifier elements for the key
* @return true if the key is stored, false if not
*/
boolean has(KeyIdentifier id);
/**
* List all keys within the store
*
* @return The list of key identifiers
*/
List<KeyIdentifier> list();
/**
* List all keys of a given type within the store
*
* @param type The type to filter on
* @return The list of keys identifiers matching the given type
*/
List<KeyIdentifier> list(KeyType type);
/**
* Get the key that relates to the given identifier
*
* @param id The identifier of the key to get
* @return The key
* @throws ObjectNotFoundException If no key is found for that identifier
*/
Key get(KeyIdentifier id) throws ObjectNotFoundException;
/**
* Add a key to the store
*
* @param key The key to store
* @throws IllegalStateException If a key already exist for the given identifier data
*/
void add(Key key) throws IllegalStateException;
void update(Key key) throws ObjectNotFoundException;
/**
* Delete a key from the store
*
* @param id The key identifier of the key to delete
* @throws ObjectNotFoundException If no key is found for that identifier
*/
void delete(KeyIdentifier id) throws ObjectNotFoundException;
/**
* Store the information of which key is the current signing key
*
* @param id The key identifier
* @throws ObjectNotFoundException If the key is not known to the store
*/
void setCurrentKey(KeyIdentifier id) throws ObjectNotFoundException;
/**
* Retrieve the previously stored information of which key is the current signing key, if any
*
* @return The optional key identifier that was previously stored
*/
Optional<KeyIdentifier> getCurrentKey();
}

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.storage.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,109 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sàrl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.storage.crypto;
import io.kamax.mxisd.exception.ObjectNotFoundException;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class MemoryKeyStore implements KeyStore {
private Map<KeyType, Map<String, Map<String, String>>> keys = new ConcurrentHashMap<>();
private KeyIdentifier current;
private Map<String, String> getMap(KeyType type, String algo) {
return keys.computeIfAbsent(type, k -> new ConcurrentHashMap<>()).computeIfAbsent(algo, k -> new ConcurrentHashMap<>());
}
@Override
public boolean has(KeyIdentifier id) {
return getMap(id.getType(), id.getAlgorithm()).containsKey(id.getSerial());
}
@Override
public List<KeyIdentifier> list() {
List<KeyIdentifier> keyIds = new ArrayList<>();
keys.forEach((key, value) -> value.forEach((key1, value1) -> value1.forEach((key2, value2) -> keyIds.add(new GenericKeyIdentifier(key, key1, key2)))));
return keyIds;
}
@Override
public List<KeyIdentifier> list(KeyType type) {
List<KeyIdentifier> keyIds = new ArrayList<>();
keys.computeIfAbsent(type, t -> new ConcurrentHashMap<>()).forEach((key, value) -> value.forEach((key1, value1) -> keyIds.add(new GenericKeyIdentifier(type, key, key1))));
return keyIds;
}
@Override
public Key get(KeyIdentifier id) throws ObjectNotFoundException {
String data = getMap(id.getType(), id.getAlgorithm()).get(id.getSerial());
if (Objects.isNull(data)) {
throw new ObjectNotFoundException("Key", id.getType() + ":" + id.getAlgorithm() + ":" + id.getSerial());
}
return new GenericKey(new GenericKeyIdentifier(id), StringUtils.isEmpty(data), data);
}
private void set(Key key) {
String data = key.isValid() ? key.getPrivateKeyBase64() : "";
getMap(key.getId().getType(), key.getId().getAlgorithm()).put(key.getId().getSerial(), data);
}
@Override
public void add(Key key) throws IllegalStateException {
if (has(key.getId())) {
throw new IllegalStateException();
}
set(key);
}
@Override
public void update(Key key) throws ObjectNotFoundException {
if (!has(key.getId())) {
throw new ObjectNotFoundException("Key", key.getId().getType() + ":" + key.getId().getAlgorithm() + ":" + key.getId().getSerial());
}
set(key);
}
@Override
public void delete(KeyIdentifier id) throws ObjectNotFoundException {
keys.computeIfAbsent(id.getType(), k -> new ConcurrentHashMap<>()).computeIfAbsent(id.getAlgorithm(), k -> new ConcurrentHashMap<>()).remove(id.getSerial());
}
@Override
public void setCurrentKey(KeyIdentifier id) throws ObjectNotFoundException {
if (!has(id)) {
throw new ObjectNotFoundException("Key", id.getType() + ":" + id.getAlgorithm() + ":" + id.getSerial());
}
current = id;
}
@Override
public Optional<KeyIdentifier> getCurrentKey() {
return Optional.ofNullable(current);
}
}

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.storage.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.storage.crypto;
public interface Signature {
KeyIdentifier getKey();
String getSignature();
}

View File

@@ -0,0 +1,57 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sàrl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.storage.crypto;
import com.google.gson.JsonObject;
import java.nio.charset.StandardCharsets;
public interface SignatureManager {
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
*/
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

@@ -74,13 +74,7 @@ public abstract class GenericTemplateNotificationGenerator extends PlaceholderNo
@Override
public String getForValidation(IThreePidSession session) {
log.info("Generating notification content for 3PID Session validation");
return populateForValidation(session, getTemplateContent(cfg.getSession().getValidation().getLocal()));
}
@Override
public String getForRemoteValidation(IThreePidSession session) {
log.info("Generating notification content for remote-only 3PID session");
return populateForRemoteValidation(session, getTemplateContent(cfg.getSession().getValidation().getRemote()));
return populateForValidation(session, getTemplateContent(cfg.getSession().getValidation()));
}
@Override

View File

@@ -37,8 +37,6 @@ public interface NotificationGenerator {
String getForValidation(IThreePidSession session);
String getForRemoteValidation(IThreePidSession session);
String getForFraudulentUnbind(ThreePid tpid);
}

View File

@@ -72,11 +72,6 @@ public abstract class GenericNotificationHandler<A extends ThreePidConnector, B
send(connector, session.getThreePid().getAddress(), generator.getForValidation(session));
}
@Override
public void sendForRemoteValidation(IThreePidSession session) {
send(connector, session.getThreePid().getAddress(), generator.getForRemoteValidation(session));
}
@Override
public void sendForFraudulentUnbind(ThreePid tpid) {
send(connector, tpid.getAddress(), generator.getForFraudulentUnbind(tpid));

View File

@@ -108,7 +108,7 @@ public class EmailSendGridNotificationHandler extends PlaceholderNotificationGen
@Override
public void sendForValidation(IThreePidSession session) {
EmailTemplate template = cfg.getTemplates().getSession().getValidation().getLocal();
EmailTemplate template = cfg.getTemplates().getSession().getValidation();
Email email = getEmail();
email.setSubject(populateForValidation(session, template.getSubject()));
email.setText(populateForValidation(session, getFromFile(template.getBody().getText())));
@@ -117,17 +117,6 @@ public class EmailSendGridNotificationHandler extends PlaceholderNotificationGen
send(session.getThreePid().getAddress(), email);
}
@Override
public void sendForRemoteValidation(IThreePidSession session) {
EmailTemplate template = cfg.getTemplates().getSession().getValidation().getRemote();
Email email = getEmail();
email.setSubject(populateForRemoteValidation(session, template.getSubject()));
email.setText(populateForRemoteValidation(session, getFromFile(template.getBody().getText())));
email.setHtml(populateForRemoteValidation(session, getFromFile(template.getBody().getHtml())));
send(session.getThreePid().getAddress(), email);
}
@Override
public void sendForFraudulentUnbind(ThreePid tpid) {
EmailTemplate template = cfg.getTemplates().getSession().getUnbind().getFraudulent();

View File

@@ -1,80 +0,0 @@
package io.kamax.mxisd.test;
import com.icegreen.greenmail.util.GreenMail;
import com.icegreen.greenmail.util.ServerSetupTest;
import io.kamax.matrix.MatrixID;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.matrix._MatrixID;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.Mxisd;
import io.kamax.mxisd.as.MatrixIdInvite;
import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.config.threepid.connector.EmailSmtpConfig;
import io.kamax.mxisd.config.threepid.medium.EmailConfig;
import io.kamax.mxisd.threepid.connector.email.EmailSmtpConnector;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.util.Collections;
import static junit.framework.TestCase.assertEquals;
public class MxisdEmailNotifTest {
private final String domain = "localhost";
private Mxisd m;
private GreenMail gm;
@Before
public void before() {
EmailSmtpConfig smtpCfg = new EmailSmtpConfig();
smtpCfg.setPort(3025);
smtpCfg.setLogin("mxisd");
smtpCfg.setPassword("mxisd");
EmailConfig eCfg = new EmailConfig();
eCfg.setConnector(EmailSmtpConnector.ID);
eCfg.getIdentity().setFrom("mxisd@" + domain);
eCfg.getIdentity().setName("Mxisd Server (Unit Test)");
eCfg.getConnectors().put(EmailSmtpConnector.ID, GsonUtil.makeObj(smtpCfg));
MxisdConfig cfg = new MxisdConfig();
cfg.getMatrix().setDomain(domain);
cfg.getKey().setPath(":memory:");
cfg.getStorage().getProvider().getSqlite().setDatabase(":memory:");
cfg.getThreepid().getMedium().put(ThreePidMedium.Email.getId(), GsonUtil.makeObj(eCfg));
m = new Mxisd(cfg);
m.start();
gm = new GreenMail(ServerSetupTest.SMTP_IMAP);
gm.start();
}
@After
public void after() {
gm.stop();
m.stop();
}
@Test
public void forMatrixIdInvite() throws MessagingException {
gm.setUser("mxisd", "mxisd");
_MatrixID sender = MatrixID.asAcceptable("mxisd", domain);
_MatrixID recipient = MatrixID.asAcceptable("john", domain);
MatrixIdInvite idInvite = new MatrixIdInvite("!rid:" + domain, sender, recipient, ThreePidMedium.Email.getId(), "john@" + domain, Collections.emptyMap());
m.getNotif().sendForInvite(idInvite);
assertEquals(1, gm.getReceivedMessages().length);
MimeMessage msg = gm.getReceivedMessages()[0];
assertEquals(1, msg.getFrom().length);
assertEquals("\"Mxisd Server (Unit Test)\" <mxisd@localhost>", msg.getFrom()[0].toString());
assertEquals(1, msg.getRecipients(Message.RecipientType.TO).length);
}
}

View File

@@ -56,32 +56,32 @@ public class ExecDirectoryStoreTest extends ExecStoreTest {
}));
}
private ExecConfig.Directory getCfg() {
ExecConfig.Directory cfg = new ExecConfig().build().getDirectory();
private ExecConfig getCfg() {
ExecConfig cfg = new ExecConfig().build();
assertFalse(cfg.isEnabled());
cfg.setEnabled(true);
assertTrue(cfg.isEnabled());
cfg.getSearch().getByName().getOutput().setType(ExecStore.JsonType);
cfg.getDirectory().getSearch().getByName().getOutput().setType(ExecStore.JsonType);
return cfg;
}
private ExecDirectoryStore getStore(ExecConfig.Directory cfg) {
private ExecDirectoryStore getStore(ExecConfig cfg) {
ExecDirectoryStore store = new ExecDirectoryStore(cfg, getMatrixCfg());
store.setExecutorSupplier(this::build);
return store;
}
private ExecDirectoryStore getStore(String command) {
ExecConfig.Directory cfg = getCfg();
cfg.getSearch().getByName().setCommand(command);
cfg.getSearch().getByThreepid().setCommand(command);
ExecConfig cfg = getCfg();
cfg.getDirectory().getSearch().getByName().setCommand(command);
cfg.getDirectory().getSearch().getByThreepid().setCommand(command);
return getStore(cfg);
}
@Test
public void byNameNoCommandDefined() {
ExecConfig.Directory cfg = getCfg();
assertTrue(StringUtils.isEmpty(cfg.getSearch().getByName().getCommand()));
ExecConfig cfg = getCfg();
assertTrue(StringUtils.isEmpty(cfg.getDirectory().getSearch().getByName().getCommand()));
ExecDirectoryStore store = getStore(cfg);
UserDirectorySearchResult result = store.searchByDisplayName("user");

View File

@@ -62,17 +62,17 @@ public class ExecIdentityStoreTest extends ExecStoreTest {
}));
}
private ExecConfig.Identity getCfg() {
ExecConfig.Identity cfg = new ExecConfig().build().getIdentity();
private ExecConfig getCfg() {
ExecConfig cfg = new ExecConfig().build();
assertFalse(cfg.isEnabled());
cfg.setEnabled(true);
assertTrue(cfg.isEnabled());
cfg.getLookup().getSingle().getOutput().setType(ExecStore.JsonType);
cfg.getLookup().getBulk().getOutput().setType(ExecStore.JsonType);
cfg.getIdentity().getLookup().getSingle().getOutput().setType(ExecStore.JsonType);
cfg.getIdentity().getLookup().getBulk().getOutput().setType(ExecStore.JsonType);
return cfg;
}
private ExecIdentityStore getStore(ExecConfig.Identity cfg) {
private ExecIdentityStore getStore(ExecConfig cfg) {
ExecIdentityStore store = new ExecIdentityStore(cfg, getMatrixCfg());
store.setExecutorSupplier(this::build);
assertTrue(store.isLocal());
@@ -80,9 +80,9 @@ public class ExecIdentityStoreTest extends ExecStoreTest {
}
private ExecIdentityStore getStore(String command) {
ExecConfig.Identity cfg = getCfg();
cfg.getLookup().getSingle().setCommand(command);
cfg.getLookup().getBulk().setCommand(command);
ExecConfig cfg = getCfg();
cfg.getIdentity().getLookup().getSingle().setCommand(command);
cfg.getIdentity().getLookup().getBulk().setCommand(command);
return getStore(cfg);
}

View File

@@ -70,28 +70,28 @@ public class ExecProfileStoreTest extends ExecStoreTest {
}
private ExecConfig.Profile getCfg() {
ExecConfig.Profile cfg = new ExecConfig().build().getProfile();
private ExecConfig getCfg() {
ExecConfig cfg = new ExecConfig().build();
assertFalse(cfg.isEnabled());
cfg.setEnabled(true);
assertTrue(cfg.isEnabled());
cfg.getDisplayName().getOutput().setType(ExecStore.JsonType);
cfg.getThreePid().getOutput().setType(ExecStore.JsonType);
cfg.getRole().getOutput().setType(ExecStore.JsonType);
cfg.getProfile().getDisplayName().getOutput().setType(ExecStore.JsonType);
cfg.getProfile().getThreePid().getOutput().setType(ExecStore.JsonType);
cfg.getProfile().getRole().getOutput().setType(ExecStore.JsonType);
return cfg;
}
private ExecProfileStore getStore(ExecConfig.Profile cfg) {
private ExecProfileStore getStore(ExecConfig cfg) {
ExecProfileStore store = new ExecProfileStore(cfg);
store.setExecutorSupplier(this::build);
return store;
}
private ExecProfileStore getStore(String command) {
ExecConfig.Profile cfg = getCfg();
cfg.getDisplayName().setCommand(command);
cfg.getThreePid().setCommand(command);
cfg.getRole().setCommand(command);
ExecConfig cfg = getCfg();
cfg.getProfile().getDisplayName().setCommand(command);
cfg.getProfile().getThreePid().setCommand(command);
cfg.getProfile().getRole().setCommand(command);
return getStore(cfg);
}

View File

@@ -23,6 +23,7 @@ package io.kamax.mxisd.test.backend.rest;
import com.github.tomakehurst.wiremock.junit.WireMockRule;
import io.kamax.matrix.MatrixID;
import io.kamax.matrix._MatrixID;
import io.kamax.matrix._ThreePid;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.backend.rest.RestProfileProvider;
import io.kamax.mxisd.config.rest.RestBackendConfig;
@@ -34,6 +35,7 @@ import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import java.util.List;
import java.util.Optional;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
@@ -42,27 +44,40 @@ import static org.junit.Assert.*;
public class RestProfileProviderTest {
private static final int MockHttpPort = 65000;
private static final String MockHttpHost = "localhost";
@Rule
public WireMockRule wireMockRule = new WireMockRule(65000);
public WireMockRule wireMockRule = new WireMockRule(MockHttpPort);
private final String displayNameEndpoint = "/displayName";
private final _MatrixID userId = MatrixID.from("john", "matrix.localhost").valid();
private final _MatrixID userId = MatrixID.from("john", "matrix." + MockHttpHost).valid();
private RestProfileProvider p;
@Before
public void before() {
ProfileEndpoints endpoints = new ProfileEndpoints();
endpoints.setDisplayName(displayNameEndpoint);
private RestBackendConfig getCfg(RestBackendConfig.Endpoints endpoints) {
RestBackendConfig cfg = new RestBackendConfig();
cfg.setEnabled(true);
cfg.setHost("http://localhost:65000");
cfg.getEndpoints().setProfile(endpoints);
cfg.setHost("http://" + MockHttpHost + ":" + MockHttpPort);
cfg.setEndpoints(endpoints);
cfg.build();
p = new RestProfileProvider(cfg);
return cfg;
}
private RestProfileProvider get(RestBackendConfig cfg) {
return new RestProfileProvider(cfg);
}
@Before
public void before() {
ProfileEndpoints pEndpoints = new ProfileEndpoints();
pEndpoints.setDisplayName(displayNameEndpoint);
RestBackendConfig.Endpoints endpoints = new RestBackendConfig.Endpoints();
endpoints.setProfile(pEndpoints);
p = get(getCfg(endpoints));
}
@Test
@@ -144,4 +159,26 @@ public class RestProfileProviderTest {
}
}
@Test
public void forEmptyEndpoints() {
ProfileEndpoints pEndpoints = new ProfileEndpoints();
pEndpoints.setDisplayName("");
pEndpoints.setThreepids("");
pEndpoints.setRoles("");
RestBackendConfig.Endpoints endpoints = new RestBackendConfig.Endpoints();
endpoints.setProfile(pEndpoints);
RestProfileProvider p = get(getCfg(endpoints));
Optional<String> dn = p.getDisplayName(userId);
assertFalse(dn.isPresent());
List<String> roles = p.getRoles(userId);
assertTrue(roles.isEmpty());
List<_ThreePid> tpids = p.getThreepids(userId);
assertTrue(tpids.isEmpty());
}
}

View File

@@ -0,0 +1,151 @@
/*
* 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.test.notification;
import com.icegreen.greenmail.util.GreenMail;
import com.icegreen.greenmail.util.ServerSetupTest;
import io.kamax.matrix.MatrixID;
import io.kamax.matrix.ThreePid;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.matrix._MatrixID;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.Mxisd;
import io.kamax.mxisd.as.MatrixIdInvite;
import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.config.threepid.connector.EmailSmtpConfig;
import io.kamax.mxisd.config.threepid.medium.EmailConfig;
import io.kamax.mxisd.threepid.connector.email.EmailSmtpConnector;
import io.kamax.mxisd.threepid.session.ThreePidSession;
import org.apache.commons.lang.RandomStringUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import java.io.IOException;
import java.util.Collections;
import static junit.framework.TestCase.assertEquals;
import static junit.framework.TestCase.assertTrue;
public class EmailNotificationTest {
private final String domain = "localhost";
private final String user = "mxisd";
private final String notifiee = "john";
private final String sender = user + "@" + domain;
private final String senderEmail = "\"Mxisd Server (Unit Test)\" <" + sender + ">";
private final String target = notifiee + "@" + domain;
private Mxisd m;
private GreenMail gm;
@Before
public void before() {
EmailSmtpConfig smtpCfg = new EmailSmtpConfig();
smtpCfg.setPort(3025);
smtpCfg.setLogin(user);
smtpCfg.setPassword(user);
EmailConfig eCfg = new EmailConfig();
eCfg.setConnector(EmailSmtpConnector.ID);
eCfg.getIdentity().setFrom(sender);
eCfg.getIdentity().setName("Mxisd Server (Unit Test)");
eCfg.getConnectors().put(EmailSmtpConnector.ID, GsonUtil.makeObj(smtpCfg));
MxisdConfig cfg = new MxisdConfig();
cfg.getMatrix().setDomain(domain);
cfg.getKey().setPath(":memory:");
cfg.getStorage().getProvider().getSqlite().setDatabase(":memory:");
cfg.getThreepid().getMedium().put(ThreePidMedium.Email.getId(), GsonUtil.makeObj(eCfg));
m = new Mxisd(cfg);
m.start();
gm = new GreenMail(ServerSetupTest.SMTP_IMAP);
gm.start();
}
@After
public void after() {
gm.stop();
m.stop();
}
@Test
public void forMatrixIdInvite() throws MessagingException {
gm.setUser("mxisd", "mxisd");
_MatrixID sender = MatrixID.asAcceptable(user, domain);
_MatrixID recipient = MatrixID.asAcceptable(notifiee, domain);
MatrixIdInvite idInvite = new MatrixIdInvite(
"!rid:" + domain,
sender,
recipient,
ThreePidMedium.Email.getId(),
target,
Collections.emptyMap()
);
m.getNotif().sendForInvite(idInvite);
assertEquals(1, gm.getReceivedMessages().length);
MimeMessage msg = gm.getReceivedMessages()[0];
assertEquals(1, msg.getFrom().length);
assertEquals(senderEmail, msg.getFrom()[0].toString());
assertEquals(1, msg.getRecipients(Message.RecipientType.TO).length);
}
@Test
public void forValidation() throws MessagingException, IOException {
gm.setUser(user, user);
String token = RandomStringUtils.randomAlphanumeric(128);
ThreePidSession session = new ThreePidSession(
"",
"",
new ThreePid(ThreePidMedium.Email.getId(), target),
"",
1,
"",
token
);
m.getNotif().sendForValidation(session);
assertEquals(1, gm.getReceivedMessages().length);
MimeMessage msg = gm.getReceivedMessages()[0];
assertEquals(1, msg.getFrom().length);
assertEquals(senderEmail, msg.getFrom()[0].toString());
assertEquals(1, msg.getRecipients(Message.RecipientType.TO).length);
// We just check on the text/plain one. HTML is multipart and it's difficult so we skip
MimeMultipart content = (MimeMultipart) msg.getContent();
MimeBodyPart mbp = (MimeBodyPart) content.getBodyPart(0);
String mbpContent = mbp.getContent().toString();
assertTrue(mbpContent.contains(token));
}
}

View File

@@ -0,0 +1,31 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sàrl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.test.storage.crypto;
public class KeyTest {
// As per https://matrix.org/docs/spec/appendices.html#signing-key
public static final String Private = "YJDBA9Xnr2sVqXD9Vj7XVUnmFZcZrlw8Md7kMW+3XA1";
// The corresponding public key, not being documented in the spec
public static final String Public = "XGX0JRS2Af3be3knz2fBiRbApjm2Dh61gXDJA8kcJNI";
}

View File

@@ -0,0 +1,102 @@
/*
* 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.test.storage.crypto;
import com.google.gson.JsonObject;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.matrix.json.MatrixJson;
import io.kamax.mxisd.storage.crypto.*;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.assertThat;
public class SignatureManagerTest {
private static SignatureManager signMgr;
private static SignatureManager build(String keySeed) {
Ed25519Key key = new Ed25519Key(new Ed2219RegularKeyIdentifier("0"), keySeed);
KeyStore store = new MemoryKeyStore();
store.add(key);
return new Ed25519SignatureManager(new Ed25519KeyManager(store));
}
@BeforeClass
public static void beforeClass() {
signMgr = build(KeyTest.Private);
}
private void testSign(String value, String sign) {
assertThat(signMgr.sign(value).getSignature(), is(equalTo(sign)));
}
// As per https://matrix.org/docs/spec/appendices.html#json-signing
@Test
public void onEmptyObject() {
String value = "{}";
String sign = "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ";
testSign(value, sign);
}
// As per https://matrix.org/docs/spec/appendices.html#json-signing
@Test
public void onSimpleObject() {
JsonObject data = new JsonObject();
data.addProperty("one", 1);
data.addProperty("two", "Two");
String value = GsonUtil.get().toJson(data);
String sign = "KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw";
testSign(value, sign);
}
@Test
public void onFederationHeader() {
SignatureManager mgr = build("1QblgjFeL3IxoY4DKOR7p5mL5sQTC0ChmeMJlqb4d5M");
JsonObject o = new JsonObject();
o.addProperty("method", "GET");
o.addProperty("uri", "/_matrix/federation/v1/query/directory?room_alias=%23a%3Amxhsd.local.kamax.io%3A8447");
o.addProperty("origin", "synapse.local.kamax.io");
o.addProperty("destination", "mxhsd.local.kamax.io:8447");
String signExpected = "SEMGSOJEsoalrBfHqPO2QrSlbLaUYLHLk4e3q4IJ2JbgvCynT1onp7QF1U4Sl3G3NzybrgdnVvpqcaEgV0WPCw";
Signature signProduced = mgr.sign(o);
assertThat(signProduced.getSignature(), is(equalTo(signExpected)));
}
@Test
public void onIdentityLookup() {
String value = MatrixJson.encodeCanonical("{\n" + " \"address\": \"mxisd-federation-test@kamax.io\",\n"
+ " \"medium\": \"email\",\n" + " \"mxid\": \"@mxisd-lookup-test:kamax.io\",\n"
+ " \"not_after\": 253402300799000,\n" + " \"not_before\": 0,\n" + " \"ts\": 1523482030147\n" + "}");
String sign = "ObKA4PNQh2g6c7Yo2QcTcuDgIwhknG7ZfqmNYzbhrbLBOqZomU22xX9raufN2Y3ke1FXsDqsGs7WBDodmzZJCg";
testSign(value, sign);
}
}