Compare commits

..

18 Commits

Author SHA1 Message Date
Maxime Dor
b46d047411 Add session disabled config 2017-09-24 04:54:48 +02:00
Maxime Dor
542c549e4e Fix config for remote-only sessions mode 2017-09-24 04:53:37 +02:00
Maxime Dor
ebb9a6daa0 Fix links and better formatting 2017-09-24 04:52:24 +02:00
Maxime Dor
f93a94ddf1 First draft of 3PID sessions/binds manual 2017-09-24 04:47:16 +02:00
Maxime Dor
597fc95cef Clean-up 2017-09-24 01:49:56 +02:00
Maxime Dor
df81dda22d First working prototype to proxy 3PID binds to central Matrix.org IS 2017-09-23 04:27:14 +02:00
Maxime Dor
5836965a1e Saving current work 2017-09-22 01:49:27 +02:00
Maxime Dor
58d80b8eb3 Notification for proxying 3PID, remote 3PID are proxied by default 2017-09-22 00:00:25 +02:00
Maxime Dor
a4b4a3f24c Polishing, prepare for proxying 3PID sessions 2017-09-21 07:26:33 +02:00
Maxime Dor
ace6019197 Refactor after first tests against synapse 2017-09-21 04:07:13 +02:00
Maxime Dor
88cefeabbf Fix refactored calls 2017-09-20 17:24:21 +02:00
Maxime Dor
bf2afd8739 Further work 2017-09-20 17:22:51 +02:00
Maxime Dor
0b087ee08c Prepare structure to handle 3PID sessions and bindings validation/proxy 2017-09-20 04:35:34 +02:00
Maxime Dor
c1746697b9 Split template creation and 3PID connector to integrate bindings verification 2017-09-19 03:46:31 +02:00
Maxime Dor
5179c4dbb5 Proper check 2017-09-19 01:08:13 +02:00
Maxime Dor
64973f57cf Better defaults for logging 2017-09-19 01:03:44 +02:00
Maxime Dor
00a00be692 LDAP backend: protect against empty username 2017-09-18 12:51:36 +02:00
Maxime Dor
9e8dade238 Clarify README and REST backend doc 2017-09-18 10:58:27 +02:00
71 changed files with 4162 additions and 627 deletions

View File

@@ -171,17 +171,26 @@ systemctl start mxisd
After following the specific instructions to create a config file from the sample:
1. Set the `matrix.domain` value to the domain value used in your Home Server configuration
2. Set an absolute location for the signing keys using `key.path`
3. Set a location for the default SQLite persistence using `storage.provider.sqlite.database`
4. Configure the E-mail invite sender with items starting in `invite.sender.email`
3. Configure the E-mail invite sender with items starting in `invite.sender.email`
In case your IS public domain does not match your Matrix domain, see `server.name` and `server.publicUrl`
config items.
If you want to use the LDAP backend:
## Backends
### LDAP (AD, Samba, LDAP)
If you want to use LDAP backend as an Identity store:
1. Enable it with `ldap.enabled`
2. Configure connection options using items starting in `ldap.connection`
3. You may want to valid default values for `ldap.attribute` items
### SQL (SQLite, PostgreSQL)
If you want to connect to use a synapse DB (SQLite or PostgreSQL) as Identity store, follow the example config for `sql` config items.
### REST (Webapps/websites integration)
If you want to use the REST backend as an Identity store:
1. Enable it with `rest.enabled`
2. Configure options starting with `rest` and see the dedicated documentation in `docs/backends/rest.md`
# Network Discovery
To allow other federated Identity Server to reach yours, the same algorithm used for Homeservers takes place:

View File

@@ -79,6 +79,9 @@ dependencies {
// Spring Boot - standalone app
compile 'org.springframework.boot:spring-boot-starter-web:1.5.3.RELEASE'
// Thymeleaf for HTML templates
compile "org.springframework.boot:spring-boot-starter-thymeleaf:1.5.3.RELEASE"
// Matrix Java SDK
compile 'io.kamax:matrix-java-sdk:0.0.2'

View File

@@ -1,16 +1,25 @@
# REST backend
The REST backend allows you to query arbitrary REST JSON endpoints as backends for the following flows:
The REST backend allows you to query identity data in existing webapps, like:
- Forums (phpBB, Discourse, etc.)
- Custom Identity stores (Keycloak, ...)
- CRMs (Wordpress, ...)
- self-hosted clouds (Nextcloud, ownCloud, ...)
It supports the following mxisd flows:
- Identity lookup
- Authentication
To integrate this backend with your webapp, you will need to implement three specific REST endpoints detailed below.
## Configuration
| Key | Default | Description |
---------------------------------|---------------------------------------|------------------------------------------------------|
| rest.enabled | false | Globally enable/disable the REST backend |
| rest.host | *empty* | Default base URL to use for the different endpoints. |
| rest.endpoints.auth | /_mxisd/identity/api/v1/auth | Endpoint to validate credentials |
| rest.endpoints.identity.single | /_mxisd/identity/api/v1/lookup/single | Endpoint to lookup a single 3PID |
| rest.endpoints.identity.bulk | /_mxisd/identity/api/v1/lookup/bulk | Endpoint to lookup a list of 3PID |
| rest.endpoints.identity.single | /_mxisd/identity/api/v1/lookup/single | Endpoint to query a single 3PID |
| rest.endpoints.identity.bulk | /_mxisd/identity/api/v1/lookup/bulk | Endpoint to query a list of 3PID |
Endpoint values can handle two formats:
- URL Path starting with `/` that gets happened to the `rest.host`

335
docs/sessions/3pid.md Normal file
View File

@@ -0,0 +1,335 @@
# 3PID Sessions
- [Overview](#overview)
- [Purpose](#purpose)
- [Federation](#federation)
- [3PID scope](#3pid-scope)
- [Session scope](#session-scope)
- [Notifications](#notifications)
- [Email](#email)
- [Usage](#usage)
- [Configuration](#configuration)
- [Scenarios](#scenarios)
- [Default](#default)
- [Local sessions only](#local-sessions-only)
- [Remote sessions only](#remote-sessions-only)
- [Sessions disabled](#sessions-disabled)
## Overview
When adding an email, a phone number or any other kind of 3PID (Third-Party Identifier),
the identity server is called to validate the 3PID.
Once this 3PID is validated, the Homeserver will publish the user Matrix ID on the Identity Server and
add this 3PID to the Matrix account which initiated the request.
## Purpose
This serves two purposes:
- Add the 3PID as an administrative/login info for the Homeserver directly
- Publish, or *Bind*, the 3PID so it can be queried from Homeservers and clients when inviting someone in a room
by a 3PID, allowing it to be resolved to a Matrix ID.
## Federation
Federation is based on the principle that one can get a domain name and serve services and information within that
domain namespace in a way which can be discovered following a specific protocol or specification.
In the Matrix eco-system, some 3PID can be federated (e.g. emails) while some others cannot (phone numbers).
Also, Matrix users might add 3PIDs that would not point to the Identity server that actually holds the 3PID binding.
Example: a user from Homeserver `example.org` adds an email `john@gmail.com`.
If a federated lookup was performed, Identity servers would try to find the 3PID bind at the `gmail.com` server, and
not `example.org`.
To allow global publishing of 3PID bindings to be found anywhere within the current protocol specification, one would
perform a *Remote session* and *Remote bind*, effectively starting a new 3PID session with another Identity server on
behalf of the user.
To ensure lookup works consistency within the current Matrix network, the central Matrix.org Identity Server should be
used to store *remote* sessions and binds.
On the flip side, at the time of writing, the Matrix specification and the central Matrix.org servers do not allow to
remote a 3PID bind. This means that once a 3PID is published (email, phone number, etc.), it cannot be easily remove
and would require contacting the Matrix.org administrators for each bind individually.
This poses a privacy, control and security concern, especially for groups/corporations that want to keep a tight control
on where such identifiers can be made publicly visible.
To ensure full control, validation management rely on two concepts:
- The scope of 3PID being validated
- The scope of 3PID sessions that should be possible/offered
### 3PID scope
3PID can either be scoped as local or remote.
Local means that they can looked up using federation and that such federation call would end up on the local
Identity Server.
Remote means that they cannot be lookup using federation or that a federation call would not end up on the local
Identity Server.
Email addresses can either be local or remote 3PID, depending on the domain. If the address is one from the configured
domain in the Identity server, it will be scoped as local. If it is from another domain, it will be as remote.
Phone number can only be scoped as remote, since there is currently no way to perform DNS queries that would lead back
to the Identity server who validated the phone number.
### Session scope
Sessions can be scoped as:
- Local only - validate 3PIDs directly, do not allow the creation of 3PID sessions on a remote Identity server.
- Local and Remote - validate 3PIDs directly, offer users to option to also validate and bind 3PID on another server.
- Remote only - validate and bind 3PIDs on another server, no validation or bind done locally.
---
**IMPORTANT NOTE:** mxisd does not store bindings directly. While a user can see its email, phone number or any other
3PID in its settings/profile, it does **NOT** mean it is published anywhere and can be used to invite/search the user.
Identity backends (LDAP, REST, SQL) are the ones holding such data.
If you still want added arbitrary 3PIDs to be discoverable on your local server, you will need to link mxisd to your
synapse DB to make it an Identity backend.
See the [Scenarios](#scenarios) for more info on how and why.
## Notifications
3PIDs are validated by sending a pre-formatted message containing a token to that 3PID address, which must be given to the
Identity server that received the request. This is usually done by means of a URL to visit for email or a short number
received by SMS for phone numbers.
mxisd use two components for this:
- Generator which produces the message to be sent with the necessary information the user needs to validate their session.
- Connector which actually send the notification (e.g. SMTP for email).
Built-in generators and connectors for supported 3PID types:
### Email
Generators:
- Template
Connectors:
- SMTP
## Usage
### Configuration
The following example of configuration (incomplete extract) shows which items are relevant for 3PID sessions.
**IMPORTANT:** Most configuration items shown have default values and should not be included in your own configuration
file unless you want to specifically overwrite them.
Please refer to the full example config file to see which keys are mandatory and to be included in your configuration.
```
matrix:
identity:
servers:
root: # Not to be included in config! Already present in default config!
- 'https://matrix.org'
threepid:
medium:
email:
connector: 'smtp'
generator: 'template'
connectors:
smtp:
host: ''
port: 587
tls: 1
login: ''
password: ''
generators:
template: # Not to be included in config! Already present in default config!
invite: 'classpath:email/invite-template.eml'
session:
validation:
local: 'classpath:email/validate-local-template.eml'
remote: 'classpath:email/validate-remote-template.eml'
session:
policy:
validation:
enabled: true
forLocal:
enabled: true
toLocal: true
toRemote:
enabled: true
server: 'configExample' # Not to be included in config! Already present in default config!
forRemote:
enabled: true
toLocal: false
toRemote:
enabled: true
server: 'configExample' # Not to be included in config! Already present in default config!
```
`matrix.identity.servers` is the namespace to configure arbitrary list of Identity servers with a label as parent key.
In the above example, the list with label `configExample` contains a single server entry pointing to `https://matrix.org`.
**NOTE:** The server list is set to `root` by default and should typically NOT be included in your config.
Identity server entry can be of two format:
- URL, bypassing any kind of domain and port discovery
- Domain name as `string`, allowing federated discovery to take place.
The label can be used in other places of the configuration, allowing you to only declare Identity servers once.
---
`threepid.medium.<3PID>` is the namespace to configure 3PID specific items, not directly tied to any other component of
mxisd.
In the above example, only `email` is defined as 3PID type.
Each 3PID namespace comes with 4 configuration key allowing you to configure generators and connectors for notifications:
- `connectors` is a configuration namespace to be used for any connector configuration. Child keys represent the unique
ID for each connector.
- `generators` is a configuration namespace to be used for any generator configuration. Child keys represent the unique
ID for each generator.
- `connector` is given the ID of the connector to be used at runtime.
- `generator` is given the ID of the generator to be used at runtime.
In the above example, emails notifications are generated by the `template` module and sent with the `smtp` module.
mxisd comes with the following IDs built-in:
**Connectors**
- `smtp` for a basic SMTP connector, attempting STARTLS by default.
**Generators**
- `template`, loading content from template files, using built-in mxisd templates by default.
---
`session.policy.validation` is the core configuration to control what users configured to use your Identity server
are allowed to do in terms of 3PID sessions.
The policy is divided contains a global on/off switch for 3PID sessions using `.enabled`
It is also divided into two sections: `forLocal` and `forRemote` which refers to the 3PID scopes.
Each scope is divided into three parts:
- global on/off switch for 3PID sessions using `.enabled`
- `toLocal` allowing or not local 3PID session validations
- `toRemote` allowing or not remote 3PID session validations and to which server such sessions should be sent.
`.server` takes a Matrix Identity server list label. Only the first server in the list is currently used.
If both `toLocal` and `toRemote` are enabled, the user will be offered to initiate a remote session once their 3PID
locally validated.
### Scenarios
It is important to keep in mind that mxisd does not create bindings, irrelevant if a user added a 3PID to their profile.
Instead, when queried for bindings, mxisd will query Identity backends which are responsible to store this kind of information.
This has the side effect that any 3PID added to a user profile which is NOT within a configured and enabled Identity backend
will simply not be usable for search or invites, **even on the same Homeserver!**
mxisd does not store binds on purpose, as one of its primary goal is to ensure maximum compatibility with federation
and the rest of the Matrix ecosystem is preserved.
Nonetheless, because mxisd also aims at offering support for tight control over identity data, it is possible to have
such 3PID bindings available for search and invite queries on the local Homeserver by using the `SQL` backend and
configuring it to use the synapse database. Support for `SQLite` and `PostgreSQL` is available.
See the [Local sessions only](#local-sessions-only) use case for more information on how to configure.
#### Default
By default, mxisd allows the following:
| | Local Session | Remote Session |
|----------------|-------|--------|
| **Local 3PID** | Yes | Yes, offered |
| **Remote 3PID** | No, Remote forced | Yes |
This is usually what people expect and will feel natural to users and does not involve further integration.
This allows to stay in control for e-mail addresses which domain matches your Matrix environment, still making them
discoverable with federation but not recorded in a 3rd party Identity server which is not under your control.
Users still get the possibility to publish globally their address if needed.
Other e-mail addresses and phone number will be redirected to remote sessions to ensure full compatibility with the Matrix
ecosystem and other federated servers.
#### Local sessions only
**NOTE:** This does not affect 3PID lookups (queries to find Matrix IDs) which will remain public due to limitation
in the Matrix protocol.
This configuration ensures maximum confidentiality and privacy.
Typical use cases:
- Private Homeserver, not federated
- Internal Homeserver without direct Internet access
- Custom product based on Matrix which does not federate
No 3PID will be sent to a remote Identity server and all validation will be performed locally.
On the flip side, people with *Remote* 3PID scopes will not be found from other servers.
Use the following values:
```
session:
policy:
validation:
enabled: true
forLocal:
enabled: true
toLocal: true
toRemote:
enabled: false
forRemote:
enabled: true
toLocal: true
toRemote:
enabled: false
```
**IMPORTANT**: When using local-only mode, you will also need to link mxisd to synapse if you want user searches and invites to work.
To do so, add/edit the following configuration keys:
```
sql:
enabled: true
type: 'postgresql'
connection: ''
```
- `sql.enabled` set to `true` to activate the SQL backend.
- `sql.type` can be set to `sqlite` or `postgresql`, depending on your synapse setup.
- `sql.connection` use a JDBC format which is appened after the `jdbc:type:` connection URI.
Example values for each type:
- `sqlite`: `/path/to/homeserver.db`
- `postgresql`: `//localhost/database?user=synapse&password=synapse`
#### Remote sessions only
This configuration ensures all 3PID are made public for maximum compatibility and reach within the Matrix ecosystem, at
the cost of confidentiality and privacy.
Typical use cases:
- Public Homeserver
- Homeserver with registration enabled
Use the following values:
```
session:
policy:
validation:
enabled: true
forLocal:
enabled: true
toLocal: false
toRemote:
enabled: true
forRemote:
enabled: true
toLocal: false
toRemote:
enabled: true
```
#### Sessions disabled
This configuration would disable 3PID session altogether, preventing users from adding emails and/or phone numbers to
their profiles.
This would be used if mxisd is also performing authentication for the Homeserver, typically with synapse and the
[REST Auth module](https://github.com/kamax-io/matrix-synapse-rest-auth).
While this feature is not yet ready in the REST auth module, you would use this configuration mode to auto-populate 3PID
at user login and prevent any further add.
**This mode comes with several important restrictions:**
- This does not prevent users from removing 3PID from their profile. They would be unable to add them back!
- This prevents users from initiating remote session to make their 3PID binds globally visible
It is therefore recommended to not fully disable sessions but instead restrict specific set of 3PID and Session scopes.
Use the following values to enable this mode:
```
session:
policy:
validation:
enabled: false
```

View File

@@ -26,6 +26,10 @@ public class ThreePid {
private String medium;
private String address;
public ThreePid(ThreePid tpid) {
this(tpid.getMedium(), tpid.getAddress());
}
public ThreePid(String medium, String address) {
this.medium = medium;
this.address = address;

View File

@@ -63,6 +63,11 @@ public class LdapAuthProvider extends LdapGenericBackend implements Authenticato
String uidType = getCfg().getAttribute().getUid().getType();
String userFilterValue = StringUtils.equals(LdapThreePidProvider.UID, uidType) ? mxid.getLocalPart() : mxid.getId();
if (StringUtils.isBlank(userFilterValue)) {
log.warn("Username is empty, failing auth");
return BackendAuthResult.failure();
}
String userFilter = "(" + getCfg().getAttribute().getUid().getValue() + "=" + userFilterValue + ")";
if (!StringUtils.isBlank(getCfg().getAuth().getFilter())) {
userFilter = "(&" + getCfg().getAuth().getFilter() + userFilter + ")";

View File

@@ -20,6 +20,7 @@
package io.kamax.mxisd.config;
import com.google.gson.Gson;
import io.kamax.mxisd.exception.ConfigurationException;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
@@ -28,14 +29,38 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Configuration
@ConfigurationProperties("matrix")
public class MatrixConfig {
public static class Identity {
private Map<String, List<String>> servers = new HashMap<>();
public Map<String, List<String>> getServers() {
return servers;
}
public void setServers(Map<String, List<String>> servers) {
this.servers = servers;
}
public List<String> getServers(String label) {
if (!servers.containsKey(label)) {
throw new RuntimeException("No Identity server list with label '" + label + "'");
}
return servers.get(label);
}
}
private Logger log = LoggerFactory.getLogger(MatrixConfig.class);
private String domain;
private Identity identity = new Identity();
public String getDomain() {
return domain;
@@ -45,6 +70,14 @@ public class MatrixConfig {
this.domain = domain;
}
public Identity getIdentity() {
return identity;
}
public void setIdentity(Identity identity) {
this.identity = identity;
}
@PostConstruct
public void build() {
log.info("--- Matrix config ---");
@@ -54,6 +87,8 @@ public class MatrixConfig {
}
log.info("Domain: {}", getDomain());
log.info("Identity:");
log.info("\tServers: {}", new Gson().toJson(identity.getServers()));
}
}

View File

@@ -0,0 +1,173 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config;
import com.google.gson.Gson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Configuration
@ConfigurationProperties("session")
public class SessionConfig {
private static Logger log = LoggerFactory.getLogger(SessionConfig.class);
public static class Policy {
public static class PolicyTemplate {
public static class PolicySource {
public static class PolicySourceRemote {
private boolean enabled;
private String server;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getServer() {
return server;
}
public void setServer(String server) {
this.server = server;
}
}
private boolean enabled;
private boolean toLocal;
private PolicySourceRemote toRemote = new PolicySourceRemote();
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean toLocal() {
return toLocal;
}
public void setToLocal(boolean toLocal) {
this.toLocal = toLocal;
}
public boolean toRemote() {
return toRemote.isEnabled();
}
public PolicySourceRemote getToRemote() {
return toRemote;
}
public void setToRemote(PolicySourceRemote toRemote) {
this.toRemote = toRemote;
}
}
private boolean enabled;
private PolicySource forLocal = new PolicySource();
private PolicySource forRemote = new PolicySource();
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public PolicySource getForLocal() {
return forLocal;
}
public PolicySource forLocal() {
return forLocal;
}
public PolicySource getForRemote() {
return forRemote;
}
public PolicySource forRemote() {
return forRemote;
}
public PolicySource forIf(boolean isLocal) {
return isLocal ? forLocal : forRemote;
}
}
private PolicyTemplate validation = new PolicyTemplate();
public PolicyTemplate getValidation() {
return validation;
}
public void setValidation(PolicyTemplate validation) {
this.validation = validation;
}
}
private MatrixConfig mxCfg;
private Policy policy = new Policy();
@Autowired
public SessionConfig(MatrixConfig mxCfg) {
this.mxCfg = mxCfg;
}
public MatrixConfig getMatrixCfg() {
return mxCfg;
}
public Policy getPolicy() {
return policy;
}
public void setPolicy(Policy policy) {
this.policy = policy;
}
@PostConstruct
public void build() {
log.info("--- Session config ---");
log.info("Global Policy: {}", new Gson().toJson(policy));
}
}

View File

@@ -0,0 +1,42 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.thymeleaf.resourceresolver.FileResourceResolver;
import org.thymeleaf.templateresolver.TemplateResolver;
@Configuration
public class ThymeleafConfig {
@Bean
public TemplateResolver getFileSystemResolver() {
TemplateResolver resolver = new TemplateResolver();
resolver.setPrefix("");
resolver.setSuffix("");
resolver.setCacheable(false);
resolver.setOrder(1);
resolver.setResourceResolver(new FileResourceResolver());
return resolver;
}
}

View File

@@ -0,0 +1,144 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config;
import com.google.gson.Gson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Configuration
@ConfigurationProperties("view")
public class ViewConfig {
private Logger log = LoggerFactory.getLogger(ViewConfig.class);
public static class Session {
public static class Paths {
private String failure;
private String success;
public String getFailure() {
return failure;
}
public void setFailure(String failure) {
this.failure = failure;
}
public String getSuccess() {
return success;
}
public void setSuccess(String success) {
this.success = success;
}
}
public static class Local {
private Paths onTokenSubmit = new Paths();
public Paths getOnTokenSubmit() {
return onTokenSubmit;
}
public void setOnTokenSubmit(Paths onTokenSubmit) {
this.onTokenSubmit = onTokenSubmit;
}
}
public static class Remote {
private Paths onRequest = new Paths();
private Paths onCheck = new Paths();
public Paths getOnRequest() {
return onRequest;
}
public void setOnRequest(Paths onRequest) {
this.onRequest = onRequest;
}
public Paths getOnCheck() {
return onCheck;
}
public void setOnCheck(Paths onCheck) {
this.onCheck = onCheck;
}
}
private Local local = new Local();
private Local localRemote = new Local();
private Remote remote = new Remote();
public Local getLocal() {
return local;
}
public void setLocal(Local local) {
this.local = local;
}
public Local getLocalRemote() {
return localRemote;
}
public void setLocalRemote(Local localRemote) {
this.localRemote = localRemote;
}
public Remote getRemote() {
return remote;
}
public void setRemote(Remote remote) {
this.remote = remote;
}
}
private Session session = new Session();
public Session getSession() {
return session;
}
public void setSession(Session session) {
this.session = session;
}
@PostConstruct
public void build() {
log.info("--- View config ---");
log.info("Session: {}", new Gson().toJson(session));
}
}

View File

@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config.invite.sender;
package io.kamax.mxisd.config.threepid.connector;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
@@ -27,22 +27,18 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.io.File;
@Configuration
@ConfigurationProperties(prefix = "invite.sender.email")
public class EmailSenderConfig {
@ConfigurationProperties(prefix = "threepid.medium.email.connectors.smtp")
public class EmailSmtpConfig {
private Logger log = LoggerFactory.getLogger(EmailSenderConfig.class);
private Logger log = LoggerFactory.getLogger(EmailSmtpConfig.class);
private String host;
private int port;
private int tls;
private String login;
private String password;
private String email;
private String name;
private String template;
public String getHost() {
return host;
@@ -84,52 +80,14 @@ public class EmailSenderConfig {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getTemplate() {
return template;
}
public void setTemplate(String template) {
this.template = template;
}
@PostConstruct
private void postConstruct() {
log.info("--- E-mail Invite Sender config ---");
public void build() {
log.info("--- E-mail SMTP Connector config ---");
log.info("Host: {}", getHost());
log.info("Port: {}", getPort());
log.info("TLS Mode: {}", getTls());
log.info("Login: {}", getLogin());
log.info("Has password: {}", StringUtils.isBlank(getPassword()));
log.info("E-mail: {}", getEmail());
if (!StringUtils.startsWith(getTemplate(), "classpath:")) {
if (StringUtils.isBlank(getTemplate())) {
log.warn("invite.sender.template is empty! Will not send invites");
} else {
File cp = new File(getTemplate()).getAbsoluteFile();
log.info("Template: {}", cp.getAbsolutePath());
if (!cp.exists() || !cp.isFile() || !cp.canRead()) {
log.warn(getTemplate() + " does not exist, is not a file or cannot be read");
}
}
} else {
log.info("Template: Built-in");
}
log.info("Has password: {}", StringUtils.isNotBlank(getPassword()));
}
}

View File

@@ -0,0 +1,116 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config.threepid.medium;
import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.exception.ConfigurationException;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.WordUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Configuration
@ConfigurationProperties("threepid.medium.email")
public class EmailConfig {
public static class Identity {
private String from;
private String name;
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
private String generator;
private String connector;
private Logger log = LoggerFactory.getLogger(EmailConfig.class);
private MatrixConfig mxCfg;
private Identity identity = new Identity();
@Autowired
public EmailConfig(MatrixConfig mxCfg) {
this.mxCfg = mxCfg;
}
public Identity getIdentity() {
return identity;
}
public String getGenerator() {
return generator;
}
public void setGenerator(String generator) {
this.generator = generator;
}
public String getConnector() {
return connector;
}
public void setConnector(String connector) {
this.connector = connector;
}
@PostConstruct
public void build() {
log.info("--- E-mail config ---");
if (StringUtils.isBlank(getGenerator())) {
throw new ConfigurationException("generator");
}
if (StringUtils.isBlank(getConnector())) {
throw new ConfigurationException("connector");
}
log.info("From: {}", identity.getFrom());
if (StringUtils.isBlank(identity.getName())) {
identity.setName(WordUtils.capitalize(mxCfg.getDomain()) + " Identity Server");
}
log.info("Name: {}", identity.getName());
log.info("Generator: {}", getGenerator());
log.info("Connector: {}", getConnector());
}
}

View File

@@ -0,0 +1,107 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config.threepid.medium;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Configuration
@ConfigurationProperties("threepid.medium.email.generators.template")
public class EmailTemplateConfig {
private static Logger log = LoggerFactory.getLogger(EmailTemplateConfig.class);
private static final String classpathPrefix = "classpath:";
private static String getName(String path) {
if (StringUtils.startsWith(path, classpathPrefix)) {
return "Built-in (" + path.substring(classpathPrefix.length()) + ")";
}
return path;
}
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;
}
}
private SessionValidation validation;
public SessionValidation getValidation() {
return validation;
}
public void setValidation(SessionValidation validation) {
this.validation = validation;
}
}
private String invite;
private Session session = new Session();
public String getInvite() {
return invite;
}
public void setInvite(String invite) {
this.invite = invite;
}
public Session getSession() {
return session;
}
@PostConstruct
public void 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()));
}
}

View File

@@ -23,7 +23,9 @@ package io.kamax.mxisd.controller.v1;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.exception.MappingAlreadyExistsException;
import io.kamax.mxisd.exception.MatrixException;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -33,6 +35,8 @@ import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.Instant;
@ControllerAdvice
@ResponseBody
@@ -50,6 +54,23 @@ public class DefaultExceptionHandler {
return gson.toJson(obj);
}
@ExceptionHandler(InternalServerError.class)
public String handle(InternalServerError e, HttpServletResponse response) {
if (StringUtils.isNotBlank(e.getInternalReason())) {
log.error("Reference #{} - {}", e.getReference(), e.getInternalReason());
} else {
log.error("Reference #{}", e);
}
return handleGeneric(e, response);
}
@ExceptionHandler(MatrixException.class)
public String handleGeneric(MatrixException e, HttpServletResponse response) {
response.setStatus(e.getStatus());
return handle(e.getErrorCode(), e.getError());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MissingServletRequestParameterException.class)
public String handle(MissingServletRequestParameterException e) {
@@ -72,7 +93,14 @@ public class DefaultExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public String handle(HttpServletRequest req, RuntimeException e) {
log.error("Unknown error when handling {}", req.getRequestURL(), e);
return handle("M_UNKNOWN", StringUtils.defaultIfBlank(e.getMessage(), "An uknown error occured. Contact the server administrator if this persists."));
return handle(
"M_UNKNOWN",
StringUtils.defaultIfBlank(
e.getMessage(),
"An internal server error occured. If this error persists, please contact support with reference #" +
Instant.now().toEpochMilli()
)
);
}
}

View File

@@ -20,142 +20,62 @@
package io.kamax.mxisd.controller.v1
import com.google.gson.Gson
import com.google.gson.JsonObject
import io.kamax.mxisd.controller.v1.io.SessionEmailTokenRequestJson
import io.kamax.mxisd.controller.v1.io.SessionPhoneTokenRequestJson
import io.kamax.mxisd.exception.BadRequestException
import io.kamax.mxisd.invitation.InvitationManager
import io.kamax.mxisd.lookup.ThreePidValidation
import io.kamax.mxisd.mapping.MappingManager
import org.apache.commons.io.IOUtils
import org.apache.commons.lang.StringUtils
import org.apache.http.HttpStatus
import io.kamax.mxisd.config.ServerConfig
import io.kamax.mxisd.config.ViewConfig
import io.kamax.mxisd.controller.v1.remote.RemoteIdentityAPIv1
import io.kamax.mxisd.session.SessionMananger
import io.kamax.mxisd.session.ValidationResult
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.*
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import java.nio.charset.StandardCharsets
@RestController
@CrossOrigin
@RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@Controller
@RequestMapping(path = IdentityAPIv1.BASE)
class SessionController {
@Autowired
private MappingManager mgr
@Autowired
private InvitationManager invMgr;
private Gson gson = new Gson()
private Logger log = LoggerFactory.getLogger(SessionController.class)
private <T> T fromJson(HttpServletRequest req, Class<T> obj) {
gson.fromJson(new InputStreamReader(req.getInputStream(), StandardCharsets.UTF_8), obj)
}
@Autowired
private ServerConfig srvCfg;
@RequestMapping(value = "/validate/{medium}/requestToken")
String init(HttpServletRequest request, HttpServletResponse response, @PathVariable String medium) {
log.info("Requested: {}", request.getRequestURL(), request.getQueryString())
@Autowired
private SessionMananger mgr
if (StringUtils.equals("email", medium)) {
SessionEmailTokenRequestJson req = fromJson(request, SessionEmailTokenRequestJson.class)
return gson.toJson(new Sid(mgr.create(req)))
}
if (StringUtils.equals("msisdn", medium)) {
SessionPhoneTokenRequestJson req = fromJson(request, SessionPhoneTokenRequestJson.class)
return gson.toJson(new Sid(mgr.create(req)))
}
JsonObject obj = new JsonObject();
obj.addProperty("errcode", "M_INVALID_3PID_TYPE")
obj.addProperty("error", medium + " is not supported as a 3PID type")
response.setStatus(HttpStatus.SC_BAD_REQUEST)
return gson.toJson(obj)
}
@Autowired
private ViewConfig viewCfg;
@RequestMapping(value = "/validate/{medium}/submitToken")
String validate(HttpServletRequest request,
@RequestParam String sid,
@RequestParam("client_secret") String secret, @RequestParam String token) {
String validate(
HttpServletRequest request,
HttpServletResponse response,
@RequestParam String sid,
@RequestParam("client_secret") String secret,
@RequestParam String token,
Model model
) {
log.info("Requested: {}?{}", request.getRequestURL(), request.getQueryString())
mgr.validate(sid, secret, token)
return "{}"
}
@RequestMapping(value = "/3pid/getValidated3pid")
String check(HttpServletRequest request, HttpServletResponse response,
@RequestParam String sid, @RequestParam("client_secret") String secret) {
log.info("Requested: {}?{}", request.getRequestURL(), request.getQueryString())
Optional<ThreePidValidation> result = mgr.getValidated(sid, secret)
if (result.isPresent()) {
log.info("requested session was validated")
ThreePidValidation pid = result.get()
JsonObject obj = new JsonObject()
obj.addProperty("medium", pid.getMedium())
obj.addProperty("address", pid.getAddress())
obj.addProperty("validated_at", pid.getValidation().toEpochMilli())
return gson.toJson(obj);
ValidationResult r = mgr.validate(sid, secret, token)
log.info("Session {} was validated", sid)
if (r.getNextUrl().isPresent()) {
String url = srvCfg.getPublicUrl() + r.getNextUrl().get()
log.info("Session {} validation: next URL is present, redirecting to {}", sid, url)
response.sendRedirect(url)
} else {
log.info("requested session was not validated")
JsonObject obj = new JsonObject()
obj.addProperty("errcode", "M_SESSION_NOT_VALIDATED")
obj.addProperty("error", "sid, secret or session not valid")
response.setStatus(HttpStatus.SC_BAD_REQUEST)
return gson.toJson(obj)
}
}
@RequestMapping(value = "/3pid/bind")
String bind(HttpServletRequest request, HttpServletResponse response,
@RequestParam String sid, @RequestParam("client_secret") String secret, @RequestParam String mxid) {
String data = IOUtils.toString(request.getReader())
log.info("Requested: {}", request.getRequestURL(), request.getQueryString())
try {
mgr.bind(sid, secret, mxid)
return "{}"
} catch (BadRequestException e) {
log.info("requested session was not validated")
JsonObject obj = new JsonObject()
obj.addProperty("errcode", "M_SESSION_NOT_VALIDATED")
obj.addProperty("error", e.getMessage())
response.setStatus(HttpStatus.SC_BAD_REQUEST)
return gson.toJson(obj)
} finally {
// If a user registers, there is no standard login event. Instead, this is the only way to trigger
// resolution at an appropriate time. Meh at synapse/Riot!
invMgr.lookupMappingsForInvites()
}
}
private class Sid {
private String sid;
public Sid(String sid) {
setSid(sid);
}
String getSid() {
return sid
}
void setSid(String sid) {
this.sid = sid
if (r.isCanRemote()) {
String url = srvCfg.getPublicUrl() + RemoteIdentityAPIv1.getRequestToken(r.getSession().getId(), r.getSession().getSecret());
model.addAttribute("remoteSessionLink", url)
return viewCfg.getSession().getLocalRemote().getOnTokenSubmit().getSuccess()
} else {
return viewCfg.getSession().getLocal().getOnTokenSubmit().getSuccess()
}
}
}

View File

@@ -0,0 +1,159 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.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.controller.v1;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.mxisd.ThreePid;
import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.config.ViewConfig;
import io.kamax.mxisd.controller.v1.io.SessionEmailTokenRequestJson;
import io.kamax.mxisd.controller.v1.io.SessionPhoneTokenRequestJson;
import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.exception.SessionNotValidatedException;
import io.kamax.mxisd.invitation.InvitationManager;
import io.kamax.mxisd.lookup.ThreePidValidation;
import io.kamax.mxisd.session.SessionMananger;
import io.kamax.mxisd.util.GsonParser;
import org.apache.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@RestController
@CrossOrigin
@RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public class SessionRestController {
private Logger log = LoggerFactory.getLogger(SessionRestController.class);
private class Sid { // FIXME replace with RequestTokenResponse
private String sid;
public Sid(String sid) {
setSid(sid);
}
String getSid() {
return sid;
}
void setSid(String sid) {
this.sid = sid;
}
}
@Autowired
private ServerConfig srvCfg;
@Autowired
private SessionMananger mgr;
@Autowired
private InvitationManager invMgr;
@Autowired
private ViewConfig viewCfg;
private Gson gson = new Gson();
private GsonParser parser = new GsonParser(gson);
@RequestMapping(value = "/validate/{medium}/requestToken")
String init(HttpServletRequest request, HttpServletResponse response, @PathVariable String medium) throws IOException {
log.info("Request {}: {}", request.getMethod(), request.getRequestURL(), request.getQueryString());
if (ThreePidMedium.Email.is(medium)) {
SessionEmailTokenRequestJson req = parser.parse(request, SessionEmailTokenRequestJson.class);
return gson.toJson(new Sid(mgr.create(
request.getRemoteHost(),
new ThreePid(req.getMedium(), req.getValue()),
req.getSecret(),
req.getAttempt(),
req.getNextLink())));
}
if (ThreePidMedium.PhoneNumber.is(medium)) {
SessionPhoneTokenRequestJson req = parser.parse(request, SessionPhoneTokenRequestJson.class);
return gson.toJson(new Sid(mgr.create(
request.getRemoteHost(),
new ThreePid(req.getMedium(), req.getValue()),
req.getSecret(),
req.getAttempt(),
req.getNextLink())));
}
JsonObject obj = new JsonObject();
obj.addProperty("errcode", "M_INVALID_3PID_TYPE");
obj.addProperty("error", medium + " is not supported as a 3PID type");
response.setStatus(HttpStatus.SC_BAD_REQUEST);
return gson.toJson(obj);
}
@RequestMapping(value = "/3pid/getValidated3pid")
String check(HttpServletRequest request, HttpServletResponse response,
@RequestParam String sid, @RequestParam("client_secret") String secret) {
log.info("Requested: {}", request.getRequestURL(), request.getQueryString());
try {
ThreePidValidation pid = mgr.getValidated(sid, secret);
JsonObject obj = new JsonObject();
obj.addProperty("medium", pid.getMedium());
obj.addProperty("address", pid.getAddress());
obj.addProperty("validated_at", pid.getValidation().toEpochMilli());
return gson.toJson(obj);
} catch (SessionNotValidatedException e) {
log.info("Session {} was requested but has not yet been validated", sid);
throw e;
}
}
@RequestMapping(value = "/3pid/bind")
String bind(HttpServletRequest request, HttpServletResponse response,
@RequestParam String sid, @RequestParam("client_secret") String secret, @RequestParam String mxid) {
log.info("Requested: {}", request.getRequestURL(), request.getQueryString());
try {
mgr.bind(sid, secret, mxid);
return "{}";
} catch (BadRequestException e) {
log.info("requested session was not validated");
JsonObject obj = new JsonObject();
obj.addProperty("errcode", "M_SESSION_NOT_VALIDATED");
obj.addProperty("error", e.getMessage());
response.setStatus(HttpStatus.SC_BAD_REQUEST);
return gson.toJson(obj);
} finally {
// If a user registers, there is no standard login event. Instead, this is the only way to trigger
// resolution at an appropriate time. Meh at synapse/Riot!
invMgr.lookupMappingsForInvites();
}
}
}

View File

@@ -20,13 +20,11 @@
package io.kamax.mxisd.controller.v1.io;
import io.kamax.mxisd.mapping.MappingSession;
public abstract class GenericTokenRequestJson implements MappingSession {
public abstract class GenericTokenRequestJson {
private String client_secret;
private int send_attempt;
private String id_server;
private String next_link;
public String getSecret() {
return client_secret;
@@ -36,8 +34,8 @@ public abstract class GenericTokenRequestJson implements MappingSession {
return send_attempt;
}
public String getServer() {
return id_server;
public String getNextLink() {
return next_link;
}
}

View File

@@ -18,14 +18,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.invitation.sender;
package io.kamax.mxisd.controller.v1.io;
import io.kamax.mxisd.invitation.IThreePidInviteReply;
public class RequestTokenResponse {
public interface IInviteSender {
private String sid;
String getMedium();
void send(IThreePidInviteReply invite);
public String getSid() {
return sid;
}
}

View File

@@ -24,12 +24,10 @@ public class SessionEmailTokenRequestJson extends GenericTokenRequestJson {
private String email;
@Override
public String getMedium() {
return "email";
}
@Override
public String getValue() {
return email;
}

View File

@@ -31,12 +31,10 @@ public class SessionPhoneTokenRequestJson extends GenericTokenRequestJson {
private String country;
private String phone_number;
@Override
public String getMedium() {
return "msisdn";
}
@Override
public String getValue() {
try {
Phonenumber.PhoneNumber num = phoneUtil.parse(phone_number, country);

View File

@@ -0,0 +1,37 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.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.controller.v1.remote;
public class RemoteIdentityAPIv1 {
public static final String BASE = "/_matrix/identity/remote/api/v1";
public static final String SESSION_REQUEST_TOKEN = BASE + "/validate/requestToken";
public static final String SESSION_CHECK = BASE + "/validate/check";
public static String getRequestToken(String id, String secret) {
return SESSION_REQUEST_TOKEN + "?sid=" + id + "&client_secret=" + secret;
}
public static String getSessionCheck(String id, String secret) {
return SESSION_CHECK + "?sid=" + id + "&client_secret=" + secret;
}
}

View File

@@ -0,0 +1,59 @@
package io.kamax.mxisd.controller.v1.remote;
import io.kamax.mxisd.config.ViewConfig;
import io.kamax.mxisd.exception.SessionNotValidatedException;
import io.kamax.mxisd.session.SessionMananger;
import io.kamax.mxisd.threepid.session.IThreePidSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import javax.servlet.http.HttpServletRequest;
import static io.kamax.mxisd.controller.v1.remote.RemoteIdentityAPIv1.SESSION_CHECK;
import static io.kamax.mxisd.controller.v1.remote.RemoteIdentityAPIv1.SESSION_REQUEST_TOKEN;
@Controller
public class RemoteSessionController {
private Logger log = LoggerFactory.getLogger(RemoteSessionController.class);
@Autowired
private ViewConfig viewCfg;
@Autowired
private SessionMananger mgr;
@RequestMapping(path = SESSION_REQUEST_TOKEN)
public String requestToken(
HttpServletRequest request,
@RequestParam String sid,
@RequestParam("client_secret") String secret,
Model model
) {
log.info("Request {}: {}", request.getMethod(), request.getRequestURL());
IThreePidSession session = mgr.createRemote(sid, secret);
model.addAttribute("checkLink", RemoteIdentityAPIv1.getSessionCheck(session.getId(), session.getSecret()));
return viewCfg.getSession().getRemote().getOnRequest().getSuccess();
}
@RequestMapping(path = SESSION_CHECK)
public String check(
HttpServletRequest request,
@RequestParam String sid,
@RequestParam("client_secret") String secret) {
log.info("Request {}: {}", request.getMethod(), request.getRequestURL());
try {
mgr.validateRemote(sid, secret);
return viewCfg.getSession().getRemote().getOnCheck().getSuccess();
} catch (SessionNotValidatedException e) {
return viewCfg.getSession().getRemote().getOnCheck().getFailure();
}
}
}

View File

@@ -0,0 +1,54 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.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.exception;
import org.apache.http.HttpStatus;
import java.time.Instant;
public class InternalServerError extends MatrixException {
private String reference = Long.toString(Instant.now().toEpochMilli());
private String internalReason;
public InternalServerError() {
super(
HttpStatus.SC_INTERNAL_SERVER_ERROR,
"M_UNKNOWN",
"An internal server error occured. If this error persists, please contact support with reference #" +
Instant.now().toEpochMilli()
);
}
public InternalServerError(String internalReason) {
this();
this.internalReason = internalReason;
}
public String getReference() {
return reference;
}
public String getInternalReason() {
return internalReason;
}
}

View File

@@ -0,0 +1,33 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.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.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value = HttpStatus.FORBIDDEN)
public class InvalidCredentialsException extends RuntimeException {
public InvalidCredentialsException() {
super("Supplied credentials are invalid");
}
}

View File

@@ -0,0 +1,47 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.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.exception;
public abstract class MatrixException extends MxisdException {
private int status;
private String errorCode;
private String error;
public MatrixException(int status, String errorCode, String error) {
this.status = status;
this.errorCode = errorCode;
this.error = error;
}
public int getStatus() {
return status;
}
public String getErrorCode() {
return errorCode;
}
public String getError() {
return error;
}
}

View File

@@ -0,0 +1,24 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.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.exception;
public class MxisdException extends RuntimeException {
}

View File

@@ -0,0 +1,32 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.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.exception;
import org.apache.http.HttpStatus;
public class NotAllowedException extends MatrixException {
public NotAllowedException(String s) {
super(HttpStatus.SC_FORBIDDEN, "M_FORBIDDEN", s);
}
}

View File

@@ -24,5 +24,10 @@ import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.ResponseStatus
@ResponseStatus(value = HttpStatus.NOT_IMPLEMENTED)
class NotImplementedException extends RuntimeException {
public class NotImplementedException extends RuntimeException {
public NotImplementedException(String s) {
super(s);
}
}

View File

@@ -0,0 +1,33 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.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.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class ObjectNotFoundException extends RuntimeException {
public ObjectNotFoundException(String type, String id) {
super(type + " with ID " + id + " does not exist");
}
}

View File

@@ -0,0 +1,31 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.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.exception;
import org.apache.http.HttpStatus;
public class RemoteIdentityServerException extends MatrixException {
public RemoteIdentityServerException(String error) {
super(HttpStatus.SC_SERVICE_UNAVAILABLE, "M_REMOTE_IS_ERROR", error);
}
}

View File

@@ -0,0 +1,31 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.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.exception;
import org.apache.http.HttpStatus;
public class SessionNotValidatedException extends MatrixException {
public SessionNotValidatedException() {
super(HttpStatus.SC_OK, "M_SESSION_NOT_VALIDATED", "This validation session has not yet been completed");
}
}

View File

@@ -0,0 +1,33 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.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.exception;
public class SessionUnknownException extends MatrixException {
public SessionUnknownException() {
this("No valid session was found matching that sid and client secret");
}
public SessionUnknownException(String error) {
super(200, "M_NO_VALID_SESSION", error);
}
}

View File

@@ -26,10 +26,10 @@ import io.kamax.mxisd.config.DnsOverwrite;
import io.kamax.mxisd.config.DnsOverwriteEntry;
import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.exception.MappingAlreadyExistsException;
import io.kamax.mxisd.invitation.sender.IInviteSender;
import io.kamax.mxisd.lookup.SingleLookupReply;
import io.kamax.mxisd.lookup.ThreePidMapping;
import io.kamax.mxisd.lookup.strategy.LookupStrategy;
import io.kamax.mxisd.notification.NotificationManager;
import io.kamax.mxisd.signature.SignatureManager;
import io.kamax.mxisd.storage.IStorage;
import io.kamax.mxisd.storage.ormlite.ThreePidInviteIO;
@@ -83,14 +83,15 @@ public class InvitationManager {
@Autowired
private DnsOverwrite dns;
private Map<String, IInviteSender> senders;
private NotificationManager notifMgr;
private CloseableHttpClient client;
private Gson gson;
private Timer refreshTimer;
private String getId(IThreePidInvite invite) {
return invite.getSender().getDomain().toLowerCase() + invite.getMedium().toLowerCase() + invite.getAddress().toLowerCase();
@Autowired
public InvitationManager(NotificationManager notifMgr) {
this.notifMgr = notifMgr;
}
@PostConstruct
@@ -140,9 +141,14 @@ public class InvitationManager {
@PreDestroy
private void preDestroy() {
refreshTimer.cancel();
ForkJoinPool.commonPool().awaitQuiescence(1, TimeUnit.MINUTES);
}
private String getId(IThreePidInvite invite) {
return invite.getSender().getDomain().toLowerCase() + invite.getMedium().toLowerCase() + invite.getAddress().toLowerCase();
}
private String getIdForLog(IThreePidInviteReply reply) {
return reply.getInvite().getSender().getId() + ":" + reply.getInvite().getRoomId() + ":" + reply.getInvite().getMedium() + ":" + reply.getInvite().getAddress();
}
@@ -193,21 +199,14 @@ public class InvitationManager {
return "https://" + domain + ":8448";
}
@Autowired
public InvitationManager(List<IInviteSender> senderList) {
senders = new HashMap<>();
senderList.forEach(sender -> senders.put(sender.getMedium(), sender));
}
public synchronized IThreePidInviteReply storeInvite(IThreePidInvite invitation) { // TODO better sync
IInviteSender sender = senders.get(invitation.getMedium());
if (sender == null) {
if (!notifMgr.isMediumSupported(invitation.getMedium())) {
throw new BadRequestException("Medium type " + invitation.getMedium() + " is not supported");
}
String invId = getId(invitation);
log.info("Handling invite for {}:{} from {} in room {}", invitation.getMedium(), invitation.getAddress(), invitation.getSender(), invitation.getRoomId());
if (invitations.containsKey(invId)) { // FIXME we need to lookup using the HS domain too!!
if (invitations.containsKey(invId)) {
log.info("Invite is already pending for {}:{}, returning data", invitation.getMedium(), invitation.getAddress());
return invitations.get(invId);
}
@@ -224,7 +223,7 @@ public class InvitationManager {
IThreePidInviteReply reply = new ThreePidInviteReply(invId, invitation, token, displayName);
log.info("Performing invite to {}:{}", invitation.getMedium(), invitation.getAddress());
sender.send(reply);
notifMgr.sendForInvite(reply);
log.info("Storing invite under ID {}", invId);
storage.insertInvite(reply);

View File

@@ -1,138 +0,0 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.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.invitation.sender;
import com.sun.mail.smtp.SMTPTransport;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.config.invite.sender.EmailSenderConfig;
import io.kamax.mxisd.exception.ConfigurationException;
import io.kamax.mxisd.invitation.IThreePidInviteReply;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.WordUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.Date;
@Component
public class EmailInviteSender implements IInviteSender {
private Logger log = LoggerFactory.getLogger(EmailInviteSender.class);
@Autowired
private EmailSenderConfig cfg;
@Autowired
private MatrixConfig mxCfg;
@Autowired
private ApplicationContext app;
private Session session;
private InternetAddress sender;
@PostConstruct
private void postConstruct() {
try {
session = Session.getInstance(System.getProperties());
sender = new InternetAddress(cfg.getEmail(), cfg.getName());
} catch (UnsupportedEncodingException e) {
// What are we supposed to do with this?!
throw new ConfigurationException(e);
}
}
@Override
public String getMedium() {
return ThreePidMedium.Email.getId();
}
@Override
public void send(IThreePidInviteReply invite) {
if (!ThreePidMedium.Email.is(invite.getInvite().getMedium())) {
throw new IllegalArgumentException(invite.getInvite().getMedium() + " is not a supported 3PID type");
}
try {
String domainPretty = WordUtils.capitalizeFully(mxCfg.getDomain());
String senderName = invite.getInvite().getProperties().getOrDefault("sender_display_name", "");
String senderNameOrId = StringUtils.defaultIfBlank(senderName, invite.getInvite().getSender().getId());
String roomName = invite.getInvite().getProperties().getOrDefault("room_name", "");
String roomNameOrId = StringUtils.defaultIfBlank(roomName, invite.getInvite().getRoomId());
String templateBody = IOUtils.toString(
StringUtils.startsWith(cfg.getTemplate(), "classpath:") ?
app.getResource(cfg.getTemplate()).getInputStream() : new FileInputStream(cfg.getTemplate()),
StandardCharsets.UTF_8);
templateBody = templateBody.replace("%DOMAIN%", mxCfg.getDomain());
templateBody = templateBody.replace("%DOMAIN_PRETTY%", domainPretty);
templateBody = templateBody.replace("%FROM_EMAIL%", cfg.getEmail());
templateBody = templateBody.replace("%FROM_NAME%", cfg.getName());
templateBody = templateBody.replace("%SENDER_ID%", invite.getInvite().getSender().getId());
templateBody = templateBody.replace("%SENDER_NAME%", senderName);
templateBody = templateBody.replace("%SENDER_NAME_OR_ID%", senderNameOrId);
templateBody = templateBody.replace("%INVITE_MEDIUM%", invite.getInvite().getMedium());
templateBody = templateBody.replace("%INVITE_ADDRESS%", invite.getInvite().getAddress());
templateBody = templateBody.replace("%ROOM_ID%", invite.getInvite().getRoomId());
templateBody = templateBody.replace("%ROOM_NAME%", roomName);
templateBody = templateBody.replace("%ROOM_NAME_OR_ID%", roomNameOrId);
MimeMessage msg = new MimeMessage(session, IOUtils.toInputStream(templateBody, StandardCharsets.UTF_8));
msg.setHeader("X-Mailer", "mxisd"); // TODO set version
msg.setSentDate(new Date());
msg.setFrom(sender);
msg.setRecipients(Message.RecipientType.TO, invite.getInvite().getAddress());
msg.saveChanges();
log.info("Sending invite to {} via SMTP using {}:{}", invite.getInvite().getAddress(), cfg.getHost(), cfg.getPort());
SMTPTransport transport = (SMTPTransport) session.getTransport("smtp");
transport.setStartTLS(cfg.getTls() > 0);
transport.setRequireStartTLS(cfg.getTls() > 1);
log.info("Connecting to {}:{}", cfg.getHost(), cfg.getPort());
transport.connect(cfg.getHost(), cfg.getPort(), cfg.getLogin(), cfg.getPassword());
try {
transport.sendMessage(msg, InternetAddress.parse(invite.getInvite().getAddress()));
log.info("Invite to {} was sent", invite.getInvite().getAddress());
} finally {
transport.close();
}
} catch (IOException | MessagingException e) {
throw new RuntimeException("Unable to send e-mail invite to " + invite.getInvite().getAddress(), e);
}
}
}

View File

@@ -28,8 +28,8 @@ public class ThreePidValidation extends ThreePid {
private Instant validation;
public ThreePidValidation(String medium, String address, Instant validation) {
super(medium, address);
public ThreePidValidation(ThreePid tpid, Instant validation) {
super(tpid);
this.validation = validation;
}

View File

@@ -25,14 +25,12 @@ import io.kamax.mxisd.lookup.SingleLookupReply
import io.kamax.mxisd.lookup.SingleLookupRequest
import io.kamax.mxisd.lookup.ThreePidMapping
import io.kamax.mxisd.lookup.fetcher.IRemoteIdentityServerFetcher
import io.kamax.mxisd.matrix.IdentityServerUtils
import org.apache.commons.lang.StringUtils
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
import org.xbill.DNS.Lookup
import org.xbill.DNS.SRVRecord
import org.xbill.DNS.Type
import java.util.concurrent.ForkJoinPool
import java.util.concurrent.RecursiveTask
@@ -64,10 +62,6 @@ class DnsLookupProvider implements IThreePidProvider {
return 10
}
String getSrvRecordName(String domain) {
return "_matrix-identity._tcp." + domain
}
Optional<String> getDomain(String email) {
int atIndex = email.lastIndexOf("@")
if (atIndex == -1) {
@@ -84,44 +78,7 @@ class DnsLookupProvider implements IThreePidProvider {
return Optional.empty()
}
log.info("Performing SRV lookup")
String lookupDns = getSrvRecordName(domain)
log.info("Lookup name: {}", lookupDns)
SRVRecord[] records = (SRVRecord[]) new Lookup(lookupDns, Type.SRV).run()
if (records != null) {
Arrays.sort(records, new Comparator<SRVRecord>() {
@Override
int compare(SRVRecord o1, SRVRecord o2) {
return Integer.compare(o1.getPriority(), o2.getPriority())
}
})
for (SRVRecord record : records) {
log.info("Found SRV record: {}", record.toString())
String baseUrl = "https://${record.getTarget().toString(true)}:${record.getPort()}"
if (fetcher.isUsable(baseUrl)) {
log.info("Found Identity Server for domain {} at {}", domain, baseUrl)
return Optional.of(baseUrl)
} else {
log.info("{} is not a usable Identity Server", baseUrl)
}
}
} else {
log.info("No SRV record for {}", lookupDns)
}
log.info("Performing basic lookup using domain name {}", domain)
String baseUrl = "https://" + domain
if (fetcher.isUsable(baseUrl)) {
log.info("Found Identity Server for domain {} at {}", domain, baseUrl)
return Optional.of(baseUrl)
} else {
log.info("{} is not a usable Identity Server", baseUrl)
return Optional.empty()
}
return IdentityServerUtils.findIsUrlForDomain(domain)
}
@Override

View File

@@ -28,6 +28,7 @@ import io.kamax.mxisd.lookup.SingleLookupReply
import io.kamax.mxisd.lookup.SingleLookupRequest
import io.kamax.mxisd.lookup.ThreePidMapping
import io.kamax.mxisd.lookup.fetcher.IRemoteIdentityServerFetcher
import io.kamax.mxisd.matrix.IdentityServerUtils
import org.apache.http.HttpEntity
import org.apache.http.HttpResponse
import org.apache.http.client.HttpClient
@@ -46,36 +47,13 @@ import org.springframework.stereotype.Component
@Lazy
public class RemoteIdentityServerFetcher implements IRemoteIdentityServerFetcher {
public static final String THREEPID_TEST_MEDIUM = "email"
public static final String THREEPID_TEST_ADDRESS = "john.doe@example.org"
private Logger log = LoggerFactory.getLogger(RemoteIdentityServerFetcher.class)
private JsonSlurper json = new JsonSlurper()
@Override
boolean isUsable(String remote) {
try {
HttpURLConnection rootSrvConn = (HttpURLConnection) new URL(
"${remote}/_matrix/identity/api/v1/lookup?medium=${THREEPID_TEST_MEDIUM}&address=${THREEPID_TEST_ADDRESS}"
).openConnection()
// TODO turn this into a configuration property
rootSrvConn.setConnectTimeout(2000)
if (rootSrvConn.getResponseCode() != 200) {
return false
}
def output = json.parseText(rootSrvConn.getInputStream().getText())
if (output['address']) {
return false
}
return true
} catch (IOException | JsonException e) {
log.info("{} is not a usable Identity Server: {}", remote, e.getMessage())
return false
}
return IdentityServerUtils.isUsable(remote)
}
@Override

View File

@@ -32,6 +32,10 @@ interface LookupStrategy {
Optional<SingleLookupReply> find(String medium, String address, boolean recursive)
Optional<SingleLookupReply> findLocal(String medium, String address);
Optional<SingleLookupReply> findRemote(String medium, String address);
Optional<SingleLookupReply> find(SingleLookupRequest request)
Optional<SingleLookupReply> findRecursive(SingleLookupRequest request)

View File

@@ -118,17 +118,44 @@ class RecursivePriorityLookupStrategy implements LookupStrategy, InitializingBea
}).collect(Collectors.toList())
}
@Override
Optional<SingleLookupReply> find(String medium, String address, boolean recursive) {
List<IThreePidProvider> getRemoteProviders() {
return providers.stream().filter(new Predicate<IThreePidProvider>() {
@Override
boolean test(IThreePidProvider iThreePidProvider) {
return iThreePidProvider.isEnabled() && !iThreePidProvider.isLocal()
}
}).collect(Collectors.toList())
}
private static SingleLookupRequest build(String medium, String address) {
SingleLookupRequest req = new SingleLookupRequest();
req.setType(medium)
req.setThreePid(address)
req.setRequester("Internal")
return find(req, recursive)
return req;
}
@Override
Optional<SingleLookupReply> find(String medium, String address, boolean recursive) {
return find(build(medium, address), recursive)
}
@Override
Optional<SingleLookupReply> findLocal(String medium, String address) {
return find(build(medium, address), getLocalProviders())
}
@Override
Optional<SingleLookupReply> findRemote(String medium, String address) {
return find(build(medium, address), getRemoteProviders())
}
Optional<SingleLookupReply> find(SingleLookupRequest request, boolean forceRecursive) {
for (IThreePidProvider provider : listUsableProviders(request, forceRecursive)) {
return find(request, listUsableProviders(request, forceRecursive));
}
Optional<SingleLookupReply> find(SingleLookupRequest request, List<IThreePidProvider> providers) {
for (IThreePidProvider provider : providers) {
Optional<SingleLookupReply> lookupDataOpt = provider.find(request)
if (lookupDataOpt.isPresent()) {
return lookupDataOpt

View File

@@ -1,175 +0,0 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.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.mapping;
import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.lookup.ThreePidValidation;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
@Component
public class MappingManager {
private Logger log = LoggerFactory.getLogger(MappingManager.class);
private Map<String, Session> sessions = new HashMap<>();
private Timer cleaner;
MappingManager() {
cleaner = new Timer();
cleaner.schedule(new TimerTask() {
@Override
public void run() {
List<Session> sList = new ArrayList<>(sessions.values());
for (Session s : sList) {
if (s.timestamp.plus(24, ChronoUnit.HOURS).isBefore(Instant.now())) { // TODO config timeout
log.info("Session {} is obsolete, removing", s.sid);
sessions.remove(s.sid);
}
}
}
}, 0, 10 * 1000); // TODO config delay
}
public String create(MappingSession data) {
String sid;
do {
sid = Long.toString(System.currentTimeMillis());
} while (sessions.containsKey(sid));
String threePidHash = data.getMedium() + data.getValue();
// TODO think how to handle different requests for the same e-mail
Session session = new Session(sid, threePidHash, data);
sessions.put(sid, session);
log.info("Created new session {} to validate {} {}", sid, session.medium, session.address);
return sid;
}
public void validate(String sid, String secret, String token) {
Session s = sessions.get(sid);
if (s == null || !StringUtils.equals(s.secret, secret)) {
throw new BadRequestException("sid or secret are not valid");
}
// TODO actually check token
s.isValidated = true;
s.validationTimestamp = Instant.now();
}
public Optional<ThreePidValidation> getValidated(String sid, String secret) {
Session s = sessions.get(sid);
if (s != null && StringUtils.equals(s.secret, secret)) {
return Optional.of(new ThreePidValidation(s.medium, s.address, s.validationTimestamp));
}
return Optional.empty();
}
public void bind(String sid, String secret, String mxid) {
Session s = sessions.get(sid);
if (s == null || !StringUtils.equals(s.secret, secret)) {
throw new BadRequestException("sid or secret are not valid");
}
log.info("Performed bind for mxid {}", mxid);
// TODO perform bind, whatever it is
}
private class Session {
private String sid;
private String hash;
private Instant timestamp;
private Instant validationTimestamp;
private boolean isValidated;
private String secret;
private String medium;
private String address;
public Session(String sid, String hash, MappingSession data) {
this.sid = sid;
this.hash = hash;
timestamp = Instant.now();
validationTimestamp = Instant.now();
secret = data.getSecret();
medium = data.getMedium();
address = data.getValue();
}
public Instant getTimestamp() {
return timestamp;
}
public void setTimestamp(Instant timestamp) {
this.timestamp = timestamp;
}
public Instant getValidationTimestamp() {
return validationTimestamp;
}
public void setValidationTimestamp(Instant validationTimestamp) {
this.validationTimestamp = validationTimestamp;
}
public boolean isValidated() {
return isValidated;
}
public void setValidated(boolean validated) {
isValidated = validated;
}
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public String getMedium() {
return medium;
}
public void setMedium(String medium) {
this.medium = medium;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
}

View File

@@ -0,0 +1,114 @@
package io.kamax.mxisd.matrix;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xbill.DNS.Lookup;
import org.xbill.DNS.SRVRecord;
import org.xbill.DNS.TextParseException;
import org.xbill.DNS.Type;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Optional;
// FIXME placeholder, this must go in matrix-java-sdk for 1.0
public class IdentityServerUtils {
public static final String THREEPID_TEST_MEDIUM = "email";
public static final String THREEPID_TEST_ADDRESS = "mxisd-email-forever-unknown@forever-invalid.kamax.io";
private static Logger log = LoggerFactory.getLogger(IdentityServerUtils.class);
private static JsonParser parser = new JsonParser();
public static boolean isUsable(String remote) {
try {
// FIXME use Apache HTTP client
HttpURLConnection rootSrvConn = (HttpURLConnection) new URL(
remote + "/_matrix/identity/api/v1/lookup?medium=" + THREEPID_TEST_MEDIUM + "&address=" + THREEPID_TEST_ADDRESS
).openConnection();
// TODO turn this into a configuration property
rootSrvConn.setConnectTimeout(2000);
if (rootSrvConn.getResponseCode() != 200) {
return false;
}
JsonElement el = parser.parse(IOUtils.toString(rootSrvConn.getInputStream(), StandardCharsets.UTF_8));
if (!el.isJsonObject()) {
log.debug("IS {} did not send back a JSON object for single 3PID lookup");
return false;
}
if (el.getAsJsonObject().has("address")) {
log.debug("IS {} did not send back a JSON object for single 3PID lookup");
return false;
}
return true;
} catch (IOException | JsonParseException e) {
log.info("{} is not a usable Identity Server: {}", remote, e.getMessage());
return false;
}
}
public static String getSrvRecordName(String domain) {
return "_matrix-identity._tcp." + domain;
}
public static Optional<String> findIsUrlForDomain(String domainOrUrl) {
try {
try {
domainOrUrl = new URL(domainOrUrl).getHost();
} catch (MalformedURLException e) {
log.info("{} is not an URL, using as-is", domainOrUrl);
}
log.info("Discovery Identity Server for {}", domainOrUrl);
log.info("Performing SRV lookup");
String lookupDns = getSrvRecordName(domainOrUrl);
log.info("Lookup name: {}", lookupDns);
SRVRecord[] records = (SRVRecord[]) new Lookup(lookupDns, Type.SRV).run();
if (records != null) {
Arrays.sort(records, Comparator.comparingInt(SRVRecord::getPriority));
for (SRVRecord record : records) {
log.info("Found SRV record: {}", record.toString());
String baseUrl = "https://${record.getTarget().toString(true)}:${record.getPort()}";
if (isUsable(baseUrl)) {
log.info("Found Identity Server for domain {} at {}", domainOrUrl, baseUrl);
return Optional.of(baseUrl);
} else {
log.info("{} is not a usable Identity Server", baseUrl);
return Optional.empty();
}
}
} else {
log.info("No SRV record for {}", lookupDns);
}
log.info("Performing basic lookup using domain name {}", domainOrUrl);
String baseUrl = "https://" + domainOrUrl;
if (isUsable(baseUrl)) {
log.info("Found Identity Server for domain {} at {}", domainOrUrl, baseUrl);
return Optional.of(baseUrl);
} else {
log.info("{} is not a usable Identity Server", baseUrl);
return Optional.empty();
}
} catch (TextParseException e) {
log.warn(domainOrUrl + " is not a valid domain name");
return Optional.empty();
}
}
}

View File

@@ -0,0 +1,36 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.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.notification;
import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.threepid.session.IThreePidSession;
public interface INotificationHandler {
String getMedium();
void sendForInvite(IThreePidInviteReply invite);
void sendForValidation(IThreePidSession session);
void sendForRemoteValidation(IThreePidSession session);
}

View File

@@ -0,0 +1,69 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.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.notification;
import io.kamax.mxisd.exception.NotImplementedException;
import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.threepid.session.IThreePidSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Component
public class NotificationManager {
private Map<String, INotificationHandler> handlers;
@Autowired
public NotificationManager(List<INotificationHandler> handlers) {
this.handlers = new HashMap<>();
handlers.forEach(h -> this.handlers.put(h.getMedium(), h));
}
private INotificationHandler ensureMedium(String medium) {
INotificationHandler handler = handlers.get(medium);
if (handler == null) {
throw new NotImplementedException(medium + " is not a supported 3PID medium type");
}
return handler;
}
public boolean isMediumSupported(String medium) {
return handlers.containsKey(medium);
}
public void sendForInvite(IThreePidInviteReply invite) {
ensureMedium(invite.getInvite().getMedium()).sendForInvite(invite);
}
public void sendForValidation(IThreePidSession session) {
ensureMedium(session.getThreePid().getMedium()).sendForValidation(session);
}
public void sendforRemoteValidation(IThreePidSession session) {
ensureMedium(session.getThreePid().getMedium()).sendForRemoteValidation(session);
}
}

View File

@@ -0,0 +1,339 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.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.session;
import com.google.gson.JsonObject;
import io.kamax.matrix.MatrixID;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.matrix._MatrixID;
import io.kamax.mxisd.ThreePid;
import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.config.SessionConfig;
import io.kamax.mxisd.controller.v1.io.RequestTokenResponse;
import io.kamax.mxisd.controller.v1.remote.RemoteIdentityAPIv1;
import io.kamax.mxisd.exception.*;
import io.kamax.mxisd.lookup.ThreePidValidation;
import io.kamax.mxisd.matrix.IdentityServerUtils;
import io.kamax.mxisd.notification.NotificationManager;
import io.kamax.mxisd.storage.IStorage;
import io.kamax.mxisd.storage.dao.IThreePidSessionDao;
import io.kamax.mxisd.threepid.session.IThreePidSession;
import io.kamax.mxisd.threepid.session.ThreePidSession;
import io.kamax.mxisd.util.GsonParser;
import io.kamax.mxisd.util.RestClientUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import static io.kamax.mxisd.config.SessionConfig.Policy.PolicyTemplate;
import static io.kamax.mxisd.config.SessionConfig.Policy.PolicyTemplate.PolicySource;
@Component
public class SessionMananger {
private Logger log = LoggerFactory.getLogger(SessionMananger.class);
private SessionConfig cfg;
private MatrixConfig mxCfg;
private IStorage storage;
private NotificationManager notifMgr;
// FIXME export into central class, set version
private CloseableHttpClient client = HttpClients.custom().setUserAgent("mxisd").build();
@Autowired
public SessionMananger(SessionConfig cfg, MatrixConfig mxCfg, IStorage storage, NotificationManager notifMgr) {
this.cfg = cfg;
this.mxCfg = mxCfg;
this.storage = storage;
this.notifMgr = notifMgr;
}
private boolean isLocal(ThreePid tpid) {
if (!ThreePidMedium.Email.is(tpid.getMedium())) { // We can only handle E-mails for now
return false;
}
String domain = tpid.getAddress().split("@")[1];
return StringUtils.equalsIgnoreCase(cfg.getMatrixCfg().getDomain(), domain);
}
private ThreePidSession getSession(String sid, String secret) {
Optional<IThreePidSessionDao> dao = storage.getThreePidSession(sid);
if (!dao.isPresent() || !StringUtils.equals(dao.get().getSecret(), secret)) {
throw new SessionUnknownException();
}
return new ThreePidSession(dao.get());
}
private ThreePidSession getSessionIfValidated(String sid, String secret) {
ThreePidSession session = getSession(sid, secret);
if (!session.isValidated()) {
throw new SessionNotValidatedException();
}
return session;
}
public String create(String server, ThreePid tpid, String secret, int attempt, String nextLink) {
PolicyTemplate policy = cfg.getPolicy().getValidation();
if (!policy.isEnabled()) {
throw new NotAllowedException("Validating 3PID is disabled globally");
}
synchronized (this) {
log.info("Server {} is asking to create session for {} (Attempt #{}) - Next link: {}", server, tpid, attempt, nextLink);
Optional<IThreePidSessionDao> dao = storage.findThreePidSession(tpid, secret);
if (dao.isPresent()) {
ThreePidSession session = new ThreePidSession(dao.get());
log.info("We already have a session for {}: {}", tpid, session.getId());
if (session.getAttempt() < attempt) {
log.info("Received attempt {} is greater than stored attempt {}, sending validation communication", attempt, session.getAttempt());
notifMgr.sendForValidation(session);
log.info("Sent validation notification to {}", tpid);
session.increaseAttempt();
storage.updateThreePidSession(session.getDao());
}
return session.getId();
} else {
log.info("No existing session for {}", tpid);
boolean isLocal = isLocal(tpid);
log.info("Is 3PID bound to local domain? {}", isLocal);
// This might need a configuration by medium type?
PolicySource policySource = policy.forIf(isLocal);
if (!policySource.isEnabled() || (!policySource.toLocal() && !policySource.toRemote())) {
log.info("Session for {}: cancelled due to policy", tpid);
throw new NotAllowedException("Validating " + (isLocal ? "local" : "remote") + " 3PID is not allowed");
}
String sessionId;
do {
sessionId = Long.toString(System.currentTimeMillis());
} while (storage.getThreePidSession(sessionId).isPresent());
String token = RandomStringUtils.randomNumeric(6);
ThreePidSession session = new ThreePidSession(sessionId, server, tpid, secret, attempt, nextLink, token);
log.info("Generated new session {} to validate {} from server {}", sessionId, tpid, server);
// This might need a configuration by medium type?
if (policySource.toLocal()) {
log.info("Session {} for {}: sending local validation notification", sessionId, tpid);
notifMgr.sendForValidation(session);
} else {
log.info("Session {} for {}: sending remote-only validation notification", sessionId, tpid);
notifMgr.sendforRemoteValidation(session);
}
storage.insertThreePidSession(session.getDao());
log.info("Stored session {}", sessionId, tpid, server);
return sessionId;
}
}
}
public ValidationResult validate(String sid, String secret, String token) {
ThreePidSession session = getSession(sid, secret);
log.info("Attempting validation for session {} from {}", session.getId(), session.getServer());
boolean isLocal = isLocal(session.getThreePid());
PolicySource policy = cfg.getPolicy().getValidation().forIf(isLocal);
if (!policy.isEnabled()) {
throw new NotAllowedException("Validating " + (isLocal ? "local" : "remote") + " 3PID is not allowed");
}
session.validate(token);
storage.updateThreePidSession(session.getDao());
log.info("Session {} has been validated", session.getId());
// FIXME definitely doable in a nicer way
ValidationResult r = new ValidationResult(session, policy.toRemote());
if (!policy.toLocal()) {
r.setNextUrl(RemoteIdentityAPIv1.getRequestToken(sid, secret));
} else {
session.getNextLink().ifPresent(r::setNextUrl);
}
return r;
}
public ThreePidValidation getValidated(String sid, String secret) {
ThreePidSession session = getSessionIfValidated(sid, secret);
return new ThreePidValidation(session.getThreePid(), session.getValidationTime());
}
public void bind(String sid, String secret, String mxidRaw) {
_MatrixID mxid = new MatrixID(mxidRaw);
ThreePidSession session = getSessionIfValidated(sid, secret);
if (!session.isRemote()) {
log.info("Session {} for {}: MXID {} was bound locally", sid, session.getThreePid(), mxid);
return;
}
log.info("Session {} for {}: MXID {} bind is remote", sid, session.getThreePid(), mxid);
if (!session.isRemoteValidated()) {
log.error("Session {} for {}: Not validated remotely", sid, session.getThreePid());
throw new SessionNotValidatedException();
}
log.info("Session {} for {}: Performing remote bind", sid, session.getThreePid());
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(
Arrays.asList(
new BasicNameValuePair("sid", session.getRemoteId()),
new BasicNameValuePair("client_secret", session.getRemoteSecret()),
new BasicNameValuePair("mxid", mxid.getId())
), StandardCharsets.UTF_8);
HttpPost bindReq = new HttpPost(session.getRemoteServer() + "/_matrix/identity/api/v1/3pid/bind");
bindReq.setEntity(entity);
try (CloseableHttpResponse response = client.execute(bindReq)) {
int status = response.getStatusLine().getStatusCode();
if (status < 200 || status >= 300) {
String body = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
log.error("Session {} for {}: Remote IS {} failed when trying to bind {} for remote session {}\n{}",
sid, session.getThreePid(), session.getRemoteServer(), mxid, session.getRemoteId(), body);
throw new RemoteIdentityServerException(body);
}
log.error("Session {} for {}: MXID {} was bound remotely", sid, session.getThreePid(), mxid);
} catch (IOException e) {
log.error("Session {} for {}: I/O Error when trying to bind mxid {}", sid, session.getThreePid(), mxid);
throw new RemoteIdentityServerException(e.getMessage());
}
}
public IThreePidSession createRemote(String sid, String secret) {
ThreePidSession session = getSessionIfValidated(sid, secret);
log.info("Creating remote 3PID session for {} with local session [{}] to {}", session.getThreePid(), sid);
boolean isLocal = isLocal(session.getThreePid());
PolicySource policy = cfg.getPolicy().getValidation().forIf(isLocal);
if (!policy.isEnabled() || !policy.toRemote()) {
throw new NotAllowedException("Validating " + (isLocal ? "local" : "remote") + " 3PID is not allowed");
}
log.info("Remote 3PID is allowed by policy");
List<String> servers = mxCfg.getIdentity().getServers(policy.getToRemote().getServer());
if (servers.isEmpty()) {
throw new InternalServerError();
}
String url = IdentityServerUtils.findIsUrlForDomain(servers.get(0)).orElseThrow(InternalServerError::new);
log.info("Will use IS endpoint {}", url);
String remoteSecret = session.isRemote() ? session.getRemoteSecret() : RandomStringUtils.randomAlphanumeric(16);
JsonObject body = new JsonObject();
body.addProperty("client_secret", remoteSecret);
body.addProperty(session.getThreePid().getMedium(), session.getThreePid().getAddress());
body.addProperty("send_attempt", session.increaseAndGetRemoteAttempt());
log.info("Requesting remote session with attempt {}", session.getRemoteAttempt());
HttpPost tokenReq = RestClientUtils.post(url + "/_matrix/identity/api/v1/validate/" + session.getThreePid().getMedium() + "/requestToken", body);
try (CloseableHttpResponse response = client.execute(tokenReq)) {
int status = response.getStatusLine().getStatusCode();
if (status < 200 || status >= 300) {
throw new RemoteIdentityServerException("Remote identity server returned with status " + status);
}
RequestTokenResponse data = new GsonParser().parse(response, RequestTokenResponse.class);
log.info("Remote Session ID: {}", data.getSid());
session.setRemoteData(url, data.getSid(), remoteSecret, 1);
storage.updateThreePidSession(session.getDao());
log.info("Updated Session {} with remote data", sid);
return session;
} catch (IOException e) {
log.warn("Failed to create remote session with {} for {}: {}", url, session.getThreePid(), e.getMessage());
throw new RemoteIdentityServerException(e.getMessage());
}
}
public void validateRemote(String sid, String secret) {
ThreePidSession session = getSessionIfValidated(sid, secret);
if (!session.isRemote()) {
throw new NotAllowedException("Cannot remotely validate a local session");
}
log.info("Session {} for {}: Validating remote 3PID session {} on {}", sid, session.getThreePid(), session.getRemoteId(), session.getRemoteServer());
if (session.isRemoteValidated()) {
log.info("Session {} for {}: Already remotely validated", sid, session.getThreePid());
return;
}
HttpGet validateReq = new HttpGet(session.getRemoteServer() + "/_matrix/identity/api/v1/3pid/getValidated3pid?sid=" + session.getRemoteId() + "&client_secret=" + session.getRemoteSecret());
try (CloseableHttpResponse response = client.execute(validateReq)) {
int status = response.getStatusLine().getStatusCode();
if (status < 200 || status >= 300) {
throw new RemoteIdentityServerException("Remote identity server returned with status " + status);
}
JsonObject o = new GsonParser().parse(response.getEntity().getContent());
if (o.has("errcode")) {
String errcode = o.get("errcode").getAsString();
if (StringUtils.equals("M_SESSION_NOT_VALIDATED", errcode)) {
throw new SessionNotValidatedException();
} else if (StringUtils.equals("M_NO_VALID_SESSION", errcode)) {
throw new SessionUnknownException();
} else {
throw new RemoteIdentityServerException("Unknown error while validating Remote 3PID session: " + errcode + " - " + o.get("error").getAsString());
}
}
if (o.has("validated_at")) {
ThreePid remoteThreePid = new ThreePid(o.get("medium").getAsString(), o.get("address").getAsString());
if (session.getThreePid().equals(remoteThreePid)) { // sanity check
throw new InternalServerError("Local 3PID " + session.getThreePid() + " and remote 3PID " + remoteThreePid + " do not match for session " + session.getId());
}
log.info("Session {} for {}: Remotely validated successfully", sid, session.getThreePid());
session.validateRemote();
storage.updateThreePidSession(session.getDao());
log.info("Session {} was updated in storage", sid);
}
} catch (IOException e) {
log.warn("Session {} for {}: Failed to validated remotely on {}: {}", sid, session.getThreePid(), session.getRemoteServer(), e.getMessage());
throw new RemoteIdentityServerException(e.getMessage());
}
}
}

View File

@@ -0,0 +1,54 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.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.session;
import io.kamax.mxisd.threepid.session.IThreePidSession;
import java.util.Optional;
public class ValidationResult {
private IThreePidSession session;
private boolean canRemote;
private String nextUrl;
public ValidationResult(IThreePidSession session, boolean canRemote) {
this.session = session;
this.canRemote = canRemote;
}
public IThreePidSession getSession() {
return session;
}
public boolean isCanRemote() {
return canRemote;
}
public Optional<String> getNextUrl() {
return Optional.ofNullable(nextUrl);
}
public void setNextUrl(String nextUrl) {
this.nextUrl = nextUrl;
}
}

View File

@@ -20,10 +20,13 @@
package io.kamax.mxisd.storage;
import io.kamax.mxisd.ThreePid;
import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.storage.dao.IThreePidSessionDao;
import io.kamax.mxisd.storage.ormlite.ThreePidInviteIO;
import java.util.Collection;
import java.util.Optional;
public interface IStorage {
@@ -33,4 +36,12 @@ public interface IStorage {
void deleteInvite(String id);
Optional<IThreePidSessionDao> getThreePidSession(String sid);
Optional<IThreePidSessionDao> findThreePidSession(ThreePid tpid, String secret);
void insertThreePidSession(IThreePidSessionDao session);
void updateThreePidSession(IThreePidSessionDao session);
}

View File

@@ -0,0 +1,59 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.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.dao;
public interface IThreePidSessionDao {
String getId();
long getCreationTime();
String getServer();
String getMedium();
String getAddress();
String getSecret();
int getAttempt();
String getNextLink();
String getToken();
boolean getValidated();
long getValidationTime();
boolean isRemote();
String getRemoteServer();
String getRemoteId();
String getRemoteSecret();
int getRemoteAttempt();
boolean isRemoteValidated();
}

View File

@@ -26,8 +26,14 @@ import com.j256.ormlite.dao.DaoManager;
import com.j256.ormlite.jdbc.JdbcConnectionSource;
import com.j256.ormlite.support.ConnectionSource;
import com.j256.ormlite.table.TableUtils;
import io.kamax.mxisd.ThreePid;
import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.storage.IStorage;
import io.kamax.mxisd.storage.dao.IThreePidSessionDao;
import io.kamax.mxisd.storage.ormlite.dao.ThreePidSessionDao;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
@@ -35,59 +41,141 @@ import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
public class OrmLiteSqliteStorage implements IStorage {
private Logger log = LoggerFactory.getLogger(OrmLiteSqliteStorage.class);
@FunctionalInterface
private interface Getter<T> {
T get() throws SQLException, IOException;
}
@FunctionalInterface
private interface Doer {
void run() throws SQLException, IOException;
}
private Dao<ThreePidInviteIO, String> invDao;
private Dao<ThreePidSessionDao, String> sessionDao;
OrmLiteSqliteStorage(String path) {
try {
withCatcher(() -> {
File parent = new File(path).getParentFile();
if (!parent.mkdirs() && !parent.isDirectory()) {
throw new RuntimeException("Unable to create DB parent directory: " + parent);
}
ConnectionSource connPool = new JdbcConnectionSource("jdbc:sqlite:" + path);
invDao = DaoManager.createDao(connPool, ThreePidInviteIO.class);
TableUtils.createTableIfNotExists(connPool, ThreePidInviteIO.class);
} catch (SQLException e) {
invDao = createDaoAndTable(connPool, ThreePidInviteIO.class);
sessionDao = createDaoAndTable(connPool, ThreePidSessionDao.class);
});
}
private <V, K> Dao<V, K> createDaoAndTable(ConnectionSource connPool, Class<V> c) throws SQLException {
Dao<V, K> dao = DaoManager.createDao(connPool, c);
TableUtils.createTableIfNotExists(connPool, c);
return dao;
}
private <T> T withCatcher(Getter<T> g) {
try {
return g.get();
} catch (SQLException | IOException e) {
throw new RuntimeException(e); // FIXME do better
}
}
private void withCatcher(Doer d) {
try {
d.run();
} catch (SQLException | IOException e) {
throw new RuntimeException(e); // FIXME do better
}
}
private <T> List<T> forIterable(CloseableWrappedIterable<? extends T> t) {
return withCatcher(() -> {
try {
List<T> ioList = new ArrayList<>();
t.forEach(ioList::add);
return ioList;
} finally {
t.close();
}
});
}
@Override
public Collection<ThreePidInviteIO> getInvites() {
try (CloseableWrappedIterable<ThreePidInviteIO> t = invDao.getWrappedIterable()) {
List<ThreePidInviteIO> ioList = new ArrayList<>();
t.forEach(ioList::add);
return ioList;
} catch (IOException e) {
throw new RuntimeException(e); // FIXME do better
}
return forIterable(invDao.getWrappedIterable());
}
@Override
public void insertInvite(IThreePidInviteReply data) {
try {
withCatcher(() -> {
int updated = invDao.create(new ThreePidInviteIO(data));
if (updated != 1) {
throw new RuntimeException("Unexpected row count after DB action: " + updated);
}
} catch (SQLException e) {
throw new RuntimeException(e); // FIXME do better
}
});
}
@Override
public void deleteInvite(String id) {
try {
withCatcher(() -> {
int updated = invDao.deleteById(id);
if (updated != 1) {
throw new RuntimeException("Unexpected row count after DB action: " + updated);
}
} catch (SQLException e) {
throw new RuntimeException(e); // FIXME do better
}
});
}
@Override
public Optional<IThreePidSessionDao> getThreePidSession(String sid) {
return withCatcher(() -> Optional.ofNullable(sessionDao.queryForId(sid)));
}
@Override
public Optional<IThreePidSessionDao> findThreePidSession(ThreePid tpid, String secret) {
return withCatcher(() -> {
List<ThreePidSessionDao> daoList = sessionDao.queryForMatchingArgs(new ThreePidSessionDao(tpid, secret));
if (daoList.size() > 1) {
log.error("Lookup for 3PID Session {}:{} returned more than one result");
throw new InternalServerError();
}
if (daoList.isEmpty()) {
return Optional.empty();
}
return Optional.of(daoList.get(0));
});
}
@Override
public void insertThreePidSession(IThreePidSessionDao session) {
withCatcher(() -> {
int updated = sessionDao.create(new ThreePidSessionDao(session));
if (updated != 1) {
throw new RuntimeException("Unexpected row count after DB action: " + updated);
}
});
}
@Override
public void updateThreePidSession(IThreePidSessionDao session) {
withCatcher(() -> {
int updated = sessionDao.update(new ThreePidSessionDao(session));
if (updated != 1) {
throw new RuntimeException("Unexpected row count after DB action: " + updated);
}
});
}
}

View File

@@ -0,0 +1,264 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.storage.ormlite.dao;
import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;
import io.kamax.mxisd.ThreePid;
import io.kamax.mxisd.storage.dao.IThreePidSessionDao;
@DatabaseTable(tableName = "session_3pid")
public class ThreePidSessionDao implements IThreePidSessionDao {
@DatabaseField(id = true)
private String id;
@DatabaseField(canBeNull = false)
private long creationTime;
@DatabaseField(canBeNull = false)
private String server;
@DatabaseField(canBeNull = false)
private String medium;
@DatabaseField(canBeNull = false)
private String address;
@DatabaseField(canBeNull = false)
private String secret;
@DatabaseField(canBeNull = false)
private int attempt;
@DatabaseField
private String nextLink;
@DatabaseField(canBeNull = false)
private String token;
@DatabaseField
private boolean validated;
@DatabaseField
private long validationTime;
@DatabaseField(canBeNull = false)
private boolean isRemote;
@DatabaseField
private String remoteServer;
@DatabaseField
private String remoteId;
@DatabaseField
private String remoteSecret;
@DatabaseField
private Integer remoteAttempt;
@DatabaseField(canBeNull = false)
private boolean isRemoteValidated;
public ThreePidSessionDao() {
// stub for ORMLite
}
public ThreePidSessionDao(IThreePidSessionDao session) {
setId(session.getId());
setCreationTime(session.getCreationTime());
setServer(session.getServer());
setMedium(session.getMedium());
setAddress(session.getAddress());
setSecret(session.getSecret());
setAttempt(session.getAttempt());
setNextLink(session.getNextLink());
setToken(session.getToken());
setValidated(session.getValidated());
setValidationTime(session.getValidationTime());
setRemote(session.isRemote());
setRemoteServer(session.getRemoteServer());
setRemoteId(session.getRemoteId());
setRemoteSecret(session.getRemoteSecret());
setRemoteAttempt(session.getRemoteAttempt());
setRemoteValidated(session.isRemoteValidated());
}
public ThreePidSessionDao(ThreePid tpid, String secret) {
setMedium(tpid.getMedium());
setAddress(tpid.getAddress());
setSecret(secret);
}
@Override
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
@Override
public long getCreationTime() {
return creationTime;
}
public void setCreationTime(long creationTime) {
this.creationTime = creationTime;
}
@Override
public String getServer() {
return server;
}
public void setServer(String server) {
this.server = server;
}
@Override
public String getMedium() {
return medium;
}
public void setMedium(String medium) {
this.medium = medium;
}
@Override
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
@Override
public int getAttempt() {
return attempt;
}
public void setAttempt(int attempt) {
this.attempt = attempt;
}
@Override
public String getNextLink() {
return nextLink;
}
public void setNextLink(String nextLink) {
this.nextLink = nextLink;
}
@Override
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public boolean getValidated() {
return validated;
}
public void setValidated(boolean validated) {
this.validated = validated;
}
@Override
public long getValidationTime() {
return validationTime;
}
@Override
public boolean isRemote() {
return isRemote;
}
public void setRemote(boolean remote) {
isRemote = remote;
}
@Override
public String getRemoteServer() {
return remoteServer;
}
public void setRemoteServer(String remoteServer) {
this.remoteServer = remoteServer;
}
@Override
public String getRemoteId() {
return remoteId;
}
public void setRemoteId(String remoteId) {
this.remoteId = remoteId;
}
@Override
public String getRemoteSecret() {
return remoteSecret;
}
public void setRemoteSecret(String remoteSecret) {
this.remoteSecret = remoteSecret;
}
@Override
public int getRemoteAttempt() {
return remoteAttempt;
}
@Override
public boolean isRemoteValidated() {
return isRemoteValidated;
}
public void setRemoteValidated(boolean remoteValidated) {
isRemoteValidated = remoteValidated;
}
public void setRemoteAttempt(int remoteAttempt) {
this.remoteAttempt = remoteAttempt;
}
public void setValidationTime(long validationTime) {
this.validationTime = validationTime;
}
}

View File

@@ -18,18 +18,12 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.mapping;
package io.kamax.mxisd.threepid.connector;
public interface MappingSession {
public interface IThreePidConnector {
String getServer();
String getSecret();
int getAttempt();
String getId();
String getMedium();
String getValue();
}

View File

@@ -0,0 +1,100 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.threepid.connector.email;
import com.sun.mail.smtp.SMTPTransport;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.mxisd.config.threepid.connector.EmailSmtpConfig;
import io.kamax.mxisd.exception.InternalServerError;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.Date;
@Component
public class EmailSmtpConnector implements IEmailConnector {
private Logger log = LoggerFactory.getLogger(EmailSmtpConnector.class);
private EmailSmtpConfig cfg;
private Session session;
@Autowired
public EmailSmtpConnector(EmailSmtpConfig cfg) {
this.cfg = cfg;
session = Session.getInstance(System.getProperties());
}
@Override
public String getId() {
return "smtp";
}
@Override
public String getMedium() {
return ThreePidMedium.Email.getId();
}
@Override
public void send(String senderAddress, String senderName, String recipient, String content) {
if (StringUtils.isBlank(content)) {
throw new InternalServerError("Notification content is empty");
}
try {
InternetAddress sender = new InternetAddress(senderAddress, senderName);
MimeMessage msg = new MimeMessage(session, IOUtils.toInputStream(content, StandardCharsets.UTF_8));
msg.setHeader("X-Mailer", "mxisd"); // FIXME set version
msg.setSentDate(new Date());
msg.setFrom(sender);
msg.setRecipients(Message.RecipientType.TO, recipient);
msg.saveChanges();
log.info("Sending invite to {} via SMTP using {}:{}", recipient, cfg.getHost(), cfg.getPort());
SMTPTransport transport = (SMTPTransport) session.getTransport("smtp");
transport.setStartTLS(cfg.getTls() > 0);
transport.setRequireStartTLS(cfg.getTls() > 1);
log.info("Connecting to {}:{}", cfg.getHost(), cfg.getPort());
transport.connect(cfg.getHost(), cfg.getPort(), cfg.getLogin(), cfg.getPassword());
try {
transport.sendMessage(msg, InternetAddress.parse(recipient));
log.info("Invite to {} was sent", recipient);
} finally {
transport.close();
}
} catch (UnsupportedEncodingException | MessagingException e) {
throw new RuntimeException("Unable to send e-mail invite to " + recipient, e);
}
}
}

View File

@@ -0,0 +1,35 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.threepid.connector.email;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.mxisd.threepid.connector.IThreePidConnector;
public interface IEmailConnector extends IThreePidConnector {
@Override
default String getMedium() {
return ThreePidMedium.Email.getId();
}
void send(String senderAddress, String senderName, String recipient, String content);
}

View File

@@ -0,0 +1,38 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.threepid.notification;
import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.threepid.session.IThreePidSession;
public interface INotificationGenerator {
String getId();
String getMedium();
String getForInvite(IThreePidInviteReply invite);
String getForValidation(IThreePidSession session);
String getForRemoteValidation(IThreePidSession session);
}

View File

@@ -0,0 +1,154 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.threepid.notification.email;
import io.kamax.mxisd.ThreePid;
import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.config.threepid.medium.EmailConfig;
import io.kamax.mxisd.config.threepid.medium.EmailTemplateConfig;
import io.kamax.mxisd.controller.v1.IdentityAPIv1;
import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.threepid.session.IThreePidSession;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.WordUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
@Component
public class EmailNotificationGenerator implements IEmailNotificationGenerator {
private Logger log = LoggerFactory.getLogger(EmailNotificationGenerator.class);
private EmailConfig cfg;
private EmailTemplateConfig templateCfg;
private MatrixConfig mxCfg;
private ServerConfig srvCfg;
@Autowired
private ApplicationContext app;
@Autowired
public EmailNotificationGenerator(EmailTemplateConfig templateCfg, EmailConfig cfg, MatrixConfig mxCfg, ServerConfig srvCfg) {
this.cfg = cfg;
this.templateCfg = templateCfg;
this.mxCfg = mxCfg;
this.srvCfg = srvCfg;
}
@Override
public String getId() {
return "template";
}
private String getTemplateContent(String location) {
try {
InputStream is = StringUtils.startsWith(location, "classpath:") ?
app.getResource(location).getInputStream() : new FileInputStream(location);
return IOUtils.toString(is, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new InternalServerError("Unable to read template content at " + location + ": " + e.getMessage());
}
}
private String populateCommon(String content, ThreePid recipient) {
String domainPretty = WordUtils.capitalizeFully(mxCfg.getDomain());
content = content.replace("%DOMAIN%", mxCfg.getDomain());
content = content.replace("%DOMAIN_PRETTY%", domainPretty);
content = content.replace("%FROM_EMAIL%", cfg.getIdentity().getFrom());
content = content.replace("%FROM_NAME%", cfg.getIdentity().getName());
content = content.replace("%RECIPIENT_MEDIUM%", recipient.getMedium());
content = content.replace("%RECIPIENT_ADDRESS%", recipient.getAddress());
return content;
}
private String getTemplateAndPopulate(String location, ThreePid recipient) {
return populateCommon(getTemplateContent(location), recipient);
}
@Override
public String getForInvite(IThreePidInviteReply invite) {
ThreePid tpid = new ThreePid(invite.getInvite().getMedium(), invite.getInvite().getAddress());
String templateBody = getTemplateAndPopulate(templateCfg.getInvite(), tpid);
String senderName = invite.getInvite().getProperties().getOrDefault("sender_display_name", "");
String senderNameOrId = StringUtils.defaultIfBlank(senderName, invite.getInvite().getSender().getId());
String roomName = invite.getInvite().getProperties().getOrDefault("room_name", "");
String roomNameOrId = StringUtils.defaultIfBlank(roomName, invite.getInvite().getRoomId());
templateBody = templateBody.replace("%SENDER_ID%", invite.getInvite().getSender().getId());
templateBody = templateBody.replace("%SENDER_NAME%", senderName);
templateBody = templateBody.replace("%SENDER_NAME_OR_ID%", senderNameOrId);
templateBody = templateBody.replace("%INVITE_MEDIUM%", tpid.getMedium());
templateBody = templateBody.replace("%INVITE_ADDRESS%", tpid.getAddress());
templateBody = templateBody.replace("%ROOM_ID%", invite.getInvite().getRoomId());
templateBody = templateBody.replace("%ROOM_NAME%", roomName);
templateBody = templateBody.replace("%ROOM_NAME_OR_ID%", roomNameOrId);
return templateBody;
}
@Override
public String getForValidation(IThreePidSession session) {
log.info("Generating notification content for 3PID Session validation");
String templateBody = getTemplateAndPopulate(templateCfg.getSession().getValidation().getLocal(), session.getThreePid());
// FIXME should have a global link builder, most likely in the SDK?
String validationLink = srvCfg.getPublicUrl() + IdentityAPIv1.BASE +
"/validate/" + session.getThreePid().getMedium() +
"/submitToken?sid=" + session.getId() + "&client_secret=" + session.getSecret() +
"&token=" + session.getToken();
templateBody = templateBody.replace("%VALIDATION_LINK%", validationLink);
templateBody = templateBody.replace("%VALIDATION_TOKEN%", session.getToken());
return templateBody;
}
@Override
public String getForRemoteValidation(IThreePidSession session) {
log.info("Generating notification content for remote-only 3PID session");
String templateBody = getTemplateAndPopulate(templateCfg.getSession().getValidation().getRemote(), session.getThreePid());
// FIXME should have a global link builder, most likely in the SDK?
String validationLink = srvCfg.getPublicUrl() + IdentityAPIv1.BASE +
"/validate/" + session.getThreePid().getMedium() +
"/submitToken?sid=" + session.getId() + "&client_secret=" + session.getSecret() +
"&token=" + session.getToken();
templateBody = templateBody.replace("%VALIDATION_LINK%", validationLink);
templateBody = templateBody.replace("%VALIDATION_TOKEN%", session.getToken());
return templateBody;
}
}

View File

@@ -0,0 +1,87 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.threepid.notification.email;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.mxisd.config.threepid.medium.EmailConfig;
import io.kamax.mxisd.exception.ConfigurationException;
import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.notification.INotificationHandler;
import io.kamax.mxisd.threepid.connector.email.IEmailConnector;
import io.kamax.mxisd.threepid.session.IThreePidSession;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class EmailNotificationHandler implements INotificationHandler {
private EmailConfig cfg;
private IEmailNotificationGenerator generator;
private IEmailConnector connector;
@Autowired
public EmailNotificationHandler(EmailConfig cfg, List<IEmailNotificationGenerator> generators, List<IEmailConnector> connectors) {
this.cfg = cfg;
generator = generators.stream()
.filter(o -> StringUtils.equals(cfg.getGenerator(), o.getId()))
.findFirst()
.orElseThrow(() -> new ConfigurationException("Email notification generator [" + cfg.getGenerator() + "] could not be found"));
connector = connectors.stream()
.filter(o -> StringUtils.equals(cfg.getConnector(), o.getId()))
.findFirst()
.orElseThrow(() -> new ConfigurationException("Email sender connector [" + cfg.getConnector() + "] could not be found"));
}
@Override
public String getMedium() {
return ThreePidMedium.Email.getId();
}
private void send(String recipient, String content) {
connector.send(
cfg.getIdentity().getFrom(),
cfg.getIdentity().getName(),
recipient,
content
);
}
@Override
public void sendForInvite(IThreePidInviteReply invite) {
send(invite.getInvite().getAddress(), generator.getForInvite(invite));
}
@Override
public void sendForValidation(IThreePidSession session) {
send(session.getThreePid().getAddress(), generator.getForValidation(session));
}
@Override
public void sendForRemoteValidation(IThreePidSession session) {
send(session.getThreePid().getAddress(), generator.getForRemoteValidation(session));
}
}

View File

@@ -0,0 +1,33 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.threepid.notification.email;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.mxisd.threepid.notification.INotificationGenerator;
public interface IEmailNotificationGenerator extends INotificationGenerator {
@Override
default String getMedium() {
return ThreePidMedium.Email.getId();
}
}

View File

@@ -0,0 +1,66 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.threepid.session;
import io.kamax.mxisd.ThreePid;
import java.time.Instant;
import java.util.Optional;
public interface IThreePidSession {
String getId();
Instant getCreationTime();
String getServer();
ThreePid getThreePid();
String getSecret();
int getAttempt();
void increaseAttempt();
Optional<String> getNextLink();
String getToken();
void validate(String token);
boolean isValidated();
Instant getValidationTime();
boolean isRemote();
String getRemoteServer();
String getRemoteId();
String getRemoteSecret();
int getRemoteAttempt();
void setRemoteData(String server, String id, String secret, int attempt);
}

View File

@@ -0,0 +1,304 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.threepid.session;
import io.kamax.mxisd.ThreePid;
import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.exception.InvalidCredentialsException;
import io.kamax.mxisd.storage.dao.IThreePidSessionDao;
import org.apache.commons.lang.StringUtils;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Optional;
public class ThreePidSession implements IThreePidSession {
private String id;
private Instant timestamp;
private String server;
private ThreePid tPid;
private String secret;
private String nextLink;
private String token;
private int attempt;
private Instant validationTimestamp;
private boolean isValidated;
private boolean isRemote;
private String remoteServer;
private String remoteId;
private String remoteSecret;
private int remoteAttempt;
private boolean isRemoteValidated;
public ThreePidSession(IThreePidSessionDao dao) {
this(
dao.getId(),
dao.getServer(),
new ThreePid(dao.getMedium(), dao.getAddress()),
dao.getSecret(),
dao.getAttempt(),
dao.getNextLink(),
dao.getToken()
);
timestamp = Instant.ofEpochMilli(dao.getCreationTime());
isValidated = dao.getValidated();
if (isValidated) {
validationTimestamp = Instant.ofEpochMilli(dao.getValidationTime());
}
isRemote = dao.isRemote();
remoteServer = dao.getRemoteServer();
remoteId = dao.getRemoteId();
remoteSecret = dao.getRemoteSecret();
remoteAttempt = dao.getRemoteAttempt();
isRemoteValidated = dao.isRemoteValidated();
}
public ThreePidSession(String id, String server, ThreePid tPid, String secret, int attempt, String nextLink, String token) {
this.id = id;
this.server = server;
this.tPid = new ThreePid(tPid);
this.secret = secret;
this.attempt = attempt;
this.nextLink = nextLink;
this.token = token;
this.timestamp = Instant.now();
}
@Override
public String getId() {
return id;
}
@Override
public Instant getCreationTime() {
return timestamp;
}
@Override
public String getServer() {
return server;
}
@Override
public ThreePid getThreePid() {
return tPid;
}
public String getSecret() {
return secret;
}
@Override
public int getAttempt() {
return attempt;
}
@Override
public void increaseAttempt() {
attempt++;
}
@Override
public Optional<String> getNextLink() {
return Optional.ofNullable(nextLink);
}
@Override
public String getToken() {
return token;
}
public synchronized void setAttempt(int attempt) {
if (isValidated()) {
throw new IllegalStateException();
}
this.attempt = attempt;
}
@Override
public Instant getValidationTime() {
return validationTimestamp;
}
@Override
public boolean isRemote() {
return isRemote;
}
@Override
public String getRemoteServer() {
return remoteServer;
}
@Override
public String getRemoteId() {
return remoteId;
}
@Override
public String getRemoteSecret() {
return remoteSecret;
}
@Override
public int getRemoteAttempt() {
return remoteAttempt;
}
public int increaseAndGetRemoteAttempt() {
return ++remoteAttempt;
}
@Override
public void setRemoteData(String server, String id, String secret, int attempt) {
this.remoteServer = server;
this.remoteId = id;
this.remoteSecret = secret;
this.attempt = attempt;
this.isRemote = true;
}
@Override
public boolean isValidated() {
return isValidated;
}
public synchronized void validate(String token) {
if (Instant.now().minus(24, ChronoUnit.HOURS).isAfter(getCreationTime())) {
throw new BadRequestException("Session " + getId() + " has expired");
}
if (!StringUtils.equals(this.token, token)) {
throw new InvalidCredentialsException();
}
if (isValidated()) {
return;
}
validationTimestamp = Instant.now();
isValidated = true;
}
public boolean isRemoteValidated() {
return isRemoteValidated;
}
public void validateRemote() {
this.isRemoteValidated = true;
}
public IThreePidSessionDao getDao() {
return new IThreePidSessionDao() {
@Override
public String getId() {
return id;
}
@Override
public long getCreationTime() {
return timestamp.toEpochMilli();
}
@Override
public String getServer() {
return server;
}
@Override
public String getMedium() {
return tPid.getMedium();
}
@Override
public String getAddress() {
return tPid.getAddress();
}
@Override
public String getSecret() {
return secret;
}
@Override
public int getAttempt() {
return attempt;
}
@Override
public String getNextLink() {
return nextLink;
}
@Override
public String getToken() {
return token;
}
@Override
public boolean getValidated() {
return isValidated;
}
@Override
public long getValidationTime() {
return isValidated ? validationTimestamp.toEpochMilli() : 0;
}
@Override
public boolean isRemote() {
return isRemote;
}
@Override
public String getRemoteServer() {
return remoteServer;
}
@Override
public String getRemoteId() {
return remoteId;
}
@Override
public String getRemoteSecret() {
return remoteSecret;
}
@Override
public int getRemoteAttempt() {
return remoteAttempt;
}
@Override
public boolean isRemoteValidated() {
return isRemoteValidated;
}
};
}
}

View File

@@ -26,6 +26,7 @@ import io.kamax.mxisd.exception.JsonMemberNotFoundException;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
@@ -53,6 +54,10 @@ public class GsonParser {
return el.getAsJsonObject();
}
public <T> T parse(HttpServletRequest req, Class<T> type) throws IOException {
return gson.fromJson(parse(req.getInputStream()), type);
}
public <T> T parse(HttpResponse res, Class<T> type) throws IOException {
return gson.fromJson(parse(res.getEntity().getContent()), type);
}

View File

@@ -20,7 +20,9 @@
package io.kamax.mxisd.util;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
@@ -29,6 +31,8 @@ import java.nio.charset.StandardCharsets;
public class RestClientUtils {
private static Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
public static HttpPost post(String url, String body) {
StringEntity entity = new StringEntity(body, StandardCharsets.UTF_8);
entity.setContentType(ContentType.APPLICATION_JSON.toString());
@@ -45,4 +49,8 @@ public class RestClientUtils {
return post(url, gson.toJson(o));
}
public static HttpPost post(String url, Object o) {
return post(url, gson.toJson(o));
}
}

View File

@@ -1,3 +1,7 @@
spring:
main:
banner-mode: 'off'
logging:
level:
org:
@@ -5,10 +9,18 @@ logging:
apache:
catalina: "WARN"
directory: "WARN"
pattern:
console: '%d{yyyy-MM-dd HH:mm:ss.SSS} ${LOG_LEVEL_PATTERN:%5p} [%15.15t] %35.35logger{34} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}'
server:
port: 8090
matrix:
identity:
servers:
root:
- 'https://matrix.org'
lookup:
recursive:
enabled: true
@@ -49,19 +61,85 @@ firebase:
enabled: false
sql:
enabled: false
type: 'sqlite'
connection: ''
auth:
enabled: false
identity:
type: 'mxid'
query: "SELECT user_id AS uid FROM user_threepids WHERE medium = ? AND address = ?"
forward:
servers:
- "https://matrix.org"
- "https://vector.im"
invite:
sender:
threepid:
medium:
email:
tls: 1
name: "mxisd Identity Server"
template: "classpath:email/invite-template.eml"
identity:
from: ''
name: ''
connector: 'smtp'
generator: 'template'
connectors:
smtp:
host: ''
port: 587
tls: 1
login: ''
password: ''
generators:
template:
invite: 'classpath:email/invite-template.eml'
session:
validation:
local: 'classpath:email/validate-local-template.eml'
remote: 'classpath:email/validate-remote-template.eml'
session:
policy:
validation:
enabled: true
forLocal:
enabled: true
toLocal: true
toRemote:
enabled: true
server: 'root'
forRemote:
enabled: true
toLocal: false
toRemote:
enabled: true
server: 'root'
view:
session:
local:
onTokenSubmit:
success: 'session/local/tokenSubmitSuccess'
failure: 'session/local/tokenSubmitFailure'
localRemote:
onTokenSubmit:
success: 'session/localRemote/tokenSubmitSuccess'
failure: 'session/local/tokenSubmitFailure'
remote:
onRequest:
success: 'session/remote/requestSuccess'
failure: 'session/remote/requestFailure'
onCheck:
success: 'session/remote/checkSuccess'
failure: 'session/remote/checkFailure'
storage:
backend: 'sqlite'
---
spring:
profiles: systemd
logging:
pattern:
console: '%d{.SSS}${LOG_LEVEL_PATTERN:%5p} [%15.15t] %35.35logger{34} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}'

View File

@@ -0,0 +1,87 @@
Subject: Your Matrix Validation Token
MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary="7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ"
--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ
Content-Type: text/plain; charset=UTF-8
Content-Disposition: inline
Hello there!
We have received a request to link this email address with your Matrix account.
If it was really you who made this request, you can click on the following link to
complete the verification of your email address:
%VALIDATION_LINK%
If you didn't make this request, you can safely disregard this email.
%DOMAIN_PRETTY% Admins
--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ
Content-Type: multipart/related;
boundary="M3yzHl5YZehm9v4bAM8sKEdcOoVnRnKR";
type="text/html"
--M3yzHl5YZehm9v4bAM8sKEdcOoVnRnKR
Content-Type: text/html; charset=UTF-8
Content-Disposition: inline
<!doctype html>
<html lang="en">
<head>
<style type="text/css">
body {
margin: 0px;
}
pre, code {
word-break: break-word;
white-space: pre-wrap;
}
#page {
font-family: 'Open Sans', Helvetica, Arial, Sans-Serif;
font-color: #454545;
font-size: 12pt;
width: 100%%;
padding: 20px;
}
#inner {
width: 640px;
}
.notif_link a, .footer a {
color: #76CFA6 ! important;
}
</style>
</head>
<body>
<table id="page">
<tr>
<td></td>
<td id="inner">
<p>Hello there!</p>
<p>We have received a request to link this email address with your Matrix account.</p>
<p>If it was really you who made this request, you can click on the following link to
complete the verification of your email address:</p>
<p><a href="%VALIDATION_LINK%">Complete email verification</a></p>
<p>If you didn't make this request, you can safely disregard this email.</p>
<p>%DOMAIN_PRETTY% Admins</p>
</td>
<td></td>
</tr>
</table>
</body>
</html>
--M3yzHl5YZehm9v4bAM8sKEdcOoVnRnKR--
--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ--

View File

@@ -0,0 +1,102 @@
Subject: Linking your Email address to your Matrix account
MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary="7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ"
--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ
Content-Type: text/plain; charset=UTF-8
Content-Disposition: inline
Hello there!
We have received a request to link this email address with your Matrix account.
Due to the security policy in place, this email address can only be stored in the central Matrix Identity Server.
If you continue, your e-mail address and Matrix ID association will be made public without any current mean to be removed.
If you would still like to continue, you will need to:
1. Go to your private Public registration process page:
%VALIDATION_LINK%
2. Follow the registration process of the central Identity Server, usually another email with similar content
3. Once your email address validated with the central Identity Server, click on "Continue" on page of step #1
4. If your public association is found by our Identity server, the next step will be given to you.
If you didn't make this request, or do not want to make your address public, you can safely disregard this email.
%DOMAIN_PRETTY% Admins
--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ
Content-Type: multipart/related;
boundary="M3yzHl5YZehm9v4bAM8sKEdcOoVnRnKR";
type="text/html"
--M3yzHl5YZehm9v4bAM8sKEdcOoVnRnKR
Content-Type: text/html; charset=UTF-8
Content-Disposition: inline
<!doctype html>
<html lang="en">
<head>
<style type="text/css">
body {
margin: 0px;
}
pre, code {
word-break: break-word;
white-space: pre-wrap;
}
#page {
font-family: 'Open Sans', Helvetica, Arial, Sans-Serif;
font-color: #454545;
font-size: 12pt;
width: 100%%;
padding: 20px;
}
#inner {
width: 640px;
}
.notif_link a, .footer a {
color: #76CFA6 ! important;
}
</style>
</head>
<body>
<table id="page">
<tr>
<td></td>
<td id="inner">
<p>Hello there!</p>
<p>We have received a request to link this email address with your Matrix account.</p>
<p>Due to the security policy in place, this email address can only be stored in the central Matrix Identity Server.
If you continue, your e-mail address and Matrix ID association will be made public without any current mean to be removed.</p>
<p>If you would still like to continue, you will need to:
<ol>
<li>Go to your private <a href="%VALIDATION_LINK%">Public registration process page</a></li>
<li>Follow the registration process of the central Identity Server, usually another email with similar content</li>
<li>Once your email address validated with the central Identity Server, click on "Continue" on page of step #1</li>
<li>If your public association is found by our Identity server, the next step will be given to you.</li>
</ol>
</p>
<p>If you didn't make this request, or do not want to make your address public, you can safely disregard this email.</p>
<p>%DOMAIN_PRETTY% Admins</p>
</td>
<td></td>
</tr>
</table>
</body>
</html>
--M3yzHl5YZehm9v4bAM8sKEdcOoVnRnKR--
--7REaIwWQCioQ6NaBlAQlg8ztbUQj6PKJ--

View File

@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Matrix Token Verification</title>
<style>
body {
font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
font-size: 12pt;
margin: 1em;
}
#message {
width: 1200px;
text-align: left;
padding: 1em;
margin-bottom: 40px;
margin-left: auto;
margin-right: auto;
margin-top: 50px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
-webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
-moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
background-color: #f8f8f8;
border: 1px #ccc solid;
}
</style>
</head>
<body>
<div id="message">
<p>Verification failed: you may need to request another verification email</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Matrix Token Verification</title>
<style>
body {
font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
font-size: 12pt;
margin: 1em;
}
#message {
width: 1200px;
text-align: left;
padding: 1em;
margin-bottom: 40px;
margin-left: auto;
margin-right: auto;
margin-top: 50px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
-webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
-moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
background-color: #f8f8f8;
border: 1px #ccc solid;
}
</style>
</head>
<body>
<div id="message">
<p>Verification successful: return to your Matrix client to complete the process.</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8"/>
<title>Matrix Token Verification</title>
<style>
body {
font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
font-size: 12pt;
margin: 1em;
}
#message {
width: 1200px;
text-align: left;
padding: 1em;
margin-bottom: 40px;
margin-left: auto;
margin-right: auto;
margin-top: 50px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
-webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
-moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
background-color: #f8f8f8;
border: 1px #ccc solid;
}
</style>
</head>
<body>
<div id="message">
<p>Verification successful!</p>
<p>Your email will remain private and you will only be discoverable with it on your own server, or any related
servers configured by your system admin.<br/>
If you would like to be globally discoverable, start the process <a th:href="${remoteSessionLink}">here</a>.
<br/>If you chose to start the global publication process, wait until it is done before returning to your
client.</p>
<p>If the remote process is finished, or if you do not wish to start it at this time, you can now return to your
Matrix client to complete the process.</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Matrix global token verification</title>
<style>
body {
font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
font-size: 12pt;
margin: 1em;
}
#message {
width: 1200px;
text-align: left;
padding: 1em;
margin-bottom: 40px;
margin-left: auto;
margin-right: auto;
margin-top: 50px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
-webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
-moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
background-color: #f8f8f8;
border: 1px #ccc solid;
}
</style>
</head>
<body>
<div id="message">
<p>You do not seem to have validated your session with the global server. Please check your messages for one similar
to the one you received initially.<br/>
Once this is done, <a href="#">click here to continue</a></p>
<p>If this problem persists, contact your system administrator with the following info: Reference #ABC</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Matrix global token verification</title>
<style>
body {
font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
font-size: 12pt;
margin: 1em;
}
#message {
width: 1200px;
text-align: left;
padding: 1em;
margin-bottom: 40px;
margin-left: auto;
margin-right: auto;
margin-top: 50px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
-webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
-moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
background-color: #f8f8f8;
border: 1px #ccc solid;
}
</style>
</head>
<body>
<div id="message">
<p>Verification successful!</p>
<p>Return to your Matrix client to complete the process and make yourself globally discoverable.</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Matrix global token verification</title>
<style>
body {
font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
font-size: 12pt;
margin: 1em;
}
#message {
width: 1200px;
text-align: left;
padding: 1em;
margin-bottom: 40px;
margin-left: auto;
margin-right: auto;
margin-top: 50px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
-webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
-moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
background-color: #f8f8f8;
border: 1px #ccc solid;
}
</style>
</head>
<body>
<div id="message">
<p>The process to be globally discoverable has failed!<br/>You can try to refresh this page in a few seconds or
minutes.</p>
<p>If this problem persists, contact your system administrator with the following info: Reference #ABC</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8"/>
<title>Matrix global token verification</title>
<style>
body {
font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
font-size: 12pt;
margin: 1em;
}
#message {
width: 1200px;
text-align: left;
padding: 1em;
margin-bottom: 40px;
margin-left: auto;
margin-right: auto;
margin-top: 50px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
-webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
-moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
background-color: #f8f8f8;
border: 1px #ccc solid;
}
</style>
</head>
<body>
<div id="message">
<p>The process to be globally discoverable has started. A verification token has been requested on your behalf.</p>
<p>You will receive a similar communication as the first verification message.<br/>
Follow the instructions and come back to this page once you are told to return to your Matrix client or that the
verification was successful.</p>
<p>Once the validation was successful with the global server, please follow <a th:href="${checkLink}">this link</a>
to validate it with us.</p>
</div>
</body>
</html>

View File

@@ -4,7 +4,7 @@ After=syslog.target
[Service]
User=mxisd
ExecStart=/usr/bin/mxisd --spring.config.location=/etc/mxisd/ --spring.config.name=mxisd
ExecStart=/usr/bin/mxisd --spring.config.location=/etc/mxisd/ --spring.config.name=mxisd --spring.profiles.active=systemd
SuccessExitStatus=143
[Install]