Compare commits

..

10 Commits

Author SHA1 Message Date
Maxime Dor
0fa04e4a54 Better wording 2017-09-16 14:19:26 +02:00
Maxime Dor
f97aa8cbef Cosmetic refactoring 2017-09-16 13:46:15 +02:00
Maxime Dor
37b0f29184 Cosmetic refactoring 2017-09-16 13:01:37 +02:00
Maxime Dor
2befdbb54f Improve network discovery explanation 2017-09-16 04:34:16 +02:00
Maxime Dor
d1a6c84e6b Properly split authoritative domain and public IS host 2017-09-16 04:29:01 +02:00
Maxime Dor
e8229b867a Add docker build targets 2017-09-16 02:07:10 +02:00
Maxime Dor
6fb18d5827 Remove problematic handling of multiple validation requests for same 3PID 2017-09-16 01:34:31 +02:00
Maxime Dor
a8488a0745 Add ability to overwrite DNS when trying to contact the related homeserver 2017-09-14 23:10:23 +02:00
Maxime Dor
89a7416367 Add prototype support for SQL auth/directory backends 2017-09-14 20:59:35 +02:00
Maxime Dor
068c6b8555 Use proper object for key validity json answer 2017-09-14 18:34:02 +02:00
25 changed files with 752 additions and 57 deletions

View File

@@ -7,7 +7,7 @@ mxisd - Federated Matrix Identity Server Daemon
[Integration](#integration) | [Support](#support) [Integration](#integration) | [Support](#support)
# Overview # Overview
mxisd is a Federated Matrix Identity server aimed to self-hosted Matrix infrastructures. mxisd is a Federated Matrix Identity server for self-hosted Matrix infrastructures.
mxisd uses a cascading lookup model which performs lookup from a more authoritative to a less authoritative source, usually doing: mxisd uses a cascading lookup model which performs lookup from a more authoritative to a less authoritative source, usually doing:
- Local identity stores: LDAP, etc. - Local identity stores: LDAP, etc.
@@ -169,11 +169,14 @@ systemctl start mxisd
# Configuration # Configuration
After following the specific instructions to create a config file from the sample: After following the specific instructions to create a config file from the sample:
1. Set the `server.name` value to the domain value used in your Home Server configuration 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` 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` 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` 4. 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: If you want to use the LDAP backend:
1. Enable it with `ldap.enabled` 1. Enable it with `ldap.enabled`
2. Configure connection options using items starting in `ldap.connection` 2. Configure connection options using items starting in `ldap.connection`
@@ -181,7 +184,12 @@ If you want to use the LDAP backend:
# Network Discovery # Network Discovery
To allow other federated Identity Server to reach yours, configure the following DNS SRV entry (adapt to your values): To allow other federated Identity Server to reach yours, the same algorithm used for Homeservers takes place:
1. Check for the appropriate DNS SRV record
2. If not found, use the base domain
If your Identity Server public hostname does not match your Matrix domain, configure the following DNS SRV entry
and replace `matrix.example.com` by your Identity server public hostname - **Make sure to end with a final dot!**
``` ```
_matrix-identity._tcp.example.com. 3600 IN SRV 10 0 443 matrix.example.com. _matrix-identity._tcp.example.com. 3600 IN SRV 10 0 443 matrix.example.com.
``` ```

View File

@@ -7,6 +7,15 @@
# Any mandatory configuration item will not be prefixed by # and will also contain a value as example that must be # Any mandatory configuration item will not be prefixed by # and will also contain a value as example that must be
# changed. It is advised to re-create a clean config file with only the required configuration item. # changed. It is advised to re-create a clean config file with only the required configuration item.
#######################
# Matrix config items #
#######################
# Matrix domain, same as the domain configure in your Homeserver configuration.
#
# This is used to build the various identifiers for identity, auth and directory.
matrix.domain: ''
####################### #######################
# Server config items # # Server config items #
@@ -19,21 +28,33 @@
#server.port: 8090 #server.port: 8090
# Realm under which this Identity Server is authoritative. # Public hostname of this identity server.
# #
# This is used to avoid unnecessary connections and endless recursive lookup. # This would be typically be the same as your Matrix domain.
# e.g. domain name in e-mails. # In case it is not, set this value.
server.name: 'example.org' #
# This value is used in various signatures within the Matrix protocol and should be a reachable hostname.
# You can validate by ensuring you see a JSON answer when calling (replace the domain):
# https://example.org/_matrix/identity/status
#
#server.name: 'example.org'
# Public URL to reach this identity server # Public URL to reach this identity server
# #
# This is used with 3PID invites in room and other Homeserver key verification workflow. # This is used with 3PID invites in room and other Homeserver key verification workflow.
# If left unconfigured, it will be generated from the server name # If left unconfigured, it will be generated from the server name.
#
# You should typically set this value if you want to change the public port under which
# this Identity server is reachable.
#
# %SERVER_NAME% placeholder is available to avoid configuration duplication.
# e.g. 'https://%SERVER_NAME%:8443'
# #
#server.publicUrl: 'https://example.org' #server.publicUrl: 'https://example.org'
############################# #############################
# Signing keys config items # # Signing keys config items #
############################# #############################
@@ -47,6 +68,7 @@ server.name: 'example.org'
key.path: '/path/to/sign.key' key.path: '/path/to/sign.key'
################################# #################################
# Recurisve lookup config items # # Recurisve lookup config items #
################################# #################################
@@ -125,14 +147,15 @@ key.path: '/path/to/sign.key'
#lookup.recursive.bridge.mappings.msisdn: '' #lookup.recursive.bridge.mappings.msisdn: ''
##################### #####################
# LDAP config items # # LDAP config items #
##################### #####################
# Global enable/disable switch # Global enable/disable switch
# #
#ldap.enabled: false #ldap.enabled: false
#### Connection related config items #### Connection related config items
# If the connection should be secure # If the connection should be secure
# #
@@ -223,6 +246,22 @@ key.path: '/path/to/sign.key'
#ldap.identity.medium.msisdn: "(|(telephoneNumber=+%3pid)(mobile=+%3pid)(homePhone=+%3pid)(otherTelephone=+%3pid)(otherMobile=+%3pid)(otherHomePhone=+%3pid))" #ldap.identity.medium.msisdn: "(|(telephoneNumber=+%3pid)(mobile=+%3pid)(homePhone=+%3pid)(otherTelephone=+%3pid)(otherMobile=+%3pid)(otherHomePhone=+%3pid))"
############################
# SQL Provider config item #
############################
#
# Example configuration to integrate with synapse SQLite DB (default configuration)
#
#sql.enabled: true
#sql.type: 'sqlite'
#sql.connection: '/var/lib/matrix-synapse/homeserver.db'
#sql.identity.type: 'mxid'
#sql.identity.query: 'SELECT user_id AS uid FROM user_threepids WHERE medium = ? AND address = ?'
#sql.identity.medium.email: "SELECT user_id AS uid FROM user_threepids WHERE medium = ? AND address = ?"
####################################### #######################################
# Lookup queries forward config items # # Lookup queries forward config items #
####################################### #######################################
@@ -236,6 +275,7 @@ key.path: '/path/to/sign.key'
# - "https://vector.im" # - "https://vector.im"
############################# #############################
# 3PID invites config items # # 3PID invites config items #
############################# #############################
@@ -304,6 +344,7 @@ invite.sender.email.email: "matrix-identity@example.org"
#invite.sender.email.template: "/absolute/path/to/file" #invite.sender.email.template: "/absolute/path/to/file"
############################ ############################
# Persistence config items # # Persistence config items #
############################ ############################
@@ -314,6 +355,7 @@ invite.sender.email.email: "matrix-identity@example.org"
# #
#storage.backend: 'sqlite' #storage.backend: 'sqlite'
#### Generic SQLite provider config #### Generic SQLite provider config
# #
# Path to the SQLite DB file, required if SQLite backend is chosen # Path to the SQLite DB file, required if SQLite backend is chosen
@@ -324,3 +366,23 @@ invite.sender.email.email: "matrix-identity@example.org"
# - /var/lib/mxisd/mxisd.db # - /var/lib/mxisd/mxisd.db
# #
storage.provider.sqlite.database: '/path/to/mxisd.db' storage.provider.sqlite.database: '/path/to/mxisd.db'
######################
# DNS-related config #
######################
# The domain to overwrite
#
#dns.overwrite.homeserver.name: 'example.org'
# - 'env' from environment variable specified by value
# - any other value will use the value as-is as host
#
#dns.overwrite.homeserver.type: 'raw'
# The value to use, depending on the type
#
#dns.overwrite.homeserver.value: 'localhost:8448'

View File

@@ -216,3 +216,11 @@ task buildDeb(dependsOn: build) {
} }
} }
} }
task dockerBuild(type: Exec, dependsOn: build) {
commandLine 'docker', 'build', '-t', "kamax/mxisd:${gitVersion()}", project.rootDir
}
task dockerPush(type: Exec) {
commandLine 'docker', 'push', "kamax/mxisd:${gitVersion()}"
}

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.auth.provider;
import io.kamax.mxisd.auth.UserAuthResult;
import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.config.sql.SqlProviderConfig;
import io.kamax.mxisd.invitation.InvitationManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class SqlAuthProvider implements AuthenticatorProvider {
private Logger log = LoggerFactory.getLogger(SqlAuthProvider.class);
@Autowired
private ServerConfig srvCfg;
@Autowired
private SqlProviderConfig cfg;
@Autowired
private InvitationManager invMgr;
@Override
public boolean isEnabled() {
return cfg.isEnabled();
}
@Override
public UserAuthResult authenticate(String id, String password) {
log.info("Performing dummy authentication try to force invite mapping refresh");
invMgr.lookupMappingsForInvites();
return new UserAuthResult().failure();
}
}

View File

@@ -0,0 +1,52 @@
/*
* 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.apache.commons.lang.StringUtils;
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 java.util.Optional;
@Configuration
@ConfigurationProperties("dns.overwrite")
public class DnsOverwrite {
private Logger log = LoggerFactory.getLogger(DnsOverwrite.class);
@Autowired
private ServerConfig srvCfg;
@Autowired
private DnsOverwriteEntry homeserver;
public Optional<DnsOverwriteEntry> findHost(String lookup) {
if (homeserver != null && StringUtils.equalsIgnoreCase(lookup, homeserver.getName())) {
return Optional.of(homeserver);
}
return Optional.empty();
}
}

View File

@@ -0,0 +1,67 @@
/*
* 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.apache.commons.lang.StringUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties("dns.overwrite.homeserver")
public class DnsOverwriteEntry {
private String name;
private String type;
private String value;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getTarget() {
if (StringUtils.equals("env", getType())) {
return System.getenv(getValue());
} else {
return getValue();
}
}
}

View File

@@ -40,7 +40,7 @@ public class FirebaseConfig {
private Logger log = LoggerFactory.getLogger(FirebaseConfig.class); private Logger log = LoggerFactory.getLogger(FirebaseConfig.class);
@Autowired @Autowired
private ServerConfig srvCfg; private MatrixConfig mxCfg;
private boolean enabled; private boolean enabled;
private String credentials; private String credentials;
@@ -85,7 +85,7 @@ public class FirebaseConfig {
if (!enabled) { if (!enabled) {
return new GoogleFirebaseAuthenticator(false); return new GoogleFirebaseAuthenticator(false);
} else { } else {
return new GoogleFirebaseAuthenticator(credentials, database, srvCfg.getName()); return new GoogleFirebaseAuthenticator(credentials, database, mxCfg.getDomain());
} }
} }
@@ -94,7 +94,7 @@ public class FirebaseConfig {
if (!enabled) { if (!enabled) {
return new GoogleFirebaseProvider(false); return new GoogleFirebaseProvider(false);
} else { } else {
return new GoogleFirebaseProvider(credentials, database, srvCfg.getName()); return new GoogleFirebaseProvider(credentials, database, mxCfg.getDomain());
} }
} }

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.config;
import io.kamax.mxisd.exception.ConfigurationException;
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("matrix")
public class MatrixConfig {
private Logger log = LoggerFactory.getLogger(MatrixConfig.class);
private String domain;
public String getDomain() {
return domain;
}
public void setDomain(String domain) {
this.domain = domain;
}
@PostConstruct
private void postConstruct() {
log.info("--- Matrix config ---");
if (StringUtils.isBlank(domain)) {
throw new ConfigurationException("matrix.domain");
}
log.info("Domain: {}", getDomain());
}
}

View File

@@ -20,11 +20,11 @@
package io.kamax.mxisd.config package io.kamax.mxisd.config
import io.kamax.mxisd.exception.ConfigurationException
import org.apache.commons.lang.StringUtils import org.apache.commons.lang.StringUtils
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.InitializingBean import org.springframework.beans.factory.InitializingBean
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
@@ -34,6 +34,9 @@ class ServerConfig implements InitializingBean {
private Logger log = LoggerFactory.getLogger(ServerConfig.class); private Logger log = LoggerFactory.getLogger(ServerConfig.class);
@Autowired
private MatrixConfig mxCfg;
private String name private String name
private int port private int port
private String publicUrl private String publicUrl
@@ -64,13 +67,18 @@ class ServerConfig implements InitializingBean {
@Override @Override
void afterPropertiesSet() throws Exception { void afterPropertiesSet() throws Exception {
log.info("--- Server config ---")
if (StringUtils.isBlank(getName())) { if (StringUtils.isBlank(getName())) {
throw new ConfigurationException("server.name") setName(mxCfg.getDomain());
log.debug("server.name is empty, using matrix.domain");
} }
if (StringUtils.isBlank(getPublicUrl())) { if (StringUtils.isBlank(getPublicUrl())) {
log.warn("Public URL is empty, generating from name {}", getName()) setPublicUrl("https://${getName()}");
publicUrl = "https://${getName()}" log.debug("Public URL is empty, generating from name");
} else {
setPublicUrl(StringUtils.replace(getPublicUrl(), "%SERVER_NAME%", getName()));
} }
try { try {
@@ -79,7 +87,6 @@ class ServerConfig implements InitializingBean {
log.warn("Public URL is not valid: {}", StringUtils.defaultIfBlank(e.getMessage(), "<no reason provided>")) log.warn("Public URL is not valid: {}", StringUtils.defaultIfBlank(e.getMessage(), "<no reason provided>"))
} }
log.info("--- Server config ---")
log.info("Name: {}", getName()) log.info("Name: {}", getName())
log.info("Port: {}", getPort()) log.info("Port: {}", getPort())
log.info("Public URL: {}", getPublicUrl()) log.info("Public URL: {}", getPublicUrl())

View File

@@ -0,0 +1,21 @@
package io.kamax.mxisd.config.sql;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
// Unused
@Configuration
@ConfigurationProperties("sql.auth")
public class SqlProviderAuthConfig {
private boolean enabled;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}

View File

@@ -0,0 +1,96 @@
/*
* 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.sql;
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("sql")
public class SqlProviderConfig {
private Logger log = LoggerFactory.getLogger(SqlProviderConfig.class);
private boolean enabled;
private String type;
private String connection;
private SqlProviderAuthConfig auth;
private SqlProviderIdentityConfig identity;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getConnection() {
return connection;
}
public void setConnection(String connection) {
this.connection = connection;
}
public SqlProviderAuthConfig getAuth() {
return auth;
}
public void setAuth(SqlProviderAuthConfig auth) {
this.auth = auth;
}
public SqlProviderIdentityConfig getIdentity() {
return identity;
}
public void setIdentity(SqlProviderIdentityConfig identity) {
this.identity = identity;
}
@PostConstruct
private void postConstruct() {
log.info("--- SQL Provider config ---");
log.info("Enabled: {}", isEnabled());
if (isEnabled()) {
log.info("Type: {}", getType());
log.info("Connection: {}", getConnection());
log.info("Auth enabled: {}", getAuth().isEnabled());
log.info("Identy type: {}", getIdentity().getType());
log.info("Identity medium queries: {}", new Gson().toJson(getIdentity().getMedium()));
}
}
}

View File

@@ -0,0 +1,61 @@
/*
* 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.sql;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Configuration
@ConfigurationProperties("sql.identity")
public class SqlProviderIdentityConfig {
private String type;
private String query;
private Map<String, String> medium = new HashMap<>();
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getQuery() {
return query;
}
public void setQuery(String query) {
this.query = query;
}
public Map<String, String> getMedium() {
return medium;
}
public void setMedium(Map<String, String> medium) {
this.medium = medium;
}
}

View File

@@ -0,0 +1,27 @@
/*
* 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;
public class IdentityAPIv1 {
public static final String BASE = "/_matrix/identity/api/v1";
}

View File

@@ -44,7 +44,7 @@ import static org.springframework.web.bind.annotation.RequestMethod.POST
@RestController @RestController
@CrossOrigin @CrossOrigin
@RequestMapping(path = "/_matrix/identity/api/v1", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) @RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
class InvitationController { class InvitationController {
private Logger log = LoggerFactory.getLogger(InvitationController.class) private Logger log = LoggerFactory.getLogger(InvitationController.class)

View File

@@ -20,9 +20,10 @@
package io.kamax.mxisd.controller.v1 package io.kamax.mxisd.controller.v1
import com.google.gson.Gson
import groovy.json.JsonOutput import groovy.json.JsonOutput
import io.kamax.mxisd.controller.v1.io.KeyValidityJson
import io.kamax.mxisd.exception.BadRequestException import io.kamax.mxisd.exception.BadRequestException
import io.kamax.mxisd.exception.NotImplementedException
import io.kamax.mxisd.key.KeyManager import io.kamax.mxisd.key.KeyManager
import org.apache.commons.lang.StringUtils import org.apache.commons.lang.StringUtils
import org.slf4j.Logger import org.slf4j.Logger
@@ -37,7 +38,7 @@ import static org.springframework.web.bind.annotation.RequestMethod.GET
@RestController @RestController
@CrossOrigin @CrossOrigin
@RequestMapping(path = "/_matrix/identity/api/v1", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) @RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
class KeyController { class KeyController {
private Logger log = LoggerFactory.getLogger(KeyController.class) private Logger log = LoggerFactory.getLogger(KeyController.class)
@@ -45,6 +46,10 @@ class KeyController {
@Autowired @Autowired
private KeyManager keyMgr private KeyManager keyMgr
private Gson gson = new Gson();
private String validKey = gson.toJson(new KeyValidityJson(true));
private String invalidKey = gson.toJson(new KeyValidityJson(false));
@RequestMapping(value = "/pubkey/{keyType}:{keyId}", method = GET) @RequestMapping(value = "/pubkey/{keyType}:{keyId}", method = GET)
String getKey(@PathVariable String keyType, @PathVariable int keyId) { String getKey(@PathVariable String keyType, @PathVariable int keyId) {
if (!"ed25519".contentEquals(keyType)) { if (!"ed25519".contentEquals(keyType)) {
@@ -59,9 +64,9 @@ class KeyController {
@RequestMapping(value = "/pubkey/ephemeral/isvalid", method = GET) @RequestMapping(value = "/pubkey/ephemeral/isvalid", method = GET)
String checkEphemeralKeyValidity(HttpServletRequest request) { String checkEphemeralKeyValidity(HttpServletRequest request) {
log.error("{} was requested but not implemented", request.getRequestURL()) log.warn("Ephemeral key was request but no ephemeral key are generated, replying not valid")
throw new NotImplementedException() return invalidKey
} }
@RequestMapping(value = "/pubkey/isvalid", method = GET) @RequestMapping(value = "/pubkey/isvalid", method = GET)
@@ -70,9 +75,7 @@ class KeyController {
// TODO do in manager // TODO do in manager
boolean valid = StringUtils.equals(pubKey, keyMgr.getPublicKeyBase64(keyMgr.getCurrentIndex())) boolean valid = StringUtils.equals(pubKey, keyMgr.getPublicKeyBase64(keyMgr.getCurrentIndex()))
return JsonOutput.toJson( return valid ? validKey : invalidKey
valid: valid
)
} }
} }

View File

@@ -45,7 +45,7 @@ import static org.springframework.web.bind.annotation.RequestMethod.POST
@RestController @RestController
@CrossOrigin @CrossOrigin
@RequestMapping(path = "/_matrix/identity/api/v1", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) @RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
class MappingController { class MappingController {
private Logger log = LoggerFactory.getLogger(MappingController.class) private Logger log = LoggerFactory.getLogger(MappingController.class)

View File

@@ -25,6 +25,7 @@ import com.google.gson.JsonObject
import io.kamax.mxisd.controller.v1.io.SessionEmailTokenRequestJson import io.kamax.mxisd.controller.v1.io.SessionEmailTokenRequestJson
import io.kamax.mxisd.controller.v1.io.SessionPhoneTokenRequestJson import io.kamax.mxisd.controller.v1.io.SessionPhoneTokenRequestJson
import io.kamax.mxisd.exception.BadRequestException import io.kamax.mxisd.exception.BadRequestException
import io.kamax.mxisd.invitation.InvitationManager
import io.kamax.mxisd.lookup.ThreePidValidation import io.kamax.mxisd.lookup.ThreePidValidation
import io.kamax.mxisd.mapping.MappingManager import io.kamax.mxisd.mapping.MappingManager
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
@@ -42,12 +43,15 @@ import java.nio.charset.StandardCharsets
@RestController @RestController
@CrossOrigin @CrossOrigin
@RequestMapping(path = "/_matrix/identity/api/v1", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) @RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
class SessionController { class SessionController {
@Autowired @Autowired
private MappingManager mgr private MappingManager mgr
@Autowired
private InvitationManager invMgr;
private Gson gson = new Gson() private Gson gson = new Gson()
private Logger log = LoggerFactory.getLogger(SessionController.class) private Logger log = LoggerFactory.getLogger(SessionController.class)
@@ -131,6 +135,10 @@ class SessionController {
obj.addProperty("error", e.getMessage()) obj.addProperty("error", e.getMessage())
response.setStatus(HttpStatus.SC_BAD_REQUEST) response.setStatus(HttpStatus.SC_BAD_REQUEST)
return gson.toJson(obj) 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,6 +20,8 @@
package io.kamax.mxisd.controller.v1; package io.kamax.mxisd.controller.v1;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@@ -30,10 +32,18 @@ import org.springframework.web.bind.annotation.RestController;
@RequestMapping(produces = MediaType.APPLICATION_JSON_UTF8_VALUE) @RequestMapping(produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public class StatusController { public class StatusController {
private Gson gson = new Gson();
@RequestMapping(value = "/_matrix/identity/status") @RequestMapping(value = "/_matrix/identity/status")
public String getStatus() { public String getStatus() {
// TODO link to backend // TODO link to backend
return "{\"status\":{\"health\":\"OK\"}}"; JsonObject status = new JsonObject();
status.addProperty("health", "OK");
JsonObject obj = new JsonObject();
obj.add("status", status);
return gson.toJson(obj);
} }
} }

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.controller.v1.io;
public class KeyValidityJson {
private boolean valid;
public KeyValidityJson(boolean isValid) {
this.valid = isValid;
}
public boolean isValid() {
return valid;
}
}

View File

@@ -22,6 +22,8 @@ package io.kamax.mxisd.invitation;
import com.google.gson.Gson; import com.google.gson.Gson;
import io.kamax.matrix.MatrixID; import io.kamax.matrix.MatrixID;
import io.kamax.mxisd.config.DnsOverwrite;
import io.kamax.mxisd.config.DnsOverwriteEntry;
import io.kamax.mxisd.exception.BadRequestException; import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.exception.MappingAlreadyExistsException; import io.kamax.mxisd.exception.MappingAlreadyExistsException;
import io.kamax.mxisd.invitation.sender.IInviteSender; import io.kamax.mxisd.invitation.sender.IInviteSender;
@@ -78,6 +80,9 @@ public class InvitationManager {
@Autowired @Autowired
private SignatureManager signMgr; private SignatureManager signMgr;
@Autowired
private DnsOverwrite dns;
private Map<String, IInviteSender> senders; private Map<String, IInviteSender> senders;
private CloseableHttpClient client; private CloseableHttpClient client;
@@ -85,7 +90,7 @@ public class InvitationManager {
private Timer refreshTimer; private Timer refreshTimer;
private String getId(IThreePidInvite invite) { private String getId(IThreePidInvite invite) {
return invite.getSender().getDomain() + invite.getMedium() + invite.getAddress(); return invite.getSender().getDomain().toLowerCase() + invite.getMedium().toLowerCase() + invite.getAddress().toLowerCase();
} }
@PostConstruct @PostConstruct
@@ -149,6 +154,13 @@ public class InvitationManager {
// TODO use caching mechanism // TODO use caching mechanism
// TODO export in matrix-java-sdk // TODO export in matrix-java-sdk
String findHomeserverForDomain(String domain) { String findHomeserverForDomain(String domain) {
Optional<DnsOverwriteEntry> entryOpt = dns.findHost(domain);
if (entryOpt.isPresent()) {
DnsOverwriteEntry entry = entryOpt.get();
log.info("Found DNS overwrite for {} to {}", entry.getName(), entry.getTarget());
return "https://" + entry.getTarget();
}
log.debug("Performing SRV lookup for {}", domain); log.debug("Performing SRV lookup for {}", domain);
String lookupDns = getSrvRecordName(domain); String lookupDns = getSrvRecordName(domain);
log.info("Lookup name: {}", lookupDns); log.info("Lookup name: {}", lookupDns);
@@ -223,17 +235,19 @@ public class InvitationManager {
} }
public void lookupMappingsForInvites() { public void lookupMappingsForInvites() {
log.info("Checking for existing mapping for pending invites"); if (!invitations.isEmpty()) {
for (IThreePidInviteReply reply : invitations.values()) { log.info("Checking for existing mapping for pending invites");
log.info("Processing invite {}", getIdForLog(reply)); for (IThreePidInviteReply reply : invitations.values()) {
ForkJoinPool.commonPool().submit(new MappingChecker(reply)); log.info("Processing invite {}", getIdForLog(reply));
ForkJoinPool.commonPool().submit(new MappingChecker(reply));
}
} }
} }
public void publishMappingIfInvited(ThreePidMapping threePid) { public void publishMappingIfInvited(ThreePidMapping threePid) {
log.info("Looking up possible pending invites for {}:{}", threePid.getMedium(), threePid.getValue()); log.info("Looking up possible pending invites for {}:{}", threePid.getMedium(), threePid.getValue());
for (IThreePidInviteReply reply : invitations.values()) { for (IThreePidInviteReply reply : invitations.values()) {
if (StringUtils.equals(reply.getInvite().getMedium(), threePid.getMedium()) && StringUtils.equals(reply.getInvite().getAddress(), threePid.getValue())) { if (StringUtils.equalsIgnoreCase(reply.getInvite().getMedium(), threePid.getMedium()) && StringUtils.equalsIgnoreCase(reply.getInvite().getAddress(), threePid.getValue())) {
log.info("{}:{} has an invite pending on HS {}, publishing mapping", threePid.getMedium(), threePid.getValue(), reply.getInvite().getSender().getDomain()); log.info("{}:{} has an invite pending on HS {}, publishing mapping", threePid.getMedium(), threePid.getValue(), reply.getInvite().getSender().getDomain());
publishMapping(reply, threePid.getMxid()); publishMapping(reply, threePid.getMxid());
} }

View File

@@ -22,7 +22,7 @@ package io.kamax.mxisd.invitation.sender;
import com.sun.mail.smtp.SMTPTransport; import com.sun.mail.smtp.SMTPTransport;
import io.kamax.matrix.ThreePidMedium; import io.kamax.matrix.ThreePidMedium;
import io.kamax.mxisd.config.ServerConfig; import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.config.invite.sender.EmailSenderConfig; import io.kamax.mxisd.config.invite.sender.EmailSenderConfig;
import io.kamax.mxisd.exception.ConfigurationException; import io.kamax.mxisd.exception.ConfigurationException;
import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.invitation.IThreePidInviteReply;
@@ -56,7 +56,7 @@ public class EmailInviteSender implements IInviteSender {
private EmailSenderConfig cfg; private EmailSenderConfig cfg;
@Autowired @Autowired
private ServerConfig srvCfg; private MatrixConfig mxCfg;
@Autowired @Autowired
private ApplicationContext app; private ApplicationContext app;
@@ -87,7 +87,7 @@ public class EmailInviteSender implements IInviteSender {
} }
try { try {
String domainPretty = WordUtils.capitalizeFully(srvCfg.getName()); String domainPretty = WordUtils.capitalizeFully(mxCfg.getDomain());
String senderName = invite.getInvite().getProperties().getOrDefault("sender_display_name", ""); String senderName = invite.getInvite().getProperties().getOrDefault("sender_display_name", "");
String senderNameOrId = StringUtils.defaultIfBlank(senderName, invite.getInvite().getSender().getId()); String senderNameOrId = StringUtils.defaultIfBlank(senderName, invite.getInvite().getSender().getId());
String roomName = invite.getInvite().getProperties().getOrDefault("room_name", ""); String roomName = invite.getInvite().getProperties().getOrDefault("room_name", "");
@@ -97,7 +97,7 @@ public class EmailInviteSender implements IInviteSender {
StringUtils.startsWith(cfg.getTemplate(), "classpath:") ? StringUtils.startsWith(cfg.getTemplate(), "classpath:") ?
app.getResource(cfg.getTemplate()).getInputStream() : new FileInputStream(cfg.getTemplate()), app.getResource(cfg.getTemplate()).getInputStream() : new FileInputStream(cfg.getTemplate()),
StandardCharsets.UTF_8); StandardCharsets.UTF_8);
templateBody = templateBody.replace("%DOMAIN%", srvCfg.getName()); templateBody = templateBody.replace("%DOMAIN%", mxCfg.getDomain());
templateBody = templateBody.replace("%DOMAIN_PRETTY%", domainPretty); templateBody = templateBody.replace("%DOMAIN_PRETTY%", domainPretty);
templateBody = templateBody.replace("%FROM_EMAIL%", cfg.getEmail()); templateBody = templateBody.replace("%FROM_EMAIL%", cfg.getEmail());
templateBody = templateBody.replace("%FROM_NAME%", cfg.getName()); templateBody = templateBody.replace("%FROM_NAME%", cfg.getName());

View File

@@ -20,7 +20,7 @@
package io.kamax.mxisd.lookup.provider package io.kamax.mxisd.lookup.provider
import io.kamax.mxisd.config.ServerConfig import io.kamax.mxisd.config.MatrixConfig
import io.kamax.mxisd.lookup.SingleLookupReply import io.kamax.mxisd.lookup.SingleLookupReply
import io.kamax.mxisd.lookup.SingleLookupRequest import io.kamax.mxisd.lookup.SingleLookupRequest
import io.kamax.mxisd.lookup.ThreePidMapping import io.kamax.mxisd.lookup.ThreePidMapping
@@ -44,7 +44,7 @@ class DnsLookupProvider implements IThreePidProvider {
private Logger log = LoggerFactory.getLogger(DnsLookupProvider.class) private Logger log = LoggerFactory.getLogger(DnsLookupProvider.class)
@Autowired @Autowired
private ServerConfig srvCfg private MatrixConfig mxCfg
@Autowired @Autowired
private IRemoteIdentityServerFetcher fetcher private IRemoteIdentityServerFetcher fetcher
@@ -79,7 +79,7 @@ class DnsLookupProvider implements IThreePidProvider {
// TODO use caching mechanism // TODO use caching mechanism
Optional<String> findIdentityServerForDomain(String domain) { Optional<String> findIdentityServerForDomain(String domain) {
if (StringUtils.equals(srvCfg.getName(), domain)) { if (StringUtils.equals(mxCfg.getDomain(), domain)) {
log.info("We are authoritative for {}, no remote lookup", domain) log.info("We are authoritative for {}, no remote lookup", domain)
return Optional.empty() return Optional.empty()
} }

View File

@@ -20,7 +20,7 @@
package io.kamax.mxisd.lookup.provider package io.kamax.mxisd.lookup.provider
import io.kamax.mxisd.config.ServerConfig import io.kamax.mxisd.config.MatrixConfig
import io.kamax.mxisd.config.ldap.LdapConfig import io.kamax.mxisd.config.ldap.LdapConfig
import io.kamax.mxisd.lookup.SingleLookupReply import io.kamax.mxisd.lookup.SingleLookupReply
import io.kamax.mxisd.lookup.SingleLookupRequest import io.kamax.mxisd.lookup.SingleLookupRequest
@@ -47,7 +47,7 @@ class LdapProvider implements IThreePidProvider {
private Logger log = LoggerFactory.getLogger(LdapProvider.class) private Logger log = LoggerFactory.getLogger(LdapProvider.class)
@Autowired @Autowired
private ServerConfig srvCfg private MatrixConfig mxCfg
@Autowired @Autowired
private LdapConfig ldapCfg private LdapConfig ldapCfg
@@ -111,7 +111,7 @@ class LdapProvider implements IThreePidProvider {
// TODO Should we turn this block into a map of functions? // TODO Should we turn this block into a map of functions?
String uidType = ldapCfg.getAttribute().getUid().getType() String uidType = ldapCfg.getAttribute().getUid().getType()
if (StringUtils.equals(UID, uidType)) { if (StringUtils.equals(UID, uidType)) {
matrixId.append("@").append(data).append(":").append(srvCfg.getName()) matrixId.append("@").append(data).append(":").append(mxCfg.getDomain())
} else if (StringUtils.equals(MATRIX_ID, uidType)) { } else if (StringUtils.equals(MATRIX_ID, uidType)) {
matrixId.append(data) matrixId.append(data)
} else { } else {

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.lookup.provider;
import io.kamax.matrix.MatrixID;
import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.config.sql.SqlProviderConfig;
import io.kamax.mxisd.lookup.SingleLookupReply;
import io.kamax.mxisd.lookup.SingleLookupRequest;
import io.kamax.mxisd.lookup.ThreePidMapping;
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 java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Component
public class SqlProvider implements IThreePidProvider {
private Logger log = LoggerFactory.getLogger(SqlProvider.class);
@Autowired
private MatrixConfig mxCfg;
@Autowired
private SqlProviderConfig cfg;
@Override
public boolean isEnabled() {
return cfg.isEnabled();
}
@Override
public boolean isLocal() {
return true;
}
@Override
public int getPriority() {
return 20;
}
private Connection getConn() throws SQLException {
return DriverManager.getConnection("jdbc:" + cfg.getType() + ":" + cfg.getConnection());
}
@Override
public Optional<SingleLookupReply> find(SingleLookupRequest request) {
log.info("SQL lookup");
String stmtSql = StringUtils.defaultIfBlank(cfg.getIdentity().getMedium().get(request.getType()), cfg.getIdentity().getQuery());
log.info("SQL query: {}", stmtSql);
try (PreparedStatement stmt = getConn().prepareStatement(stmtSql)) {
stmt.setString(1, request.getType().toLowerCase());
stmt.setString(2, request.getThreePid().toLowerCase());
ResultSet rSet = stmt.executeQuery();
while (rSet.next()) {
String uid = rSet.getString("uid");
log.info("Found match: {}", uid);
if (StringUtils.equals("uid", cfg.getIdentity().getType())) {
log.info("Resolving as localpart");
return Optional.of(new SingleLookupReply(request, new MatrixID(uid, mxCfg.getDomain())));
}
if (StringUtils.equals("mxid", cfg.getIdentity().getType())) {
log.info("Resolving as MXID");
return Optional.of(new SingleLookupReply(request, new MatrixID(uid)));
}
log.info("Identity type is unknown, skipping");
}
log.info("No match found in SQL");
return Optional.empty();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public List<ThreePidMapping> populate(List<ThreePidMapping> mappings) {
return new ArrayList<>();
}
}

View File

@@ -36,7 +36,6 @@ public class MappingManager {
private Logger log = LoggerFactory.getLogger(MappingManager.class); private Logger log = LoggerFactory.getLogger(MappingManager.class);
private Map<String, Session> threePidLookups = new WeakHashMap<>();
private Map<String, Session> sessions = new HashMap<>(); private Map<String, Session> sessions = new HashMap<>();
private Timer cleaner; private Timer cleaner;
@@ -51,7 +50,6 @@ public class MappingManager {
log.info("Session {} is obsolete, removing", s.sid); log.info("Session {} is obsolete, removing", s.sid);
sessions.remove(s.sid); sessions.remove(s.sid);
threePidLookups.remove(s.hash);
} }
} }
} }
@@ -65,16 +63,9 @@ public class MappingManager {
} while (sessions.containsKey(sid)); } while (sessions.containsKey(sid));
String threePidHash = data.getMedium() + data.getValue(); String threePidHash = data.getMedium() + data.getValue();
Session session = threePidLookups.get(threePidHash); // TODO think how to handle different requests for the same e-mail
if (session != null) { Session session = new Session(sid, threePidHash, data);
sid = session.sid; sessions.put(sid, session);
} else {
// TODO perform some kind of validation
session = new Session(sid, threePidHash, data);
sessions.put(sid, session);
threePidLookups.put(threePidHash, session);
}
log.info("Created new session {} to validate {} {}", sid, session.medium, session.address); log.info("Created new session {} to validate {} {}", sid, session.medium, session.address);
return sid; return sid;