Compare commits

...

23 Commits

Author SHA1 Message Date
Max Dor
795798ee06 Research code for #14 2019-03-08 23:25:49 +01:00
Max Dor
57c7e4a91d Show signatures into admin lookup queries 2019-03-04 02:12:55 +01:00
Max Dor
1dce59a02e Add lookup and invite commands to the admin AS interface 2019-03-04 00:02:13 +01:00
Max Dor
de840b9d00 Skeleton for modular AS admin command processing 2019-03-03 16:39:58 +01:00
Max Dor
53c85d2248 Package/Class refactoring (no-op) 2019-03-03 03:44:38 +01:00
Max Dor
254dc5684f Add mechanisms for 3PID invite expiration and AS integration
- Integration with AS and a fallback user to decline expired invites (#120)
- Rework of the AS feature to make it more independent/re-usable
- Skeleton for admin interface via bot to manage invites (#138)
2019-03-02 03:21:29 +01:00
Max Dor
de92e98f7d Save work in progress 2019-03-01 17:51:33 +01:00
Max Dor
d5f9137056 split into app svc processor 2019-03-01 15:58:37 +01:00
Max Dor
1307e3aa43 Add missing javadoc 2019-03-01 15:18:47 +01:00
Max Dor
dfedde0df6 Improve crypto
- Re-organize packages to be consistent
- Add Key store tests
2019-03-01 15:16:19 +01:00
Max Dor
93bd7354c2 Improve Authentication doc 2019-03-01 12:42:13 +01:00
Max Dor
c302789898 Add mechanism for 3PID invites expiration (#120) 2019-03-01 06:51:18 +01:00
Max Dor
96155c1876 Improving logging 2019-03-01 01:12:02 +01:00
Max Dor
95ee328281 Block custom internal endpoint that should never be called
- Is not spec'd
- Will not be spec'd
- Is 100% internal as per its authors
2019-02-25 14:06:32 +01:00
Max Dor
72a1794cc3 Skeleton for 3PID registration policies (#130) 2019-02-18 23:08:50 +01:00
Max Dor
37ddd0e588 Talk about server.name in the example config 2019-02-17 03:22:48 +01:00
Max Dor
4d63bba251 Add version in jar
- Cli argument
- In HTTP client
- /version endpoint
2019-02-17 02:08:50 +01:00
Max Dor
aadfae2965 Skeleton for invitation policies (#130) 2019-02-17 02:08:50 +01:00
Max Dor
2f7e5e4025 Fix migration in case of empty dir 2019-02-17 02:08:50 +01:00
Max Dor
77dc75d383 Basic check for pending invite when requesting token on registration 2019-02-17 02:08:50 +01:00
Max Dor
f3b528d1ba Store ephemeral key in invite and add support for /sign-ed25519 2019-02-17 02:08:50 +01:00
Max Dor
91e5e08e70 Support for all key types 2019-02-17 02:08:50 +01:00
Max Dor
acd8c7d7c5 Skeleton for full support of all key types 2019-02-17 02:08:50 +01:00
91 changed files with 4633 additions and 332 deletions

View File

@@ -146,12 +146,23 @@ dependencies {
// HTTP server // HTTP server
compile 'io.undertow:undertow-core:2.0.16.Final' compile 'io.undertow:undertow-core:2.0.16.Final'
// Command parser for AS interface
implementation 'commons-cli:commons-cli:1.4'
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'
testCompile 'com.github.tomakehurst:wiremock:2.8.0' testCompile 'com.github.tomakehurst:wiremock:2.8.0'
testCompile 'com.unboundid:unboundid-ldapsdk:4.0.9' testCompile 'com.unboundid:unboundid-ldapsdk:4.0.9'
testCompile 'com.icegreen:greenmail:1.5.9' testCompile 'com.icegreen:greenmail:1.5.9'
} }
jar {
manifest {
attributes(
'Implementation-Version': mxisdVersion()
)
}
}
shadowJar { shadowJar {
baseName = project.name baseName = project.name
classifier = null classifier = null

View File

@@ -21,7 +21,7 @@ It allows to use Identity stores configured in mxisd to authenticate users on yo
Authentication is divided into two parts: Authentication is divided into two parts:
- [Basic](#basic): authenticate with a regular username. - [Basic](#basic): authenticate with a regular username.
- [Advanced](#advanced): same as basic with extra ability to authenticate using a 3PID. - [Advanced](#advanced): same as basic with extra abilities like authenticate using a 3PID or do username rewrite.
## Basic ## Basic
Authentication by username is possible by linking synapse and mxisd together using a specific module for synapse, also Authentication by username is possible by linking synapse and mxisd together using a specific module for synapse, also
@@ -145,7 +145,49 @@ Your VirtualHost should now look similar to:
</VirtualHost> </VirtualHost>
``` ```
##### nginx
The specific configuration to add under the relevant `server`:
```nginx
location /_matrix/client/r0/login {
proxy_pass http://localhost:8090;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
}
```
Your `server` section should now look similar to:
```nginx
server {
listen 443 ssl;
server_name matrix.example.org;
# ...
location /_matrix/client/r0/login {
proxy_pass http://localhost:8090;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
}
location /_matrix/identity {
proxy_pass http://localhost:8090/_matrix/identity;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
}
location /_matrix {
proxy_pass http://localhost:8008/_matrix;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
}
}
```
#### DNS Overwrite #### DNS Overwrite
Just like you need to configure a reverse proxy to send client requests to mxisd, you also need to configure mxisd with Just like you need to configure a reverse proxy to send client requests to mxisd, you also need to configure mxisd with
the internal IP of the Homeserver so it can talk to it directly to integrate its directory search. the internal IP of the Homeserver so it can talk to it directly to integrate its directory search.
@@ -165,6 +207,12 @@ In case the hostname is the same as your Matrix domain and `server.name` is not
`value` is the base internal URL of the Homeserver, without any `/_matrix/..` or trailing `/`. `value` is the base internal URL of the Homeserver, without any `/_matrix/..` or trailing `/`.
### Optional features
The following features are available after you have a working Advanced setup:
- Username rewrite: Allows you to rewrite the username of a regular login/pass authentication to a 3PID, that then gets resolved using the regular lookup process. Most common use case is to allow login with numerical usernames on synapse, which is not possible out of the box.
#### Username rewrite #### Username rewrite
In mxisd config: In mxisd config:
```yaml ```yaml

View File

@@ -14,6 +14,11 @@
# NOTE: in Synapse Homeserver, the Matrix domain is defined as 'server_name' in configuration file. # NOTE: in Synapse Homeserver, the Matrix domain is defined as 'server_name' in configuration file.
# #
# This is used to build the various identifiers in all the features. # This is used to build the various identifiers in all the features.
#
# If the hostname of the public URL used to reach your Matrix services is different from your Matrix domain,
# per example matrix.domain.tld vs domain.tld, then use the server.name configuration option.
# See the "Configure" section of the Getting Started guide for more info.
#
matrix: matrix:
domain: '' domain: ''

View File

@@ -21,23 +21,30 @@
package io.kamax.mxisd; package io.kamax.mxisd;
import io.kamax.mxisd.config.MxisdConfig; import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.http.undertow.handler.InternalInfoHandler;
import io.kamax.mxisd.http.undertow.handler.OptionsHandler; import io.kamax.mxisd.http.undertow.handler.OptionsHandler;
import io.kamax.mxisd.http.undertow.handler.SaneHandler; import io.kamax.mxisd.http.undertow.handler.SaneHandler;
import io.kamax.mxisd.http.undertow.handler.as.v1.AsNotFoundHandler; import io.kamax.mxisd.http.undertow.handler.as.v1.AsNotFoundHandler;
import io.kamax.mxisd.http.undertow.handler.as.v1.AsTransactionHandler; import io.kamax.mxisd.http.undertow.handler.as.v1.AsTransactionHandler;
import io.kamax.mxisd.http.undertow.handler.as.v1.AsUserHandler;
import io.kamax.mxisd.http.undertow.handler.auth.RestAuthHandler; import io.kamax.mxisd.http.undertow.handler.auth.RestAuthHandler;
import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginGetHandler; import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginGetHandler;
import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginHandler; import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginHandler;
import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginPostHandler; import io.kamax.mxisd.http.undertow.handler.auth.v1.LoginPostHandler;
import io.kamax.mxisd.http.undertow.handler.directory.v1.UserDirectorySearchHandler; import io.kamax.mxisd.http.undertow.handler.directory.v1.UserDirectorySearchHandler;
import io.kamax.mxisd.http.undertow.handler.identity.v1.*; import io.kamax.mxisd.http.undertow.handler.identity.v1.*;
import io.kamax.mxisd.http.undertow.handler.invite.v1.RoomInviteHandler;
import io.kamax.mxisd.http.undertow.handler.profile.v1.InternalProfileHandler; import io.kamax.mxisd.http.undertow.handler.profile.v1.InternalProfileHandler;
import io.kamax.mxisd.http.undertow.handler.profile.v1.ProfileHandler; import io.kamax.mxisd.http.undertow.handler.profile.v1.ProfileHandler;
import io.kamax.mxisd.http.undertow.handler.register.v1.Register3pidRequestTokenHandler;
import io.kamax.mxisd.http.undertow.handler.status.StatusHandler; import io.kamax.mxisd.http.undertow.handler.status.StatusHandler;
import io.kamax.mxisd.http.undertow.handler.status.VersionHandler;
import io.undertow.Handlers; import io.undertow.Handlers;
import io.undertow.Undertow; import io.undertow.Undertow;
import io.undertow.server.HttpHandler; import io.undertow.server.HttpHandler;
import java.util.Objects;
public class HttpMxisd { public class HttpMxisd {
// Core // Core
@@ -46,6 +53,12 @@ public class HttpMxisd {
// I/O // I/O
private Undertow httpSrv; private Undertow httpSrv;
static {
// Used in XNIO package, dependency of Undertow
// We switch to slf4j
System.setProperty("org.jboss.logging.provider", "slf4j");
}
public HttpMxisd(MxisdConfig cfg) { public HttpMxisd(MxisdConfig cfg) {
m = new Mxisd(cfg); m = new Mxisd(cfg);
} }
@@ -54,9 +67,12 @@ public class HttpMxisd {
m.start(); m.start();
HttpHandler helloHandler = SaneHandler.around(new HelloHandler()); HttpHandler helloHandler = SaneHandler.around(new HelloHandler());
HttpHandler asNotFoundHandler = SaneHandler.around(new AsNotFoundHandler(m.getAs()));
HttpHandler asUserHandler = SaneHandler.around(new AsUserHandler(m.getAs()));
HttpHandler asTxnHandler = SaneHandler.around(new AsTransactionHandler(m.getAs())); HttpHandler asTxnHandler = SaneHandler.around(new AsTransactionHandler(m.getAs()));
HttpHandler storeInvHandler = SaneHandler.around(new StoreInviteHandler(m.getConfig().getServer(), m.getInvitationManager(), m.getKeyManager())); HttpHandler asNotFoundHandler = SaneHandler.around(new AsNotFoundHandler(m.getAs()));
HttpHandler storeInvHandler = SaneHandler.around(new StoreInviteHandler(m.getConfig().getServer(), m.getInvite(), m.getKeyManager()));
HttpHandler sessValidateHandler = SaneHandler.around(new SessionValidateHandler(m.getSession(), m.getConfig().getServer(), m.getConfig().getView())); HttpHandler sessValidateHandler = SaneHandler.around(new SessionValidateHandler(m.getSession(), m.getConfig().getServer(), m.getConfig().getView()));
httpSrv = Undertow.builder().addHttpListener(m.getConfig().getServer().getPort(), "0.0.0.0").setHandler(Handlers.routing() httpSrv = Undertow.builder().addHttpListener(m.getConfig().getServer().getPort(), "0.0.0.0").setHandler(Handlers.routing()
@@ -65,6 +81,7 @@ public class HttpMxisd {
// Status endpoints // Status endpoints
.get(StatusHandler.Path, SaneHandler.around(new StatusHandler())) .get(StatusHandler.Path, SaneHandler.around(new StatusHandler()))
.get(VersionHandler.Path, SaneHandler.around(new VersionHandler()))
// Authentication endpoints // Authentication endpoints
.get(LoginHandler.Path, SaneHandler.around(new LoginGetHandler(m.getAuth(), m.getHttpClient()))) .get(LoginHandler.Path, SaneHandler.around(new LoginGetHandler(m.getAuth(), m.getHttpClient())))
@@ -77,40 +94,53 @@ public class HttpMxisd {
// Key endpoints // Key endpoints
.get(KeyGetHandler.Path, SaneHandler.around(new KeyGetHandler(m.getKeyManager()))) .get(KeyGetHandler.Path, SaneHandler.around(new KeyGetHandler(m.getKeyManager())))
.get(RegularKeyIsValidHandler.Path, SaneHandler.around(new RegularKeyIsValidHandler(m.getKeyManager()))) .get(RegularKeyIsValidHandler.Path, SaneHandler.around(new RegularKeyIsValidHandler(m.getKeyManager())))
.get(EphemeralKeyIsValidHandler.Path, SaneHandler.around(new EphemeralKeyIsValidHandler())) .get(EphemeralKeyIsValidHandler.Path, SaneHandler.around(new EphemeralKeyIsValidHandler(m.getKeyManager())))
// Identity endpoints // Identity endpoints
.get(HelloHandler.Path, helloHandler) .get(HelloHandler.Path, helloHandler)
.get(HelloHandler.Path + "/", helloHandler) // Be lax with possibly trailing slash .get(HelloHandler.Path + "/", helloHandler) // Be lax with possibly trailing slash
.get(SingleLookupHandler.Path, SaneHandler.around(new SingleLookupHandler(m.getIdentity(), m.getSign()))) .get(SingleLookupHandler.Path, SaneHandler.around(new SingleLookupHandler(m.getConfig(), m.getIdentity(), m.getSign())))
.post(BulkLookupHandler.Path, SaneHandler.around(new BulkLookupHandler(m.getIdentity()))) .post(BulkLookupHandler.Path, SaneHandler.around(new BulkLookupHandler(m.getIdentity())))
.post(StoreInviteHandler.Path, storeInvHandler) .post(StoreInviteHandler.Path, storeInvHandler)
.post(SessionStartHandler.Path, SaneHandler.around(new SessionStartHandler(m.getSession()))) .post(SessionStartHandler.Path, SaneHandler.around(new SessionStartHandler(m.getSession())))
.get(SessionValidateHandler.Path, sessValidateHandler) .get(SessionValidateHandler.Path, sessValidateHandler)
.post(SessionValidateHandler.Path, sessValidateHandler) .post(SessionValidateHandler.Path, sessValidateHandler)
.get(SessionTpidGetValidatedHandler.Path, SaneHandler.around(new SessionTpidGetValidatedHandler(m.getSession()))) .get(SessionTpidGetValidatedHandler.Path, SaneHandler.around(new SessionTpidGetValidatedHandler(m.getSession())))
.post(SessionTpidBindHandler.Path, SaneHandler.around(new SessionTpidBindHandler(m.getSession(), m.getInvitationManager()))) .post(SessionTpidBindHandler.Path, SaneHandler.around(new SessionTpidBindHandler(m.getSession(), m.getInvite())))
.post(SessionTpidUnbindHandler.Path, SaneHandler.around(new SessionTpidUnbindHandler(m.getSession()))) .post(SessionTpidUnbindHandler.Path, SaneHandler.around(new SessionTpidUnbindHandler(m.getSession())))
.post(SignEd25519Handler.Path, SaneHandler.around(new SignEd25519Handler(m.getConfig(), m.getInvite(), m.getSign())))
// Profile endpoints // Profile endpoints
.get(ProfileHandler.Path, SaneHandler.around(new ProfileHandler(m.getProfile()))) .get(ProfileHandler.Path, SaneHandler.around(new ProfileHandler(m.getProfile())))
.get(InternalProfileHandler.Path, SaneHandler.around(new InternalProfileHandler(m.getProfile()))) .get(InternalProfileHandler.Path, SaneHandler.around(new InternalProfileHandler(m.getProfile())))
// Registration endpoints
.post(Register3pidRequestTokenHandler.Path, SaneHandler.around(new Register3pidRequestTokenHandler(m.getReg(), m.getClientDns(), m.getHttpClient())))
// Invite endpoints
.post(RoomInviteHandler.Path, SaneHandler.around(new RoomInviteHandler(m.getHttpClient(), m.getClientDns(), m.getInvite())))
// Application Service endpoints // Application Service endpoints
.get("/_matrix/app/v1/users/**", asNotFoundHandler) .get(AsUserHandler.Path, asUserHandler)
.get("/users/**", asNotFoundHandler) // Legacy endpoint
.get("/_matrix/app/v1/rooms/**", asNotFoundHandler) .get("/_matrix/app/v1/rooms/**", asNotFoundHandler)
.get("/rooms/**", asNotFoundHandler) // Legacy endpoint
.put(AsTransactionHandler.Path, asTxnHandler) .put(AsTransactionHandler.Path, asTxnHandler)
.get("/users/{" + AsUserHandler.ID + "}", asUserHandler) // Legacy endpoint
.get("/rooms/**", asNotFoundHandler) // Legacy endpoint
.put("/transactions/{" + AsTransactionHandler.ID + "}", asTxnHandler) // Legacy endpoint .put("/transactions/{" + AsTransactionHandler.ID + "}", asTxnHandler) // Legacy endpoint
// Banned endpoints
.get(InternalInfoHandler.Path, SaneHandler.around(new InternalInfoHandler()))
).build(); ).build();
httpSrv.start(); httpSrv.start();
} }
public void stop() { public void stop() {
httpSrv.stop(); // Because it might have never been initialized if an exception is thrown early
if (Objects.nonNull(httpSrv)) httpSrv.stop();
m.stop(); m.stop();
} }

View File

@@ -20,8 +20,6 @@
package io.kamax.mxisd; package io.kamax.mxisd;
import io.kamax.matrix.crypto.KeyManager;
import io.kamax.matrix.crypto.SignatureManager;
import io.kamax.mxisd.as.AppSvcManager; import io.kamax.mxisd.as.AppSvcManager;
import io.kamax.mxisd.auth.AuthManager; import io.kamax.mxisd.auth.AuthManager;
import io.kamax.mxisd.auth.AuthProviders; import io.kamax.mxisd.auth.AuthProviders;
@@ -29,6 +27,9 @@ import io.kamax.mxisd.backend.IdentityStoreSupplier;
import io.kamax.mxisd.backend.sql.synapse.Synapse; import io.kamax.mxisd.backend.sql.synapse.Synapse;
import io.kamax.mxisd.config.MxisdConfig; import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.crypto.CryptoFactory; import io.kamax.mxisd.crypto.CryptoFactory;
import io.kamax.mxisd.crypto.KeyManager;
import io.kamax.mxisd.crypto.SignatureManager;
import io.kamax.mxisd.crypto.ed25519.Ed25519KeyManager;
import io.kamax.mxisd.directory.DirectoryManager; import io.kamax.mxisd.directory.DirectoryManager;
import io.kamax.mxisd.directory.DirectoryProviders; import io.kamax.mxisd.directory.DirectoryProviders;
import io.kamax.mxisd.dns.ClientDnsOverwrite; import io.kamax.mxisd.dns.ClientDnsOverwrite;
@@ -46,9 +47,11 @@ import io.kamax.mxisd.notification.NotificationHandlers;
import io.kamax.mxisd.notification.NotificationManager; import io.kamax.mxisd.notification.NotificationManager;
import io.kamax.mxisd.profile.ProfileManager; import io.kamax.mxisd.profile.ProfileManager;
import io.kamax.mxisd.profile.ProfileProviders; import io.kamax.mxisd.profile.ProfileProviders;
import io.kamax.mxisd.registration.RegistrationManager;
import io.kamax.mxisd.session.SessionManager; import io.kamax.mxisd.session.SessionManager;
import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.IStorage;
import io.kamax.mxisd.storage.ormlite.OrmLiteSqlStorage; import io.kamax.mxisd.storage.ormlite.OrmLiteSqlStorage;
import org.apache.commons.lang.StringUtils;
import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.client.HttpClients;
@@ -56,6 +59,10 @@ import java.util.ServiceLoader;
public class Mxisd { public class Mxisd {
public static final String Name = StringUtils.defaultIfBlank(Mxisd.class.getPackage().getImplementationTitle(), "mxisd");
public static final String Version = StringUtils.defaultIfBlank(Mxisd.class.getPackage().getImplementationVersion(), "UNKNOWN");
public static final String Agent = Name + "/" + Version;
private MxisdConfig cfg; private MxisdConfig cfg;
private CloseableHttpClient httpClient; private CloseableHttpClient httpClient;
@@ -63,8 +70,9 @@ public class Mxisd {
private IStorage store; private IStorage store;
private KeyManager keyMgr; private Ed25519KeyManager keyMgr;
private SignatureManager signMgr; private SignatureManager signMgr;
private ClientDnsOverwrite clientDns;
// Features // Features
private AuthManager authMgr; private AuthManager authMgr;
@@ -75,6 +83,10 @@ public class Mxisd {
private AppSvcManager asHander; private AppSvcManager asHander;
private SessionManager sessMgr; private SessionManager sessMgr;
private NotificationManager notifMgr; private NotificationManager notifMgr;
private RegistrationManager regMgr;
// HS-specific classes
private Synapse synapse;
public Mxisd(MxisdConfig cfg) { public Mxisd(MxisdConfig cfg) {
this.cfg = cfg.build(); this.cfg = cfg.build();
@@ -82,7 +94,7 @@ public class Mxisd {
private void build() { private void build() {
httpClient = HttpClients.custom() httpClient = HttpClients.custom()
.setUserAgent("mxisd") .setUserAgent(Agent)
.setMaxConnPerRoute(Integer.MAX_VALUE) .setMaxConnPerRoute(Integer.MAX_VALUE)
.setMaxConnTotal(Integer.MAX_VALUE) .setMaxConnTotal(Integer.MAX_VALUE)
.build(); .build();
@@ -92,10 +104,10 @@ public class Mxisd {
store = new OrmLiteSqlStorage(cfg); store = new OrmLiteSqlStorage(cfg);
keyMgr = CryptoFactory.getKeyManager(cfg.getKey()); keyMgr = CryptoFactory.getKeyManager(cfg.getKey());
signMgr = CryptoFactory.getSignatureManager(keyMgr, cfg.getServer()); signMgr = CryptoFactory.getSignatureManager(keyMgr);
ClientDnsOverwrite clientDns = new ClientDnsOverwrite(cfg.getDns().getOverwrite()); clientDns = new ClientDnsOverwrite(cfg.getDns().getOverwrite());
FederationDnsOverwrite fedDns = new FederationDnsOverwrite(cfg.getDns().getOverwrite()); FederationDnsOverwrite fedDns = new FederationDnsOverwrite(cfg.getDns().getOverwrite());
Synapse synapse = new Synapse(cfg.getSynapseSql()); synapse = new Synapse(cfg.getSynapseSql());
BridgeFetcher bridgeFetcher = new BridgeFetcher(cfg.getLookup().getRecursive().getBridge(), srvFetcher); BridgeFetcher bridgeFetcher = new BridgeFetcher(cfg.getLookup().getRecursive().getBridge(), srvFetcher);
ServiceLoader.load(IdentityStoreSupplier.class).iterator().forEachRemaining(p -> p.accept(this)); ServiceLoader.load(IdentityStoreSupplier.class).iterator().forEachRemaining(p -> p.accept(this));
@@ -105,10 +117,11 @@ public class Mxisd {
pMgr = new ProfileManager(ProfileProviders.get(), clientDns, httpClient); pMgr = new ProfileManager(ProfileProviders.get(), clientDns, httpClient);
notifMgr = new NotificationManager(cfg.getNotification(), NotificationHandlers.get()); notifMgr = new NotificationManager(cfg.getNotification(), NotificationHandlers.get());
sessMgr = new SessionManager(cfg.getSession(), cfg.getMatrix(), store, notifMgr, idStrategy, httpClient); sessMgr = new SessionManager(cfg.getSession(), cfg.getMatrix(), store, notifMgr, idStrategy, httpClient);
invMgr = new InvitationManager(cfg.getInvite(), store, idStrategy, signMgr, fedDns, notifMgr); invMgr = new InvitationManager(cfg, store, idStrategy, keyMgr, signMgr, fedDns, notifMgr, pMgr);
authMgr = new AuthManager(cfg, AuthProviders.get(), idStrategy, invMgr, clientDns, httpClient); authMgr = new AuthManager(cfg, AuthProviders.get(), idStrategy, invMgr, clientDns, httpClient);
dirMgr = new DirectoryManager(cfg.getDirectory(), clientDns, httpClient, DirectoryProviders.get()); dirMgr = new DirectoryManager(cfg.getDirectory(), clientDns, httpClient, DirectoryProviders.get());
asHander = new AppSvcManager(cfg, store, pMgr, notifMgr, synapse); regMgr = new RegistrationManager(cfg.getRegister(), httpClient, clientDns, invMgr);
asHander = new AppSvcManager(this);
} }
public MxisdConfig getConfig() { public MxisdConfig getConfig() {
@@ -119,6 +132,10 @@ public class Mxisd {
return httpClient; return httpClient;
} }
public ClientDnsOverwrite getClientDns() {
return clientDns;
}
public IRemoteIdentityServerFetcher getServerFetcher() { public IRemoteIdentityServerFetcher getServerFetcher() {
return srvFetcher; return srvFetcher;
} }
@@ -127,7 +144,7 @@ public class Mxisd {
return keyMgr; return keyMgr;
} }
public InvitationManager getInvitationManager() { public InvitationManager getInvite() {
return invMgr; return invMgr;
} }
@@ -155,6 +172,10 @@ public class Mxisd {
return signMgr; return signMgr;
} }
public RegistrationManager getReg() {
return regMgr;
}
public AppSvcManager getAs() { public AppSvcManager getAs() {
return asHander; return asHander;
} }
@@ -163,6 +184,14 @@ public class Mxisd {
return notifMgr; return notifMgr;
} }
public IStorage getStore() {
return store;
}
public Synapse getSynapse() {
return synapse;
}
public void start() { public void start() {
build(); build();
} }

View File

@@ -37,21 +37,33 @@ public class MxisdStandaloneExec {
public static void main(String[] args) { public static void main(String[] args) {
try { try {
log.info("------------- mxisd starting -------------");
MxisdConfig cfg = null; MxisdConfig cfg = null;
Iterator<String> argsIt = Arrays.asList(args).iterator(); Iterator<String> argsIt = Arrays.asList(args).iterator();
while (argsIt.hasNext()) { while (argsIt.hasNext()) {
String arg = argsIt.next(); String arg = argsIt.next();
if (StringUtils.equals("-c", arg)) { if (StringUtils.equalsAny(arg, "-h", "--help", "-?", "--usage")) {
System.out.println("Available arguments:" + System.lineSeparator());
System.out.println(" -h, --help Show this help message");
System.out.println(" --version Print the version then exit");
System.out.println(" -c, --config Set the configuration file location");
System.out.println(" ");
System.exit(0);
} else if (StringUtils.equalsAny(arg, "-c", "--config")) {
String cfgFile = argsIt.next(); String cfgFile = argsIt.next();
cfg = YamlConfigLoader.loadFromFile(cfgFile); cfg = YamlConfigLoader.loadFromFile(cfgFile);
} else if (StringUtils.equals("--version", arg)) {
System.out.println(Mxisd.Version);
System.exit(0);
} else { } else {
log.info("Invalid argument: {}", arg); System.err.println("Invalid argument: " + arg);
System.err.println("Try '--help' for available arguments");
System.exit(1); System.exit(1);
} }
} }
log.info("mxisd starting");
log.info("Version: {}", Mxisd.Version);
if (Objects.isNull(cfg)) { if (Objects.isNull(cfg)) {
cfg = YamlConfigLoader.tryLoadFromFile("mxisd.yaml").orElseGet(MxisdConfig::new); cfg = YamlConfigLoader.tryLoadFromFile("mxisd.yaml").orElseGet(MxisdConfig::new);
} }
@@ -59,11 +71,11 @@ public class MxisdStandaloneExec {
HttpMxisd mxisd = new HttpMxisd(cfg); HttpMxisd mxisd = new HttpMxisd(cfg);
Runtime.getRuntime().addShutdownHook(new Thread(() -> { Runtime.getRuntime().addShutdownHook(new Thread(() -> {
mxisd.stop(); mxisd.stop();
log.info("------------- mxisd stopped -------------"); log.info("mxisd stopped");
})); }));
mxisd.start(); mxisd.start();
log.info("------------- mxisd started -------------"); log.info("mxisd started");
} catch (ConfigurationException e) { } catch (ConfigurationException e) {
log.error(e.getDetailedMessage()); log.error(e.getDetailedMessage());
log.error(e.getMessage()); log.error(e.getMessage());

View File

@@ -22,76 +22,183 @@ package io.kamax.mxisd.as;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import io.kamax.matrix.MatrixID; import io.kamax.matrix.MatrixID;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.matrix._MatrixID; import io.kamax.matrix._MatrixID;
import io.kamax.matrix._ThreePid; import io.kamax.matrix.client.MatrixClientContext;
import io.kamax.matrix.client.as.MatrixApplicationServiceClient;
import io.kamax.matrix.event.EventKey; import io.kamax.matrix.event.EventKey;
import io.kamax.matrix.json.GsonUtil; import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.backend.sql.synapse.Synapse; import io.kamax.mxisd.Mxisd;
import io.kamax.mxisd.config.MatrixConfig; import io.kamax.mxisd.as.processor.event.EventTypeProcessor;
import io.kamax.mxisd.as.processor.event.MembershipEventProcessor;
import io.kamax.mxisd.as.processor.event.MessageEventProcessor;
import io.kamax.mxisd.as.registration.SynapseRegistrationYaml;
import io.kamax.mxisd.config.AppServiceConfig;
import io.kamax.mxisd.config.MxisdConfig; import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.exception.ConfigurationException;
import io.kamax.mxisd.exception.HttpMatrixException; import io.kamax.mxisd.exception.HttpMatrixException;
import io.kamax.mxisd.exception.NotAllowedException; import io.kamax.mxisd.exception.NotAllowedException;
import io.kamax.mxisd.notification.NotificationManager;
import io.kamax.mxisd.profile.ProfileManager;
import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.IStorage;
import io.kamax.mxisd.storage.ormlite.dao.ASTransactionDao; import io.kamax.mxisd.storage.ormlite.dao.ASTransactionDao;
import io.kamax.mxisd.util.GsonParser; import io.kamax.mxisd.util.GsonParser;
import org.apache.commons.lang.StringUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.introspector.BeanAccess;
import org.yaml.snakeyaml.representer.Representer;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.Instant; import java.time.Instant;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
public class AppSvcManager { public class AppSvcManager {
private transient final Logger log = LoggerFactory.getLogger(AppSvcManager.class); private static final Logger log = LoggerFactory.getLogger(AppSvcManager.class);
private final GsonParser parser; private final AppServiceConfig cfg;
private final IStorage store;
private final GsonParser parser = new GsonParser();
private MatrixConfig cfg; private MatrixApplicationServiceClient client;
private IStorage store; private Map<String, EventTypeProcessor> processors = new HashMap<>();
private ProfileManager profiler; private Map<String, CompletableFuture<String>> transactionsInProgress = new ConcurrentHashMap<>();
private NotificationManager notif;
private Synapse synapse;
private Map<String, CompletableFuture<String>> transactionsInProgress; public AppSvcManager(Mxisd m) {
this.cfg = m.getConfig().getAppsvc();
this.store = m.getStore();
public AppSvcManager(MxisdConfig cfg, IStorage store, ProfileManager profiler, NotificationManager notif, Synapse synapse) { /*
this.cfg = cfg.getMatrix(); We process the configuration to make sure all is fine and setting default values if needed
this.store = store; */
this.profiler = profiler;
this.notif = notif;
this.synapse = synapse;
parser = new GsonParser(); // By default, the feature is enabled
transactionsInProgress = new ConcurrentHashMap<>(); cfg.setEnabled(ObjectUtils.defaultIfNull(cfg.isEnabled(), false));
if (!cfg.isEnabled()) {
return;
}
if (Objects.isNull(cfg.getEndpoint().getToAS().getUrl())) {
throw new ConfigurationException("App Service: Endpoint: To AS: URL");
}
if (Objects.isNull(cfg.getEndpoint().getToAS().getToken())) {
throw new ConfigurationException("App Service: Endpoint: To AS: Token", "Must be set, even if to an empty string");
}
if (Objects.isNull(cfg.getEndpoint().getToHS().getUrl())) {
throw new ConfigurationException("App Service: Endpoint: To HS: URL");
}
if (Objects.isNull(cfg.getEndpoint().getToHS().getToken())) {
throw new ConfigurationException("App Service: Endpoint: To HS: Token", "Must be set, even if to an empty string");
}
// We set a default status for each feature individually
cfg.getFeature().getAdmin().setEnabled(ObjectUtils.defaultIfNull(cfg.getFeature().getAdmin().getEnabled(), cfg.isEnabled()));
cfg.getFeature().setCleanExpiredInvite(ObjectUtils.defaultIfNull(cfg.getFeature().getCleanExpiredInvite(), cfg.isEnabled()));
cfg.getFeature().setInviteById(ObjectUtils.defaultIfNull(cfg.getFeature().getInviteById(), false));
if (cfg.getFeature().getAdmin().getEnabled()) {
if (StringUtils.isBlank(cfg.getUser().getMain())) {
throw new ConfigurationException("App Service admin feature is enabled, but no main user configured");
}
if (cfg.getUser().getMain().startsWith("@") || cfg.getUser().getMain().contains(":")) {
throw new ConfigurationException("App Service: Users: Main ID: Is not a localpart");
}
}
if (cfg.getFeature().getCleanExpiredInvite()) {
if (StringUtils.isBlank(cfg.getUser().getInviteExpired())) {
throw new ConfigurationException("App Service user for Expired Invite is not set");
}
if (cfg.getUser().getMain().startsWith("@") || cfg.getUser().getMain().contains(":")) {
throw new ConfigurationException("App Service: Users: Expired Invite ID: Is not a localpart");
}
}
MatrixClientContext mxContext = new MatrixClientContext();
mxContext.setDomain(m.getConfig().getMatrix().getDomain());
mxContext.setToken(cfg.getEndpoint().getToHS().getToken());
mxContext.setHsBaseUrl(cfg.getEndpoint().getToHS().getUrl());
client = new MatrixApplicationServiceClient(mxContext);
processors.put("m.room.member", new MembershipEventProcessor(client, m));
processors.put("m.room.message", new MessageEventProcessor(m, client));
processSynapseConfig(m.getConfig());
}
private void processSynapseConfig(MxisdConfig cfg) {
String synapseRegFile = cfg.getAppsvc().getRegistration().getSynapse().getFile();
if (StringUtils.isBlank(synapseRegFile)) {
log.info("No synapse registration file path given - skipping generation...");
return;
}
SynapseRegistrationYaml syncCfg = SynapseRegistrationYaml.parse(cfg.getAppsvc(), cfg.getMatrix().getDomain());
Representer rep = new Representer();
rep.getPropertyUtils().setBeanAccess(BeanAccess.FIELD);
Yaml yaml = new Yaml(rep);
// SnakeYAML set the type of object on the first line, which can fail to be parsed on synapse
// We therefore need to split the resulting string, remove the first line, and then write it
List<String> lines = new ArrayList<>(Arrays.asList(yaml.dump(syncCfg).split("\\R+")));
if (StringUtils.equals(lines.get(0), "!!" + SynapseRegistrationYaml.class.getCanonicalName())) {
lines.remove(0);
}
try (FileOutputStream os = new FileOutputStream(synapseRegFile)) {
IOUtils.writeLines(lines, System.lineSeparator(), os, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException("Unable to write synapse appservice registration file", e);
}
}
private void ensureEnabled() {
if (!cfg.isEnabled()) {
throw new HttpMatrixException(503, "M_NOT_AVAILABLE", "This feature is disabled");
}
} }
public AppSvcManager withToken(String token) { public AppSvcManager withToken(String token) {
ensureEnabled();
if (StringUtils.isBlank(token)) { if (StringUtils.isBlank(token)) {
throw new HttpMatrixException(401, "M_UNAUTHORIZED", "No HS token"); throw new HttpMatrixException(401, "M_UNAUTHORIZED", "No HS token");
} }
if (!StringUtils.equals(cfg.getListener().getToken().getHs(), token)) { if (!StringUtils.equals(cfg.getEndpoint().getToAS().getToken(), token)) {
throw new NotAllowedException("Invalid HS token"); throw new NotAllowedException("Invalid HS token");
} }
return this; return this;
} }
public void processUser(String userId) {
client.createUser(MatrixID.asAcceptable(userId).getLocalPart());
}
public CompletableFuture<String> processTransaction(String txnId, InputStream is) { public CompletableFuture<String> processTransaction(String txnId, InputStream is) {
ensureEnabled();
if (StringUtils.isEmpty(txnId)) { if (StringUtils.isEmpty(txnId)) {
throw new IllegalArgumentException("Transaction ID cannot be empty"); throw new IllegalArgumentException("Transaction ID cannot be empty");
} }
synchronized (this) { synchronized (this) {
Optional<ASTransactionDao> dao = store.getTransactionResult(cfg.getListener().getLocalpart(), txnId); Optional<ASTransactionDao> dao = store.getTransactionResult(cfg.getUser().getMain(), txnId);
if (dao.isPresent()) { if (dao.isPresent()) {
log.info("AS Transaction {} already processed - returning computed result", txnId); log.info("AS Transaction {} already processed - returning computed result", txnId);
return CompletableFuture.completedFuture(dao.get().getResult()); return CompletableFuture.completedFuture(dao.get().getResult());
@@ -122,7 +229,7 @@ public class AppSvcManager {
try { try {
log.info("Saving transaction details to store"); log.info("Saving transaction details to store");
store.insertTransactionResult(cfg.getListener().getLocalpart(), txnId, end, result); store.insertTransactionResult(cfg.getUser().getMain(), txnId, end, result);
} finally { } finally {
log.debug("Removing CompletedFuture from transaction map"); log.debug("Removing CompletedFuture from transaction map");
transactionsInProgress.remove(txnId); transactionsInProgress.remove(txnId);
@@ -139,7 +246,7 @@ public class AppSvcManager {
return future; return future;
} }
public void processTransaction(List<JsonObject> eventsJson) { private void processTransaction(List<JsonObject> eventsJson) {
log.info("Processing transaction events: start"); log.info("Processing transaction events: start");
eventsJson.forEach(ev -> { eventsJson.forEach(ev -> {
@@ -165,54 +272,14 @@ public class AppSvcManager {
_MatrixID sender = MatrixID.asAcceptable(senderId); _MatrixID sender = MatrixID.asAcceptable(senderId);
log.debug("Sender: {}", senderId); log.debug("Sender: {}", senderId);
if (!StringUtils.equals("m.room.member", GsonUtil.getStringOrNull(ev, "type"))) { String evType = StringUtils.defaultIfBlank(EventKey.Type.getStringOrNull(ev), "<EMPTY/MISSING>");
log.debug("This is not a room membership event, skipping"); EventTypeProcessor p = processors.get(evType);
if (Objects.isNull(p)) {
log.debug("No event processor for type {}, skipping", evType);
return; return;
} }
if (!StringUtils.equals("invite", GsonUtil.getStringOrNull(ev, "membership"))) { p.process(ev, sender, roomId);
log.debug("This is not an invite event, skipping");
return;
}
String inviteeId = EventKey.StateKey.getStringOrNull(ev);
if (StringUtils.isBlank(inviteeId)) {
log.warn("Invalid event: No invitee ID, skipping");
return;
}
_MatrixID invitee = MatrixID.asAcceptable(inviteeId);
if (!StringUtils.equals(invitee.getDomain(), cfg.getDomain())) {
log.debug("Ignoring invite for {}: not a local user");
return;
}
log.info("Got invite from {} to {}", senderId, inviteeId);
boolean wasSent = false;
List<_ThreePid> tpids = profiler.getThreepids(invitee).stream()
.filter(tpid -> ThreePidMedium.Email.is(tpid.getMedium()))
.collect(Collectors.toList());
log.info("Found {} email(s) in identity store for {}", tpids.size(), inviteeId);
for (_ThreePid tpid : tpids) {
log.info("Found Email to notify about room invitation: {}", tpid.getAddress());
Map<String, String> properties = new HashMap<>();
profiler.getDisplayName(sender).ifPresent(name -> properties.put("sender_display_name", name));
try {
synapse.getRoomName(roomId).ifPresent(name -> properties.put("room_name", name));
} catch (RuntimeException e) {
log.warn("Could not fetch room name", e);
log.info("Unable to fetch room name: Did you integrate your Homeserver as documented?");
}
IMatrixIdInvite inv = new MatrixIdInvite(roomId, sender, invitee, tpid.getMedium(), tpid.getAddress(), properties);
notif.sendForInvite(inv);
log.info("Notification for invite of {} sent to {}", inviteeId, tpid.getAddress());
wasSent = true;
}
log.info("Was notification sent? {}", wasSent);
log.debug("Event {}: processing end", evId); log.debug("Event {}: processing end", evId);
}); });

View File

@@ -0,0 +1,32 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sarl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.as.processor.command;
import io.kamax.matrix.client._MatrixClient;
import io.kamax.matrix.hs._MatrixRoom;
import io.kamax.mxisd.Mxisd;
import org.apache.commons.cli.CommandLine;
public interface CommandProcessor {
void process(Mxisd m, _MatrixClient client, _MatrixRoom room, CommandLine cmdLine);
}

View File

@@ -0,0 +1,117 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sarl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.as.processor.command;
import io.kamax.matrix.client._MatrixClient;
import io.kamax.matrix.hs._MatrixRoom;
import io.kamax.mxisd.Mxisd;
import io.kamax.mxisd.invitation.IThreePidInviteReply;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.text.StrBuilder;
import java.util.List;
public class InviteCommandProcessor implements CommandProcessor {
public static final String Command = "invite";
@Override
public void process(Mxisd m, _MatrixClient client, _MatrixRoom room, CommandLine cmdLine) {
if (cmdLine.getArgs().length < 2) {
room.sendNotice(buildHelp());
} else {
String arg = cmdLine.getArgList().get(1);
String response;
if (StringUtils.equals("list", arg)) {
StrBuilder b = new StrBuilder();
List<IThreePidInviteReply> invites = m.getInvite().listInvites();
if (invites.isEmpty()) {
b.appendln("No invites!");
response = b.toString();
} else {
b.appendln("Invites:");
for (IThreePidInviteReply invite : invites) {
b.appendNewLine().append("ID: ").append(invite.getId());
b.appendNewLine().append("Room: ").append(invite.getInvite().getRoomId());
b.appendNewLine().append("Medium: ").append(invite.getInvite().getMedium());
b.appendNewLine().append("Address: ").append(invite.getInvite().getAddress());
b.appendNewLine();
}
response = b.appendNewLine().append("Total: " + invites.size()).toString();
}
} else if (StringUtils.equals("show", arg)) {
if (cmdLine.getArgList().size() < 3) {
response = buildHelp();
} else {
String id = cmdLine.getArgList().get(2);
IThreePidInviteReply invite = m.getInvite().getInvite(id);
StrBuilder b = new StrBuilder();
b.appendln("Details for Invitation #" + id);
b.appendNewLine().append("Room: ").append(invite.getInvite().getRoomId());
b.appendNewLine().append("Sender: ").append(invite.getInvite().getSender().toString());
b.appendNewLine().append("Medium: ").append(invite.getInvite().getMedium());
b.appendNewLine().append("Address: ").append(invite.getInvite().getAddress());
b.appendNewLine().append("Display name: ").append(invite.getDisplayName());
b.appendNewLine().appendNewLine().append("Properties:");
invite.getInvite().getProperties().forEach((k, v) -> {
b.appendNewLine().append("\t").append(k).append("=").append(v);
});
b.appendNewLine();
response = b.toString();
}
} else if (StringUtils.equals("revoke", arg)) {
if (cmdLine.getArgList().size() < 3) {
response = buildHelp();
} else {
m.getInvite().expireInvite(cmdLine.getArgList().get(2));
response = "OK";
}
} else {
response = buildError("Unknown invite action: " + arg, true);
}
room.sendNotice(response);
}
}
private String buildError(String message, boolean showHelp) {
if (showHelp) {
message = message + "\n\n" + buildHelp();
}
return message;
}
private String buildHelp() {
return "Available actions:\n\n" +
"list - List invites\n" +
"show ID - Show detailed info about a specific invite\n" +
"revoke ID - Revoke a pending invite by resolving it to the configured Expiration user\n";
}
}

View File

@@ -0,0 +1,78 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sarl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.as.processor.command;
import io.kamax.matrix.client._MatrixClient;
import io.kamax.matrix.hs._MatrixRoom;
import io.kamax.mxisd.Mxisd;
import io.kamax.mxisd.lookup.SingleLookupReply;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.lang.text.StrBuilder;
import org.apache.commons.lang3.StringUtils;
import java.util.Optional;
public class LookupCommandProcessor implements CommandProcessor {
public static final String Command = "lookup";
@Override
public void process(Mxisd m, _MatrixClient client, _MatrixRoom room, CommandLine cmdLine) {
if (cmdLine.getArgList().size() != 3) {
room.sendNotice(getUsage());
return;
}
String medium = cmdLine.getArgList().get(1);
String address = cmdLine.getArgList().get(2);
if (StringUtils.isAnyBlank(medium, address)) {
room.sendNotice(getUsage());
return;
}
room.sendNotice("Processing...");
Optional<SingleLookupReply> r = m.getIdentity().find(medium, address, true);
if (!r.isPresent()) {
room.sendNotice("No result");
return;
}
SingleLookupReply lookup = r.get();
StrBuilder b = new StrBuilder();
b.append("Result for 3PID lookup of ").append(medium).append(" ").appendln(address).appendNewLine();
b.append("Matrix ID: ").appendln(lookup.getMxid().getId());
b.appendln("Validity:")
.append(" Not Before: ").appendln(lookup.getNotBefore())
.append(" Not After: ").appendln(lookup.getNotAfter());
b.appendln("Signatures:");
lookup.getSignatures().forEach((host, signs) -> {
b.append(" ").append(host).appendln(":");
signs.forEach((key, sign) -> b.append(" ").append(key).append(" -> ").appendln("OK"));
});
room.sendNotice(b.toString());
}
public String getUsage() {
return "lookup MEDIUM ADDRESS";
}
}

View File

@@ -0,0 +1,37 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sarl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.as.processor.command;
import io.kamax.matrix.client._MatrixClient;
import io.kamax.matrix.hs._MatrixRoom;
import io.kamax.mxisd.Mxisd;
import org.apache.commons.cli.CommandLine;
public class PingCommandProcessor implements CommandProcessor {
public static final String Command = "ping";
@Override
public void process(Mxisd m, _MatrixClient client, _MatrixRoom room, CommandLine cmdLine) {
room.sendNotice("Pong!");
}
}

View File

@@ -0,0 +1,30 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sarl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.as.processor.event;
import com.google.gson.JsonObject;
import io.kamax.matrix._MatrixID;
public interface EventTypeProcessor {
void process(JsonObject ev, _MatrixID sender, String roomId);
}

View File

@@ -0,0 +1,172 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sarl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.as.processor.event;
import com.google.gson.JsonObject;
import io.kamax.matrix.MatrixID;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.matrix._MatrixID;
import io.kamax.matrix._ThreePid;
import io.kamax.matrix.client.as.MatrixApplicationServiceClient;
import io.kamax.matrix.event.EventKey;
import io.kamax.matrix.hs._MatrixRoom;
import io.kamax.mxisd.Mxisd;
import io.kamax.mxisd.backend.sql.synapse.Synapse;
import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.invitation.IMatrixIdInvite;
import io.kamax.mxisd.invitation.MatrixIdInvite;
import io.kamax.mxisd.notification.NotificationManager;
import io.kamax.mxisd.profile.ProfileManager;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class MembershipEventProcessor implements EventTypeProcessor {
private final static Logger log = LoggerFactory.getLogger(MembershipEventProcessor.class);
private MatrixApplicationServiceClient client;
private final MxisdConfig cfg;
private ProfileManager profiler;
private NotificationManager notif;
private Synapse synapse;
public MembershipEventProcessor(
MatrixApplicationServiceClient client,
Mxisd m
) {
this.client = client;
this.cfg = m.getConfig();
this.profiler = m.getProfile();
this.notif = m.getNotif();
this.synapse = m.getSynapse();
}
@Override
public void process(JsonObject ev, _MatrixID sender, String roomId) {
JsonObject content = EventKey.Content.findObj(ev).orElseGet(() -> {
log.debug("No content found, falling back to full object");
return ev;
});
String targetId = EventKey.StateKey.getStringOrNull(ev);
if (StringUtils.isBlank(targetId)) {
log.warn("Invalid event: No invitee ID, skipping");
return;
}
_MatrixID target = MatrixID.asAcceptable(targetId);
if (!StringUtils.equals(target.getDomain(), cfg.getMatrix().getDomain())) {
log.debug("Ignoring invite for {}: not a local user");
return;
}
log.info("Got membership event from {} to {} for room {}", sender.getId(), targetId, roomId);
boolean isForMainUser = StringUtils.equals(target.getLocalPart(), cfg.getAppsvc().getUser().getMain());
boolean isForExpInvUser = StringUtils.equals(target.getLocalPart(), cfg.getAppsvc().getUser().getInviteExpired());
boolean isUs = isForMainUser || isForExpInvUser;
if (StringUtils.equals("join", EventKey.Membership.getStringOrNull(content))) {
if (!isForMainUser) {
log.warn("We joined the room {} for another identity as the main user, which is not supported. Leaving...", roomId);
client.getUser(target.getLocalPart()).getRoom(roomId).tryLeave().ifPresent(err -> {
log.warn("Could not decline invite to room {}: {} - {}", roomId, err.getErrcode(), err.getError());
});
}
} else if (StringUtils.equals("invite", EventKey.Membership.getStringOrNull(content))) {
if (isForMainUser) {
processForMainUser(roomId, sender);
} else if (isForExpInvUser) {
processForExpiredInviteUser(roomId, target);
} else {
processForUserIdInvite(roomId, sender, target);
}
} else if (StringUtils.equals("leave", EventKey.Membership.getStringOrNull(content))) {
_MatrixRoom room = client.getRoom(roomId);
if (!isUs && room.getJoinedUsers().size() == 1) {
// TODO we need to find out if this is only us remaining and leave the room if so, using the right client for it
}
} else {
log.debug("This is not an supported type of membership event, skipping");
}
}
private void processForMainUser(String roomId, _MatrixID sender) {
boolean isAllowed = profiler.hasAnyRole(sender, cfg.getAppsvc().getFeature().getAdmin().getAllowedRoles());
if (!isAllowed) {
log.info("Sender does not have any of the required roles, denying");
client.getRoom(roomId).tryLeave().ifPresent(err -> {
log.warn("Could not decline invite to room {}: {} - {}", roomId, err.getErrcode(), err.getError());
});
} else {
client.getRoom(roomId).tryJoin().ifPresent(err -> {
log.warn("Could not join room {}: {} - {}", roomId, err.getErrcode(), err.getError());
client.getRoom(roomId).tryLeave().ifPresent(err1 -> {
log.warn("Could not decline invite to room {} after failed join: {} - {}", roomId, err1.getErrcode(), err1.getError());
});
});
}
}
private void processForExpiredInviteUser(String roomId, _MatrixID invitee) {
client.getUser(invitee.getLocalPart()).getRoom(roomId).tryLeave().ifPresent(err -> {
log.warn("Could not decline invite to room {}: {} - {}", roomId, err.getErrcode(), err.getError());
});
}
private void processForUserIdInvite(String roomId, _MatrixID sender, _MatrixID invitee) {
String inviteeId = invitee.getId();
boolean wasSent = false;
List<_ThreePid> tpids = profiler.getThreepids(invitee).stream()
.filter(tpid -> ThreePidMedium.Email.is(tpid.getMedium()))
.collect(Collectors.toList());
log.info("Found {} email(s) in identity store for {}", tpids.size(), inviteeId);
for (_ThreePid tpid : tpids) {
log.info("Found Email to notify about room invitation: {}", tpid.getAddress());
Map<String, String> properties = new HashMap<>();
profiler.getDisplayName(sender).ifPresent(name -> properties.put("sender_display_name", name));
try {
synapse.getRoomName(roomId).ifPresent(name -> properties.put("room_name", name));
} catch (RuntimeException e) {
log.warn("Could not fetch room name", e);
log.info("Unable to fetch room name: Did you integrate your Homeserver as documented?");
}
IMatrixIdInvite inv = new MatrixIdInvite(roomId, sender, invitee, tpid.getMedium(), tpid.getAddress(), properties);
notif.sendForInvite(inv);
log.info("Notification for invite of {} sent to {}", inviteeId, tpid.getAddress());
wasSent = true;
}
log.info("Was notification sent? {}", wasSent);
}
}

View File

@@ -0,0 +1,127 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sarl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.as.processor.event;
import com.google.gson.JsonObject;
import io.kamax.matrix._MatrixID;
import io.kamax.matrix._MatrixUserProfile;
import io.kamax.matrix.client.as.MatrixApplicationServiceClient;
import io.kamax.matrix.hs._MatrixRoom;
import io.kamax.matrix.json.event.MatrixJsonRoomMessageEvent;
import io.kamax.mxisd.Mxisd;
import io.kamax.mxisd.as.processor.command.CommandProcessor;
import io.kamax.mxisd.as.processor.command.InviteCommandProcessor;
import io.kamax.mxisd.as.processor.command.LookupCommandProcessor;
import io.kamax.mxisd.as.processor.command.PingCommandProcessor;
import org.apache.commons.cli.*;
import org.apache.commons.lang.text.StrBuilder;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
public class MessageEventProcessor implements EventTypeProcessor {
private static final Logger log = LoggerFactory.getLogger(MessageEventProcessor.class);
private final Mxisd m;
private final MatrixApplicationServiceClient client;
private Map<String, CommandProcessor> processors;
public MessageEventProcessor(Mxisd m, MatrixApplicationServiceClient client) {
this.m = m;
this.client = client;
processors = new HashMap<>();
processors.put("?", (m1, client1, room, cmdLine) -> room.sendNotice(getHelp()));
processors.put("help", (m1, client1, room, cmdLine) -> room.sendNotice(getHelp()));
processors.put(PingCommandProcessor.Command, new PingCommandProcessor());
processors.put(InviteCommandProcessor.Command, new InviteCommandProcessor());
processors.put(LookupCommandProcessor.Command, new LookupCommandProcessor());
}
@Override
public void process(JsonObject ev, _MatrixID sender, String roomId) {
MatrixJsonRoomMessageEvent msgEv = new MatrixJsonRoomMessageEvent(ev);
if (StringUtils.equals("m.notice", msgEv.getBodyType())) {
log.info("Ignoring automated message");
return;
}
_MatrixRoom room = client.getRoom(roomId);
if (!m.getProfile().hasAnyRole(sender, m.getConfig().getAppsvc().getFeature().getAdmin().getAllowedRoles())) {
room.sendNotice("You are not allowed to interact with me.");
return;
}
List<_MatrixID> joinedUsers = room.getJoinedUsers().stream().map(_MatrixUserProfile::getId).collect(Collectors.toList());
boolean joinedWithMainUser = joinedUsers.contains(client.getWhoAmI());
boolean isAdminPrivate = joinedWithMainUser && joinedUsers.size() == 2;
if (!StringUtils.equals("m.text", msgEv.getBodyType())) {
log.info("Unsupported message event type: {}", msgEv.getBodyType());
return;
}
String command = msgEv.getBody();
if (!isAdminPrivate) {
if (!StringUtils.startsWith(command, "!" + Mxisd.Name + " ")) {
// Not for us
return;
}
command = command.substring(("!" + Mxisd.Name + " ").length());
}
try {
CommandLineParser p = new DefaultParser();
CommandLine cmdLine = p.parse(new Options(), command.split(" ", 0));
String cmd = cmdLine.getArgList().get(0);
CommandProcessor cp = processors.get(cmd);
if (Objects.isNull(cp)) {
room.sendNotice("Unknown command: " + command + "\n\n" + getHelp());
} else {
cp.process(m, client, room, cmdLine);
}
} catch (ParseException e) {
room.sendNotice("Invalid input" + "\n\n" + getHelp());
} catch (RuntimeException e) {
room.sendNotice("Error when running command: " + e.getMessage());
}
}
public String getHelp() {
StrBuilder builder = new StrBuilder();
builder.appendln("Available commands:");
for (String cmd : processors.keySet()) {
builder.append("\t").appendln(cmd);
}
return builder.toString();
}
}

View File

@@ -0,0 +1,176 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sarl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.as.registration;
import io.kamax.mxisd.config.AppServiceConfig;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class SynapseRegistrationYaml {
public static SynapseRegistrationYaml parse(AppServiceConfig cfg, String domain) {
SynapseRegistrationYaml yaml = new SynapseRegistrationYaml();
yaml.setId(cfg.getRegistration().getSynapse().getId());
yaml.setUrl(cfg.getEndpoint().getToAS().getUrl());
yaml.setAsToken(cfg.getEndpoint().getToHS().getToken());
yaml.setHsToken(cfg.getEndpoint().getToAS().getToken());
yaml.setSenderLocalpart(cfg.getUser().getMain());
if (cfg.getFeature().getCleanExpiredInvite()) {
Namespace ns = new Namespace();
ns.setExclusive(true);
ns.setRegex("@" + cfg.getUser().getInviteExpired() + ":" + domain);
yaml.getNamespaces().getUsers().add(ns);
}
if (cfg.getFeature().getInviteById()) {
Namespace ns = new Namespace();
ns.setExclusive(false);
ns.setRegex("@*:" + domain);
yaml.getNamespaces().getUsers().add(ns);
}
return yaml;
}
public static class Namespace {
private String regex;
private boolean exclusive;
public String getRegex() {
return regex;
}
public void setRegex(String regex) {
this.regex = regex;
}
public boolean isExclusive() {
return exclusive;
}
public void setExclusive(boolean exclusive) {
this.exclusive = exclusive;
}
}
public static class Namespaces {
private List<Namespace> users = new ArrayList<>();
private List<Namespace> aliases = new ArrayList<>();
private List<Namespace> rooms = new ArrayList<>();
public List<Namespace> getUsers() {
return users;
}
public void setUsers(List<Namespace> users) {
this.users = users;
}
public List<Namespace> getAliases() {
return aliases;
}
public void setAliases(List<Namespace> aliases) {
this.aliases = aliases;
}
public List<Namespace> getRooms() {
return rooms;
}
public void setRooms(List<Namespace> rooms) {
this.rooms = rooms;
}
}
private String id;
private String url;
private String as_token;
private String hs_token;
private String sender_localpart;
private Namespaces namespaces = new Namespaces();
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public void setUrl(URL url) {
if (Objects.isNull(url)) {
this.url = null;
} else {
this.url = url.toString();
}
}
public String getAsToken() {
return as_token;
}
public void setAsToken(String as_token) {
this.as_token = as_token;
}
public String getHsToken() {
return hs_token;
}
public void setHsToken(String hs_token) {
this.hs_token = hs_token;
}
public String getSenderLocalpart() {
return sender_localpart;
}
public void setSenderLocalpart(String sender_localpart) {
this.sender_localpart = sender_localpart;
}
public Namespaces getNamespaces() {
return namespaces;
}
public void setNamespaces(Namespaces namespaces) {
this.namespaces = namespaces;
}
}

View File

@@ -72,7 +72,11 @@ public abstract class LdapBackend {
} }
protected synchronized LdapConnection getConn() { protected synchronized LdapConnection getConn() {
return new LdapNetworkConnection(cfg.getConnection().getHost(), cfg.getConnection().getPort(), cfg.getConnection().isTls()); return getConn(cfg.getConnection().getHost());
}
protected synchronized LdapConnection getConn(String host) {
return new LdapNetworkConnection(host, cfg.getConnection().getPort(), cfg.getConnection().isTls());
} }
protected void bind(LdapConnection conn) throws LdapException { protected void bind(LdapConnection conn) throws LdapException {

View File

@@ -28,6 +28,7 @@ import io.kamax.mxisd.lookup.SingleLookupRequest;
import io.kamax.mxisd.lookup.ThreePidMapping; import io.kamax.mxisd.lookup.ThreePidMapping;
import io.kamax.mxisd.lookup.provider.IThreePidProvider; import io.kamax.mxisd.lookup.provider.IThreePidProvider;
import io.kamax.mxisd.util.GsonUtil; import io.kamax.mxisd.util.GsonUtil;
import org.apache.commons.lang.StringUtils;
import org.apache.directory.api.ldap.model.cursor.CursorException; import org.apache.directory.api.ldap.model.cursor.CursorException;
import org.apache.directory.api.ldap.model.cursor.CursorLdapReferralException; import org.apache.directory.api.ldap.model.cursor.CursorLdapReferralException;
import org.apache.directory.api.ldap.model.cursor.EntryCursor; import org.apache.directory.api.ldap.model.cursor.EntryCursor;
@@ -37,8 +38,10 @@ import org.apache.directory.api.ldap.model.message.SearchScope;
import org.apache.directory.ldap.client.api.LdapConnection; import org.apache.directory.ldap.client.api.LdapConnection;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.xbill.DNS.*;
import java.io.IOException; import java.io.IOException;
import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -91,7 +94,10 @@ public class LdapThreePidProvider extends LdapBackend implements IThreePidProvid
return Optional.of(buildMatrixIdFromUid(data.get())); return Optional.of(buildMatrixIdFromUid(data.get()));
} }
} catch (CursorLdapReferralException e) { } catch (CursorLdapReferralException e) {
log.warn("3PID {} is only available via referral, skipping", value); log.info("Got referral info: {}", e.getReferralInfo());
return followReferral(medium, value, e.getReferralInfo());
//log.warn("3PID {} is only available via referral, skipping", value);
} catch (IOException | LdapException | CursorException e) { } catch (IOException | LdapException | CursorException e) {
throw new InternalServerError(e); throw new InternalServerError(e);
} }
@@ -104,12 +110,50 @@ public class LdapThreePidProvider extends LdapBackend implements IThreePidProvid
public Optional<SingleLookupReply> find(SingleLookupRequest request) { public Optional<SingleLookupReply> find(SingleLookupRequest request) {
log.info("Performing LDAP lookup {} of type {}", request.getThreePid(), request.getType()); log.info("Performing LDAP lookup {} of type {}", request.getThreePid(), request.getType());
try (LdapConnection conn = getConn()) { List<String> hosts = new ArrayList<>();
bind(conn);
return lookup(conn, request.getType(), request.getThreePid()).map(id -> new SingleLookupReply(request, id)); String domain = getCfg().getConnection().getDomain();
} catch (LdapException | IOException e) { if (StringUtils.isNotBlank(domain)) {
throw new InternalServerError(e); try {
Record[] records = new Lookup("_ldap._tcp.DomainDnsZones." + domain, Type.SRV).run();
if (records == null || records.length == 0) {
log.warn("No LDAP server found for domain {}", domain);
return Optional.empty();
}
for (Record record : records) {
if (record instanceof SRVRecord) {
SRVRecord srvRec = (SRVRecord) record;
hosts.add(srvRec.getTarget().toString(true));
}
}
if (hosts.isEmpty()) {
return Optional.empty();
}
} catch (TextParseException e) {
throw new RuntimeException(e);
}
} else {
hosts.add(getCfg().getConnection().getHost());
} }
for (String host : hosts) {
log.info("Trying host {}", host);
try (LdapConnection conn = getConn(host)) {
bind(conn);
Optional<SingleLookupReply> reply = lookup(conn, request.getType(), request.getThreePid()).map(id -> new SingleLookupReply(request, id));
if (reply.isPresent()) return reply;
} catch (LdapException | IOException e) {
if (hosts.size() == 1) {
throw new InternalServerError(e);
} else {
log.warn("Unable to query {}: {}", host, e.getMessage());
}
}
}
return Optional.empty();
} }
@Override @Override
@@ -137,4 +181,51 @@ public class LdapThreePidProvider extends LdapBackend implements IThreePidProvid
return mappingsFound; return mappingsFound;
} }
private Optional<String> followReferral(String medium, String value, String ref) {
URI uri = URI.create(ref);
Optional<String> tPidQueryOpt = getCfg().getIdentity().getQuery(medium);
if (!tPidQueryOpt.isPresent()) {
log.warn("{} is not a configured 3PID type for LDAP lookup", medium);
return Optional.empty();
}
LdapConnection conn = getConn(uri.getHost());
try {
bind(conn);
} catch (LdapException e) {
throw new RuntimeException(e);
}
// we merge 3PID specific query with global/specific filter, if one exists.
String tPidQuery = tPidQueryOpt.get().replaceAll(getCfg().getIdentity().getToken(), value);
String searchQuery = buildWithFilter(tPidQuery, getCfg().getIdentity().getFilter());
log.debug("Query: {}", searchQuery);
log.debug("Attributes: {}", GsonUtil.build().toJson(getUidAtt()));
try (EntryCursor cursor = conn.search(uri.getPath().substring(1), searchQuery, SearchScope.SUBTREE, getUidAtt())) {
while (cursor.next()) {
Entry entry = cursor.get();
log.info("Found possible match, DN: {}", entry.getDn().getName());
Optional<String> data = getAttribute(entry, getUidAtt());
if (!data.isPresent()) {
continue;
}
log.info("DN {} is a valid match", entry.getDn().getName());
return Optional.of(buildMatrixIdFromUid(data.get()));
}
return Optional.empty();
} catch (CursorLdapReferralException e) {
log.info("Got referral info: {}", e.getReferralInfo());
return followReferral(medium, value, e.getReferralInfo());
//log.warn("3PID {} is only available via referral, skipping", value);
} catch (IOException | LdapException | CursorException e) {
throw new InternalServerError(e);
}
}
} }

View File

@@ -23,7 +23,9 @@ package io.kamax.mxisd.backend.sql;
import io.kamax.matrix.ThreePid; import io.kamax.matrix.ThreePid;
import io.kamax.matrix._MatrixID; import io.kamax.matrix._MatrixID;
import io.kamax.matrix._ThreePid; import io.kamax.matrix._ThreePid;
import io.kamax.mxisd.UserIdType;
import io.kamax.mxisd.config.sql.SqlConfig; import io.kamax.mxisd.config.sql.SqlConfig;
import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.profile.ProfileProvider; import io.kamax.mxisd.profile.ProfileProvider;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -33,16 +35,14 @@ import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
public abstract class SqlProfileProvider implements ProfileProvider { public abstract class SqlProfileProvider implements ProfileProvider {
private transient final Logger log = LoggerFactory.getLogger(SqlProfileProvider.class); private static final Logger log = LoggerFactory.getLogger(SqlProfileProvider.class);
private SqlConfig.Profile cfg; private SqlConfig.Profile cfg;
private SqlConnectionPool pool; private SqlConnectionPool pool;
public SqlProfileProvider(SqlConfig cfg) { public SqlProfileProvider(SqlConfig cfg) {
@@ -50,6 +50,12 @@ public abstract class SqlProfileProvider implements ProfileProvider {
this.pool = new SqlConnectionPool(cfg); this.pool = new SqlConnectionPool(cfg);
} }
private void setParameters(PreparedStatement stmt, String value) throws SQLException {
for (int i = 1; i <= stmt.getParameterMetaData().getParameterCount(); i++) {
stmt.setString(i, value);
}
}
@Override @Override
public Optional<String> getDisplayName(_MatrixID user) { public Optional<String> getDisplayName(_MatrixID user) {
String stmtSql = cfg.getDisplayName().getQuery(); String stmtSql = cfg.getDisplayName().getQuery();
@@ -94,7 +100,33 @@ public abstract class SqlProfileProvider implements ProfileProvider {
@Override @Override
public List<String> getRoles(_MatrixID user) { public List<String> getRoles(_MatrixID user) {
return Collections.emptyList(); log.info("Querying roles for {}", user.getId());
List<String> roles = new ArrayList<>();
String stmtSql = cfg.getRole().getQuery();
try (Connection conn = pool.get()) {
PreparedStatement stmt = conn.prepareStatement(stmtSql);
if (UserIdType.Localpart.is(cfg.getRole().getType())) {
setParameters(stmt, user.getLocalPart());
} else if (UserIdType.MatrixID.is(cfg.getRole().getType())) {
setParameters(stmt, user.getId());
} else {
throw new InternalServerError("Unsupported user type in SQL Role fetching: " + cfg.getRole().getType());
}
ResultSet rSet = stmt.executeQuery();
while (rSet.next()) {
String role = rSet.getString(1);
roles.add(role);
log.debug("Found role {}", role);
}
log.info("Got {} roles", roles.size());
return roles;
} catch (SQLException e) {
throw new RuntimeException(e);
}
} }
} }

View File

@@ -32,7 +32,7 @@ public class GenericSqlStoreSupplier implements IdentityStoreSupplier {
@Override @Override
public void accept(Mxisd mxisd) { public void accept(Mxisd mxisd) {
if (mxisd.getConfig().getSql().getAuth().isEnabled()) { if (mxisd.getConfig().getSql().getAuth().isEnabled()) {
AuthProviders.register(() -> new GenericSqlAuthProvider(mxisd.getConfig().getSql(), mxisd.getInvitationManager())); AuthProviders.register(() -> new GenericSqlAuthProvider(mxisd.getConfig().getSql(), mxisd.getInvite()));
} }
if (mxisd.getConfig().getSql().getDirectory().isEnabled()) { if (mxisd.getConfig().getSql().getDirectory().isEnabled()) {

View File

@@ -43,6 +43,10 @@ public class SynapseQueries {
return "SELECT medium, address FROM user_threepids WHERE user_id = ?"; return "SELECT medium, address FROM user_threepids WHERE user_id = ?";
} }
public static String getRoles() {
return "SELECT DISTINCT(group_id) FROM group_users WHERE user_id = ?";
}
public static String findByDisplayName(String type, String domain) { public static String findByDisplayName(String type, String domain) {
if (StringUtils.equals("sqlite", type)) { if (StringUtils.equals("sqlite", type)) {
return "select " + getUserId(type, domain) + ", displayname from profiles p where displayname like ?"; return "select " + getUserId(type, domain) + ", displayname from profiles p where displayname like ?";

View File

@@ -26,9 +26,13 @@ import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.directory.DirectoryProviders; import io.kamax.mxisd.directory.DirectoryProviders;
import io.kamax.mxisd.lookup.ThreePidProviders; import io.kamax.mxisd.lookup.ThreePidProviders;
import io.kamax.mxisd.profile.ProfileProviders; import io.kamax.mxisd.profile.ProfileProviders;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SynapseSqlStoreSupplier implements IdentityStoreSupplier { public class SynapseSqlStoreSupplier implements IdentityStoreSupplier {
private static final Logger log = LoggerFactory.getLogger(SynapseSqlStoreSupplier.class);
@Override @Override
public void accept(Mxisd mxisd) { public void accept(Mxisd mxisd) {
accept(mxisd.getConfig()); accept(mxisd.getConfig());
@@ -44,6 +48,7 @@ public class SynapseSqlStoreSupplier implements IdentityStoreSupplier {
} }
if (cfg.getSynapseSql().getProfile().isEnabled()) { if (cfg.getSynapseSql().getProfile().isEnabled()) {
log.debug("Profile is enabled, registering provider");
ProfileProviders.register(() -> new SynapseSqlProfileProvider(cfg.getSynapseSql())); ProfileProviders.register(() -> new SynapseSqlProfileProvider(cfg.getSynapseSql()));
} }
} }

View File

@@ -0,0 +1,287 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2018 Kamax Sarl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config;
import io.kamax.mxisd.Mxisd;
import io.kamax.mxisd.exception.ConfigurationException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class AppServiceConfig {
public static class Users {
private String main = "mxisd";
private String inviteExpired = "_mxisd_invite-expired";
public String getMain() {
return main;
}
public void setMain(String main) {
this.main = main;
}
public String getInviteExpired() {
return inviteExpired;
}
public void setInviteExpired(String inviteExpired) {
this.inviteExpired = inviteExpired;
}
public void build() {
// no-op
}
}
public static class Endpoint {
private String url;
private String token;
private transient URL cUrl;
public URL getUrl() {
return cUrl;
}
public void setUrl(String url) {
this.url = url;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public void build() {
if (Objects.isNull(url)) {
return;
}
try {
cUrl = new URL(url);
} catch (MalformedURLException e) {
throw new ConfigurationException("AppService endpoint(s) URL definition");
}
}
}
public static class Endpoints {
private Endpoint toAS = new Endpoint();
private Endpoint toHS = new Endpoint();
public Endpoint getToAS() {
return toAS;
}
public void setToAS(Endpoint toAS) {
this.toAS = toAS;
}
public Endpoint getToHS() {
return toHS;
}
public void setToHS(Endpoint toHS) {
this.toHS = toHS;
}
public void build() {
toAS.build();
toHS.build();
}
}
public static class Synapse {
private String id = "appservice-" + Mxisd.Name;
private String file;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getFile() {
return file;
}
public void setFile(String file) {
this.file = file;
}
public void build() {
// no-op
}
}
public static class Registration {
private Synapse synapse = new Synapse();
public Synapse getSynapse() {
return synapse;
}
public void setSynapse(Synapse synapse) {
this.synapse = synapse;
}
public void build() {
synapse.build();
}
}
public static class AdminFeature {
private Boolean enabled;
private List<String> allowedRoles = new ArrayList<>();
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public List<String> getAllowedRoles() {
return allowedRoles;
}
public void setAllowedRoles(List<String> allowedRoles) {
this.allowedRoles = allowedRoles;
}
public void build() {
// no-op
}
}
public static class Features {
private AdminFeature admin = new AdminFeature();
private Boolean inviteById;
private Boolean cleanExpiredInvite;
public AdminFeature getAdmin() {
return admin;
}
public void setAdmin(AdminFeature admin) {
this.admin = admin;
}
public Boolean getInviteById() {
return inviteById;
}
public void setInviteById(Boolean inviteById) {
this.inviteById = inviteById;
}
public Boolean getCleanExpiredInvite() {
return cleanExpiredInvite;
}
public void setCleanExpiredInvite(Boolean cleanExpiredInvite) {
this.cleanExpiredInvite = cleanExpiredInvite;
}
public void build() {
admin.build();
}
}
private Boolean enabled;
private Features feature = new Features();
private Endpoints endpoint = new Endpoints();
private Registration registration = new Registration();
private Users user = new Users();
public Boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public Features getFeature() {
return feature;
}
public void setFeature(Features feature) {
this.feature = feature;
}
public Endpoints getEndpoint() {
return endpoint;
}
public void setEndpoint(Endpoints endpoint) {
this.endpoint = endpoint;
}
public Registration getRegistration() {
return registration;
}
public void setRegistration(Registration registration) {
this.registration = registration;
}
public Users getUser() {
return user;
}
public void setUser(Users user) {
this.user = user;
}
public void build() {
endpoint.build();
feature.build();
registration.build();
user.build();
}
}

View File

@@ -20,18 +20,53 @@
package io.kamax.mxisd.config; package io.kamax.mxisd.config;
import io.kamax.mxisd.util.GsonUtil; import io.kamax.matrix.json.GsonUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
public class InvitationConfig { public class InvitationConfig {
private transient final Logger log = LoggerFactory.getLogger(InvitationConfig.class); private static final Logger log = LoggerFactory.getLogger(InvitationConfig.class);
public static class Expiration {
private Boolean enabled;
private long after;
private String resolveTo;
public Boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public long getAfter() {
return after;
}
public void setAfter(long after) {
this.after = after;
}
public String getResolveTo() {
return resolveTo;
}
public void setResolveTo(String resolveTo) {
this.resolveTo = resolveTo;
}
}
public static class Resolution { public static class Resolution {
private boolean recursive = true; private boolean recursive = true;
private long timer = 1; private long timer = 5;
public boolean isRecursive() { public boolean isRecursive() {
return recursive; return recursive;
@@ -51,7 +86,43 @@ public class InvitationConfig {
} }
public static class SenderPolicy {
private List<String> hasRole = new ArrayList<>();
public List<String> getHasRole() {
return hasRole;
}
public void setHasRole(List<String> hasRole) {
this.hasRole = hasRole;
}
}
public static class Policies {
private SenderPolicy ifSender = new SenderPolicy();
public SenderPolicy getIfSender() {
return ifSender;
}
public void setIfSender(SenderPolicy ifSender) {
this.ifSender = ifSender;
}
}
private Expiration expiration = new Expiration();
private Resolution resolution = new Resolution(); private Resolution resolution = new Resolution();
private Policies policy = new Policies();
public Expiration getExpiration() {
return expiration;
}
public void setExpiration(Expiration expiration) {
this.expiration = expiration;
}
public Resolution getResolution() { public Resolution getResolution() {
return resolution; return resolution;
@@ -61,9 +132,19 @@ public class InvitationConfig {
this.resolution = resolution; this.resolution = resolution;
} }
public Policies getPolicy() {
return policy;
}
public void setPolicy(Policies policy) {
this.policy = policy;
}
public void build() { public void build() {
log.info("--- Invite config ---"); log.info("--- Invite config ---");
log.info("Resolution: {}", GsonUtil.build().toJson(resolution)); log.info("Expiration: {}", GsonUtil.get().toJson(getExpiration()));
log.info("Resolution: {}", GsonUtil.get().toJson(getResolution()));
log.info("Policies: {}", GsonUtil.get().toJson(getPolicy()));
} }
} }

View File

@@ -1,107 +0,0 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2018 Kamax Sarl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config;
import io.kamax.mxisd.exception.ConfigurationException;
import org.apache.commons.lang.StringUtils;
import java.net.MalformedURLException;
import java.net.URL;
public class ListenerConfig {
public static class Token {
private String as;
private String hs;
public String getAs() {
return as;
}
public void setAs(String as) {
this.as = as;
}
public String getHs() {
return hs;
}
public void setHs(String hs) {
this.hs = hs;
}
}
private transient URL csUrl;
private String url;
private String localpart;
private Token token = new Token();
public URL getUrl() {
return csUrl;
}
public void setUrl(String url) {
this.url = url;
}
public String getLocalpart() {
return localpart;
}
public void setLocalpart(String localpart) {
this.localpart = localpart;
}
public Token getToken() {
return token;
}
public void setToken(Token token) {
this.token = token;
}
public void build() {
try {
if (StringUtils.isBlank(url)) {
return;
}
csUrl = new URL(url);
if (StringUtils.isBlank(getLocalpart())) {
throw new IllegalArgumentException("localpart for matrix listener is not set");
}
if (StringUtils.isBlank(getToken().getAs())) {
throw new IllegalArgumentException("AS token is not set");
}
if (StringUtils.isBlank(getToken().getHs())) {
throw new IllegalArgumentException("HS token is not set");
}
} catch (MalformedURLException e) {
throw new ConfigurationException(e);
}
}
}

View File

@@ -63,7 +63,6 @@ public class MatrixConfig {
private String domain; private String domain;
private Identity identity = new Identity(); private Identity identity = new Identity();
private ListenerConfig listener = new ListenerConfig();
public String getDomain() { public String getDomain() {
return domain; return domain;
@@ -81,14 +80,6 @@ public class MatrixConfig {
this.identity = identity; this.identity = identity;
} }
public ListenerConfig getListener() {
return listener;
}
public void setListener(ListenerConfig listener) {
this.listener = listener;
}
public void build() { public void build() {
log.info("--- Matrix config ---"); log.info("--- Matrix config ---");
@@ -99,8 +90,6 @@ public class MatrixConfig {
log.info("Domain: {}", getDomain()); log.info("Domain: {}", getDomain());
log.info("Identity:"); log.info("Identity:");
log.info("\tServers: {}", GsonUtil.get().toJson(identity.getServers())); log.info("\tServers: {}", GsonUtil.get().toJson(identity.getServers()));
listener.build();
} }
} }

View File

@@ -83,6 +83,7 @@ public class MxisdConfig {
} }
private AppServiceConfig appsvc = new AppServiceConfig();
private AuthenticationConfig auth = new AuthenticationConfig(); private AuthenticationConfig auth = new AuthenticationConfig();
private DirectoryConfig directory = new DirectoryConfig(); private DirectoryConfig directory = new DirectoryConfig();
private Dns dns = new Dns(); private Dns dns = new Dns();
@@ -97,6 +98,7 @@ public class MxisdConfig {
private MemoryStoreConfig memory = new MemoryStoreConfig(); private MemoryStoreConfig memory = new MemoryStoreConfig();
private NotificationConfig notification = new NotificationConfig(); private NotificationConfig notification = new NotificationConfig();
private NetIqLdapConfig netiq = new NetIqLdapConfig(); private NetIqLdapConfig netiq = new NetIqLdapConfig();
private RegisterConfig register = new RegisterConfig();
private ServerConfig server = new ServerConfig(); private ServerConfig server = new ServerConfig();
private SessionConfig session = new SessionConfig(); private SessionConfig session = new SessionConfig();
private StorageConfig storage = new StorageConfig(); private StorageConfig storage = new StorageConfig();
@@ -107,6 +109,14 @@ public class MxisdConfig {
private ViewConfig view = new ViewConfig(); private ViewConfig view = new ViewConfig();
private WordpressConfig wordpress = new WordpressConfig(); private WordpressConfig wordpress = new WordpressConfig();
public AppServiceConfig getAppsvc() {
return appsvc;
}
public void setAppsvc(AppServiceConfig appsvc) {
this.appsvc = appsvc;
}
public AuthenticationConfig getAuth() { public AuthenticationConfig getAuth() {
return auth; return auth;
} }
@@ -219,6 +229,14 @@ public class MxisdConfig {
this.netiq = netiq; this.netiq = netiq;
} }
public RegisterConfig getRegister() {
return register;
}
public void setRegister(RegisterConfig register) {
this.register = register;
}
public ServerConfig getServer() { public ServerConfig getServer() {
return server; return server;
} }
@@ -297,6 +315,7 @@ public class MxisdConfig {
log.debug("server.name is empty, using matrix.domain"); log.debug("server.name is empty, using matrix.domain");
} }
getAppsvc().build();
getAuth().build(); getAuth().build();
getDirectory().build(); getDirectory().build();
getExec().build(); getExec().build();
@@ -310,6 +329,7 @@ public class MxisdConfig {
getMemory().build(); getMemory().build();
getNetiq().build(); getNetiq().build();
getNotification().build(); getNotification().build();
getRegister().build();
getRest().build(); getRest().build();
getSession().build(); getSession().build();
getServer().build(); getServer().build();

View File

@@ -0,0 +1,201 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sarl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.matrix.json.GsonUtil;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.stream.Collectors;
public class RegisterConfig {
private static final Logger log = LoggerFactory.getLogger(RegisterConfig.class);
public static class ThreepidPolicyPattern {
private List<String> blacklist = new ArrayList<>();
private List<String> whitelist = new ArrayList<>();
public List<String> getBlacklist() {
return blacklist;
}
public void setBlacklist(List<String> blacklist) {
this.blacklist = blacklist;
}
public List<String> getWhitelist() {
return whitelist;
}
public void setWhitelist(List<String> whitelist) {
this.whitelist = whitelist;
}
}
public static class EmailPolicy extends ThreepidPolicy {
private ThreepidPolicyPattern domain = new ThreepidPolicyPattern();
public ThreepidPolicyPattern getDomain() {
return domain;
}
public void setDomain(ThreepidPolicyPattern domain) {
this.domain = domain;
}
private List<String> buildPatterns(List<String> domains) {
log.info("Building email policy");
return domains.stream().map(d -> {
if (StringUtils.startsWith(d, "*")) {
log.info("Found domain and subdomain policy");
d = "(.*)" + d.substring(1);
} else if (StringUtils.startsWith(d, ".")) {
log.info("Found subdomain-only policy");
d = "(.*)" + d;
} else {
log.info("Found domain-only policy");
}
return "([^@]+)@" + d.replace(".", "\\.");
}).collect(Collectors.toList());
}
@Override
public void build() {
if (Objects.isNull(getDomain())) {
return;
}
if (Objects.nonNull(getDomain().getBlacklist())) {
if (Objects.isNull(getPattern().getBlacklist())) {
getPattern().setBlacklist(new ArrayList<>());
}
List<String> domains = buildPatterns(getDomain().getBlacklist());
getPattern().getBlacklist().addAll(domains);
}
if (Objects.nonNull(getDomain().getWhitelist())) {
if (Objects.isNull(getPattern().getWhitelist())) {
getPattern().setWhitelist(new ArrayList<>());
}
List<String> domains = buildPatterns(getDomain().getWhitelist());
getPattern().getWhitelist().addAll(domains);
}
setDomain(null);
}
}
public static class ThreepidPolicy {
private ThreepidPolicyPattern pattern = new ThreepidPolicyPattern();
public ThreepidPolicyPattern getPattern() {
return pattern;
}
public void setPattern(ThreepidPolicyPattern pattern) {
this.pattern = pattern;
}
public void build() {
// no-op
}
}
public static class Policy {
private boolean allowed;
private boolean invite = true;
private Map<String, Object> threepid = new HashMap<>();
public boolean isAllowed() {
return allowed;
}
public void setAllowed(boolean allowed) {
this.allowed = allowed;
}
public boolean forInvite() {
return invite;
}
public void setInvite(boolean invite) {
this.invite = invite;
}
public Map<String, Object> getThreepid() {
return threepid;
}
public void setThreepid(Map<String, Object> threepid) {
this.threepid = threepid;
}
}
private Policy policy = new Policy();
public Policy getPolicy() {
return policy;
}
public void setPolicy(Policy policy) {
this.policy = policy;
}
public void build() {
log.info("--- Registration config ---");
log.info("Before Build");
log.info(GsonUtil.getPrettyForLog(this));
new HashMap<>(getPolicy().getThreepid()).forEach((medium, policy) -> {
if (ThreePidMedium.Email.is(medium)) {
EmailPolicy pPolicy = GsonUtil.get().fromJson(GsonUtil.get().toJson(policy), EmailPolicy.class);
pPolicy.build();
policy = GsonUtil.makeObj(pPolicy);
} else {
ThreepidPolicy pPolicy = GsonUtil.get().fromJson(GsonUtil.get().toJson(policy), ThreepidPolicy.class);
pPolicy.build();
policy = GsonUtil.makeObj(pPolicy);
}
getPolicy().getThreepid().put(medium, policy);
});
log.info("After Build");
log.info(GsonUtil.getPrettyForLog(this));
}
}

View File

@@ -125,6 +125,7 @@ public abstract class LdapConfig {
private boolean tls = false; private boolean tls = false;
private String host; private String host;
private String domain;
private int port = 389; private int port = 389;
private String bindDn; private String bindDn;
private String bindPassword; private String bindPassword;
@@ -147,6 +148,14 @@ public abstract class LdapConfig {
this.host = host; this.host = host;
} }
public String getDomain() {
return domain;
}
public void setDomain(String domain) {
this.domain = domain;
}
public int getPort() { public int getPort() {
return port; return port;
} }

View File

@@ -193,11 +193,35 @@ public abstract class SqlConfig {
} }
public static class ProfileRoles {
private String type;
private String query;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getQuery() {
return query;
}
public void setQuery(String query) {
this.query = query;
}
}
public static class Profile { public static class Profile {
private Boolean enabled; private Boolean enabled;
private ProfileDisplayName displayName = new ProfileDisplayName(); private ProfileDisplayName displayName = new ProfileDisplayName();
private ProfileThreepids threepid = new ProfileThreepids(); private ProfileThreepids threepid = new ProfileThreepids();
private ProfileRoles role = new ProfileRoles();
public Boolean isEnabled() { public Boolean isEnabled() {
return enabled; return enabled;
@@ -223,6 +247,14 @@ public abstract class SqlConfig {
this.threepid = threepid; this.threepid = threepid;
} }
public ProfileRoles getRole() {
return role;
}
public void setRole(ProfileRoles role) {
this.role = role;
}
} }
private boolean enabled; private boolean enabled;
@@ -323,10 +355,11 @@ public abstract class SqlConfig {
log.info("3PID mapping query: {}", getIdentity().getQuery()); log.info("3PID mapping query: {}", getIdentity().getQuery());
log.info("Identity medium queries: {}", GsonUtil.build().toJson(getIdentity().getMedium())); log.info("Identity medium queries: {}", GsonUtil.build().toJson(getIdentity().getMedium()));
log.info("Profile:"); log.info("Profile:");
log.info("\tEnabled: {}", getProfile().isEnabled()); log.info(" Enabled: {}", getProfile().isEnabled());
if (getProfile().isEnabled()) { if (getProfile().isEnabled()) {
log.info("\tDisplay name query: {}", getProfile().getDisplayName().getQuery()); log.info(" Display name query: {}", getProfile().getDisplayName().getQuery());
log.info("\tProfile 3PID query: {}", getProfile().getThreepid().getQuery()); log.info(" Profile 3PID query: {}", getProfile().getThreepid().getQuery());
log.info(" Role query: {}", getProfile().getRole().getQuery());
} }
} }
} }

View File

@@ -20,6 +20,7 @@
package io.kamax.mxisd.config.sql.synapse; package io.kamax.mxisd.config.sql.synapse;
import io.kamax.mxisd.UserIdType;
import io.kamax.mxisd.backend.sql.synapse.SynapseQueries; import io.kamax.mxisd.backend.sql.synapse.SynapseQueries;
import io.kamax.mxisd.config.sql.SqlConfig; import io.kamax.mxisd.config.sql.SqlConfig;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
@@ -48,9 +49,17 @@ public class SynapseSqlProviderConfig extends SqlConfig {
if (StringUtils.isBlank(getProfile().getDisplayName().getQuery())) { if (StringUtils.isBlank(getProfile().getDisplayName().getQuery())) {
getProfile().getDisplayName().setQuery(SynapseQueries.getDisplayName()); getProfile().getDisplayName().setQuery(SynapseQueries.getDisplayName());
} }
if (StringUtils.isBlank(getProfile().getThreepid().getQuery())) { if (StringUtils.isBlank(getProfile().getThreepid().getQuery())) {
getProfile().getThreepid().setQuery(SynapseQueries.getThreepids()); getProfile().getThreepid().setQuery(SynapseQueries.getThreepids());
} }
if (StringUtils.isBlank(getProfile().getRole().getType())) {
getProfile().getRole().setType(UserIdType.MatrixID.getId());
}
if (StringUtils.isBlank(getProfile().getRole().getQuery())) {
getProfile().getRole().setQuery(SynapseQueries.getRoles());
}
} }
printConfig(); printConfig();

View File

@@ -20,9 +20,12 @@
package io.kamax.mxisd.crypto; package io.kamax.mxisd.crypto;
import io.kamax.matrix.crypto.*;
import io.kamax.mxisd.config.KeyConfig; import io.kamax.mxisd.config.KeyConfig;
import io.kamax.mxisd.config.ServerConfig; import io.kamax.mxisd.crypto.ed25519.Ed25519KeyManager;
import io.kamax.mxisd.crypto.ed25519.Ed25519SignatureManager;
import io.kamax.mxisd.storage.crypto.FileKeyStore;
import io.kamax.mxisd.storage.crypto.KeyStore;
import io.kamax.mxisd.storage.crypto.MemoryKeyStore;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -31,10 +34,10 @@ import java.io.IOException;
public class CryptoFactory { public class CryptoFactory {
public static KeyManager getKeyManager(KeyConfig keyCfg) { public static Ed25519KeyManager getKeyManager(KeyConfig keyCfg) {
_KeyStore store; KeyStore store;
if (StringUtils.equals(":memory:", keyCfg.getPath())) { if (StringUtils.equals(":memory:", keyCfg.getPath())) {
store = new KeyMemoryStore(); store = new MemoryKeyStore();
} else { } else {
File keyStore = new File(keyCfg.getPath()); File keyStore = new File(keyCfg.getPath());
if (!keyStore.exists()) { if (!keyStore.exists()) {
@@ -45,14 +48,14 @@ public class CryptoFactory {
} }
} }
store = new KeyFileStore(keyCfg.getPath()); store = new FileKeyStore(keyCfg.getPath());
} }
return new KeyManager(store); return new Ed25519KeyManager(store);
} }
public static SignatureManager getSignatureManager(KeyManager keyMgr, ServerConfig cfg) { public static SignatureManager getSignatureManager(Ed25519KeyManager keyMgr) {
return new SignatureManager(keyMgr, cfg.getName()); return new Ed25519SignatureManager(keyMgr);
} }
} }

View File

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

View File

@@ -0,0 +1,76 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sàrl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.crypto;
import org.apache.commons.lang3.StringUtils;
import java.util.Objects;
public class GenericKeyIdentifier implements KeyIdentifier {
private final KeyType type;
private final String algo;
private final String serial;
public GenericKeyIdentifier(KeyIdentifier id) {
this(id.getType(), id.getAlgorithm(), id.getSerial());
}
public GenericKeyIdentifier(KeyType type, String algo, String serial) {
if (StringUtils.isAnyBlank(algo, serial)) {
throw new IllegalArgumentException("Aglorith and/or Serial cannot be blank");
}
this.type = Objects.requireNonNull(type);
this.algo = algo;
this.serial = serial;
}
@Override
public KeyType getType() {
return type;
}
@Override
public String getAlgorithm() {
return algo;
}
@Override
public String getSerial() {
return serial;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof GenericKeyIdentifier)) return false;
GenericKeyIdentifier that = (GenericKeyIdentifier) o;
return type == that.type &&
algo.equals(that.algo) &&
serial.equals(that.serial);
}
@Override
public int hashCode() {
return Objects.hash(type, algo, serial);
}
}

View File

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

View File

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

View File

@@ -0,0 +1,54 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sàrl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.crypto;
/**
* Identifying data for a given Key.
*/
public interface KeyIdentifier {
/**
* Type of key.
*
* @return The type of the key
*/
KeyType getType();
/**
* Algorithm of the key. Typically <code>ed25519</code>.
*
* @return The algorithm of the key
*/
String getAlgorithm();
/**
* Serial of the key, unique for the algorithm.
* It is typically made of random alphanumerical characters.
*
* @return The serial of the key
*/
String getSerial();
default String getId() {
return getAlgorithm().toLowerCase() + ":" + getSerial();
}
}

View File

@@ -0,0 +1,41 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sàrl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.crypto;
import java.util.List;
public interface KeyManager {
KeyIdentifier generateKey(KeyType type);
List<KeyIdentifier> getKeys(KeyType type);
Key getServerSigningKey();
Key getKey(KeyIdentifier id);
void disableKey(KeyIdentifier id);
String getPublicKeyBase64(KeyIdentifier id);
boolean isValid(KeyType type, String publicKeyBase64);
}

View File

@@ -0,0 +1,39 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sàrl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.crypto;
/**
* Types of keys used by an Identity server.
* See https://matrix.org/docs/spec/identity_service/r0.1.0.html#key-management
*/
public enum KeyType {
/**
* Ephemeral keys are related to 3PID invites and are only valid while the invite is pending.
*/
Ephemeral,
/**
* Regular keys are used by the Identity Server itself to sign requests/responses
*/
Regular
}

View File

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

View File

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

View File

@@ -0,0 +1,64 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sàrl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.crypto;
import com.google.gson.JsonObject;
import java.nio.charset.StandardCharsets;
public interface SignatureManager {
/**
* Sign the message and produce a <code>signatures</code> object that can directly be added to the object being signed.
*
* @param domain The domain under which the signature should be added
* @param message The message to sign
* @return The <code>signatures</code> object
*/
JsonObject signMessageGson(String domain, String message);
/**
* Sign the canonical form of a JSON object.
*
* @param obj The JSON object to canonicalize and sign
* @return The signature
*/
Signature sign(JsonObject obj);
/**
* Sign the message, using UTF-8 as decoding character set.
*
* @param message The UTF-8 encoded message
* @return The signature
*/
default Signature sign(String message) {
return sign(message.getBytes(StandardCharsets.UTF_8));
}
/**
* Sign the data.
*
* @param data The data to sign
* @return The signature
*/
Signature sign(byte[] data);
}

View File

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

View File

@@ -0,0 +1,148 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sàrl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.crypto.ed25519;
import io.kamax.matrix.codec.MxBase64;
import io.kamax.mxisd.crypto.*;
import io.kamax.mxisd.storage.crypto.KeyStore;
import net.i2p.crypto.eddsa.EdDSAPrivateKey;
import net.i2p.crypto.eddsa.EdDSAPublicKey;
import net.i2p.crypto.eddsa.KeyPairGenerator;
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
import net.i2p.crypto.eddsa.spec.EdDSAParameterSpec;
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec;
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.security.KeyPair;
import java.time.Instant;
import java.util.List;
import java.util.stream.Collectors;
public class Ed25519KeyManager implements KeyManager {
private static final Logger log = LoggerFactory.getLogger(Ed25519KeyManager.class);
private final EdDSAParameterSpec keySpecs;
private final KeyStore store;
public Ed25519KeyManager(KeyStore store) {
this.keySpecs = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.CURVE_ED25519_SHA512);
this.store = store;
if (!store.getCurrentKey().isPresent()) {
List<KeyIdentifier> keys = store.list(KeyType.Regular).stream()
.map(this::getKey)
.filter(Key::isValid)
.map(Key::getId)
.collect(Collectors.toList());
if (keys.isEmpty()) {
keys.add(generateKey(KeyType.Regular));
}
store.setCurrentKey(keys.get(0));
}
}
private String generateId() {
ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
buffer.putLong(Instant.now().toEpochMilli() - 1546297200000L); // TS since 2019-01-01T00:00:00Z to keep IDs short
return Base64.encodeBase64URLSafeString(buffer.array()) + RandomStringUtils.randomAlphanumeric(1);
}
private String getPrivateKeyBase64(EdDSAPrivateKey key) {
return MxBase64.encode(key.getSeed());
}
EdDSAParameterSpec getKeySpecs() {
return keySpecs;
}
@Override
public KeyIdentifier generateKey(KeyType type) {
KeyIdentifier id;
do {
id = new GenericKeyIdentifier(type, KeyAlgorithm.Ed25519, generateId());
} while (store.has(id));
KeyPair pair = (new KeyPairGenerator()).generateKeyPair();
String keyEncoded = getPrivateKeyBase64((EdDSAPrivateKey) pair.getPrivate());
Key key = new GenericKey(id, true, keyEncoded);
store.add(key);
return id;
}
@Override
public List<KeyIdentifier> getKeys(KeyType type) {
return store.list(type);
}
@Override
public Key getServerSigningKey() {
return store.get(store.getCurrentKey().orElseThrow(IllegalStateException::new));
}
@Override
public Key getKey(KeyIdentifier id) {
return store.get(id);
}
private EdDSAPrivateKeySpec getPrivateKeySpecs(KeyIdentifier id) {
return new EdDSAPrivateKeySpec(Base64.decodeBase64(getKey(id).getPrivateKeyBase64()), keySpecs);
}
EdDSAPrivateKey getPrivateKey(KeyIdentifier id) {
return new EdDSAPrivateKey(getPrivateKeySpecs(id));
}
private EdDSAPublicKey getPublicKey(KeyIdentifier id) {
EdDSAPrivateKeySpec privKeySpec = getPrivateKeySpecs(id);
EdDSAPublicKeySpec pubKeySpec = new EdDSAPublicKeySpec(privKeySpec.getA(), keySpecs);
return new EdDSAPublicKey(pubKeySpec);
}
@Override
public void disableKey(KeyIdentifier id) {
Key key = store.get(id);
key = new GenericKey(id, false, key.getPrivateKeyBase64());
store.update(key);
}
@Override
public String getPublicKeyBase64(KeyIdentifier id) {
return MxBase64.encode(getPublicKey(id).getAbyte());
}
@Override
public boolean isValid(KeyType type, String publicKeyBase64) {
// TODO caching?
return getKeys(type).stream().anyMatch(id -> StringUtils.equals(getPublicKeyBase64(id), publicKeyBase64));
}
}

View File

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

View File

@@ -0,0 +1,86 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sàrl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.crypto.ed25519;
import com.google.gson.JsonObject;
import io.kamax.matrix.codec.MxBase64;
import io.kamax.matrix.json.MatrixJson;
import io.kamax.mxisd.crypto.KeyIdentifier;
import io.kamax.mxisd.crypto.Signature;
import io.kamax.mxisd.crypto.SignatureManager;
import net.i2p.crypto.eddsa.EdDSAEngine;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
public class Ed25519SignatureManager implements SignatureManager {
private final Ed25519KeyManager keyMgr;
public Ed25519SignatureManager(Ed25519KeyManager keyMgr) {
this.keyMgr = keyMgr;
}
@Override
public JsonObject signMessageGson(String domain, String message) {
Signature sign = sign(message);
JsonObject keySignature = new JsonObject();
keySignature.addProperty(sign.getKey().getAlgorithm() + ":" + sign.getKey().getSerial(), sign.getSignature());
JsonObject signature = new JsonObject();
signature.add(domain, keySignature);
return signature;
}
@Override
public Signature sign(JsonObject obj) {
return sign(MatrixJson.encodeCanonical(obj));
}
@Override
public Signature sign(byte[] data) {
try {
KeyIdentifier signingKeyId = keyMgr.getServerSigningKey().getId();
EdDSAEngine signEngine = new EdDSAEngine(MessageDigest.getInstance(keyMgr.getKeySpecs().getHashAlgorithm()));
signEngine.initSign(keyMgr.getPrivateKey(signingKeyId));
byte[] signRaw = signEngine.signOneShot(data);
String sign = MxBase64.encode(signRaw);
return new Signature() {
@Override
public KeyIdentifier getKey() {
return signingKeyId;
}
@Override
public String getSignature() {
return sign;
}
};
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -22,8 +22,12 @@ package io.kamax.mxisd.exception;
public class ObjectNotFoundException extends RuntimeException { public class ObjectNotFoundException extends RuntimeException {
public ObjectNotFoundException(String message) {
super(message);
}
public ObjectNotFoundException(String type, String id) { public ObjectNotFoundException(String type, String id) {
super(type + " with ID " + id + " does not exist"); this(type + " with ID " + id + " does not exist");
} }
} }

View File

@@ -22,6 +22,9 @@ package io.kamax.mxisd.http.io.identity;
import io.kamax.mxisd.lookup.SingleLookupReply; import io.kamax.mxisd.lookup.SingleLookupReply;
import java.util.HashMap;
import java.util.Map;
public class SingeLookupReplyJson { public class SingeLookupReplyJson {
private String address; private String address;
@@ -30,6 +33,7 @@ public class SingeLookupReplyJson {
private long not_after; private long not_after;
private long not_before; private long not_before;
private long ts; private long ts;
private Map<String, Map<String, String>> signatures = new HashMap<>();
public SingeLookupReplyJson(SingleLookupReply reply) { public SingeLookupReplyJson(SingleLookupReply reply) {
this.address = reply.getRequest().getThreePid(); this.address = reply.getRequest().getThreePid();
@@ -64,4 +68,8 @@ public class SingeLookupReplyJson {
return ts; return ts;
} }
public Map<String, Map<String, String>> getSignatures() {
return signatures;
}
} }

View File

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

View File

@@ -0,0 +1,50 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sarl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.http.undertow.handler;
import io.undertow.server.HttpServerExchange;
import java.util.concurrent.ThreadLocalRandom;
public class InternalInfoHandler extends BasicHttpHandler {
/*
* This endpoint should never be called as being entierly custom as per instructions of New Vector,
* the author of that endpoint.
*
* Used for the first time at https://github.com/matrix-org/synapse/pull/4681/files#diff-a73c645c44a17da6ab70f256da6b60afR41
*
* Full context: https://matrix.to/#/!YkZelGRiqijtzXZODa:matrix.org/$15510967621328WMKVu:kamax.io?via=matrix.org
* Room name: #matrix-spec
* Room alias: #matrix-spec:matrix.org
*/
public static final String Path = "/_matrix/identity/api/{version}/internal-info";
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
// We will return a random status code in all possible error codes
int type = ThreadLocalRandom.current().nextInt(4, 6) * 100; // Random 4 or 5, times 100
int status = type + ThreadLocalRandom.current().nextInt(0, 100); // Random 0 to 99
respond(exchange, status, "M_FORBIDDEN", "This endpoint is under quarantine and possibly wrongfully labeled stable.");
}
}

View File

@@ -0,0 +1,46 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sarl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.http.undertow.handler.as.v1;
import io.kamax.mxisd.as.AppSvcManager;
import io.undertow.server.HttpServerExchange;
import java.util.LinkedList;
public class AsUserHandler extends ApplicationServiceHandler {
public static final String ID = "userId";
public static final String Path = "/_matrix/app/v1/users/{" + ID + "}";
private final AppSvcManager app;
public AsUserHandler(AppSvcManager app) {
this.app = app;
}
@Override
public void handleRequest(HttpServerExchange exchange) {
String userId = exchange.getQueryParameters().getOrDefault(ID, new LinkedList<>()).peekFirst();
app.withToken(getToken(exchange)).processUser(userId);
respondJson(exchange, "{}");
}
}

View File

@@ -20,6 +20,8 @@
package io.kamax.mxisd.http.undertow.handler.identity.v1; package io.kamax.mxisd.http.undertow.handler.identity.v1;
import io.kamax.mxisd.crypto.KeyManager;
import io.kamax.mxisd.crypto.KeyType;
import io.kamax.mxisd.http.IsAPIv1; import io.kamax.mxisd.http.IsAPIv1;
import io.undertow.server.HttpServerExchange; import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -31,11 +33,19 @@ public class EphemeralKeyIsValidHandler extends KeyIsValidHandler {
private transient final Logger log = LoggerFactory.getLogger(EphemeralKeyIsValidHandler.class); private transient final Logger log = LoggerFactory.getLogger(EphemeralKeyIsValidHandler.class);
private KeyManager mgr;
public EphemeralKeyIsValidHandler(KeyManager mgr) {
this.mgr = mgr;
}
@Override @Override
public void handleRequest(HttpServerExchange exchange) { public void handleRequest(HttpServerExchange exchange) {
log.warn("Ephemeral key was requested but no ephemeral key are generated, replying not valid"); // FIXME process + correctly in query parameter handling
String pubKey = getQueryParameter(exchange, "public_key").replace(" ", "+");
log.info("Validating ephemeral public key {}", pubKey);
respondJson(exchange, invalidKey); respondJson(exchange, mgr.isValid(KeyType.Ephemeral, pubKey) ? validKey : invalidKey);
} }
} }

View File

@@ -21,18 +21,20 @@
package io.kamax.mxisd.http.undertow.handler.identity.v1; package io.kamax.mxisd.http.undertow.handler.identity.v1;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import io.kamax.matrix.crypto.KeyManager; import io.kamax.mxisd.crypto.GenericKeyIdentifier;
import io.kamax.mxisd.exception.BadRequestException; import io.kamax.mxisd.crypto.KeyManager;
import io.kamax.mxisd.crypto.KeyType;
import io.kamax.mxisd.http.IsAPIv1; import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler; import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.undertow.server.HttpServerExchange; import io.undertow.server.HttpServerExchange;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
public class KeyGetHandler extends BasicHttpHandler { public class KeyGetHandler extends BasicHttpHandler {
public static final String Key = "key"; public static final String Key = "key";
public static final String Path = IsAPIv1.Base + "/pubkey/{key}"; public static final String Path = IsAPIv1.Base + "/pubkey/{" + Key + "}";
private transient final Logger log = LoggerFactory.getLogger(KeyGetHandler.class); private transient final Logger log = LoggerFactory.getLogger(KeyGetHandler.class);
@@ -45,17 +47,17 @@ public class KeyGetHandler extends BasicHttpHandler {
@Override @Override
public void handleRequest(HttpServerExchange exchange) { public void handleRequest(HttpServerExchange exchange) {
String key = getQueryParameter(exchange, Key); String key = getQueryParameter(exchange, Key);
String[] v = key.split(":", 2); if (StringUtils.isBlank(key)) {
String keyType = v[0]; throw new IllegalArgumentException("Key ID cannot be empty or blank");
int keyId = Integer.parseInt(v[1]);
if (!"ed25519".contentEquals(keyType)) {
throw new BadRequestException("Invalid algorithm: " + keyType);
} }
log.info("Key {}:{} was requested", keyType, keyId); String[] v = key.split(":", 2); // Maybe use regex?
String keyAlgo = v[0];
String keyId = v[1];
log.info("Key {}:{} was requested", keyAlgo, keyId);
JsonObject obj = new JsonObject(); JsonObject obj = new JsonObject();
obj.addProperty("public_key", mgr.getPublicKeyBase64(keyId)); obj.addProperty("public_key", mgr.getPublicKeyBase64(new GenericKeyIdentifier(KeyType.Regular, keyAlgo, keyId)));
respond(exchange, obj); respond(exchange, obj);
} }

View File

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

View File

@@ -0,0 +1,75 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sarl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.http.undertow.handler.identity.v1;
import com.google.gson.JsonObject;
import io.kamax.matrix.MatrixID;
import io.kamax.matrix._MatrixID;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.matrix.json.MatrixJson;
import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.crypto.SignatureManager;
import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.invitation.InvitationManager;
import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SignEd25519Handler extends BasicHttpHandler {
public static final String Path = IsAPIv1.Base + "/sign-ed25519";
private static final Logger log = LoggerFactory.getLogger(SignEd25519Handler.class);
private final MxisdConfig cfg;
private final InvitationManager invMgr;
private final SignatureManager signMgr;
public SignEd25519Handler(MxisdConfig cfg, InvitationManager invMgr, SignatureManager signMgr) {
this.cfg = cfg;
this.invMgr = invMgr;
this.signMgr = signMgr;
}
@Override
public void handleRequest(HttpServerExchange exchange) {
JsonObject body = parseJsonObject(exchange);
_MatrixID mxid = MatrixID.asAcceptable(GsonUtil.getStringOrThrow(body, "mxid"));
String token = GsonUtil.getStringOrThrow(body, "token");
String privKey = GsonUtil.getStringOrThrow(body, "private_key");
IThreePidInviteReply reply = invMgr.getInvite(token, privKey);
_MatrixID sender = reply.getInvite().getSender();
JsonObject res = new JsonObject();
res.addProperty("token", token);
res.addProperty("sender", sender.getId());
res.addProperty("mxid", mxid.getId());
res.add("signatures", signMgr.signMessageGson(cfg.getServer().getName(), MatrixJson.encodeCanonical(res)));
log.info("Signed data for invite using token {}", token);
respondJson(exchange, res);
}
}

View File

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

View File

@@ -24,9 +24,9 @@ import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import io.kamax.matrix.MatrixID; import io.kamax.matrix.MatrixID;
import io.kamax.matrix._MatrixID; import io.kamax.matrix._MatrixID;
import io.kamax.matrix.crypto.KeyManager;
import io.kamax.matrix.json.GsonUtil; import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.config.ServerConfig; import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.crypto.KeyManager;
import io.kamax.mxisd.exception.BadRequestException; import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.http.IsAPIv1; import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.http.io.identity.StoreInviteRequest; import io.kamax.mxisd.http.io.identity.StoreInviteRequest;
@@ -96,7 +96,8 @@ public class StoreInviteHandler extends BasicHttpHandler {
IThreePidInvite invite = new ThreePidInvite(sender, inv.getMedium(), inv.getAddress(), inv.getRoomId(), parameters); IThreePidInvite invite = new ThreePidInvite(sender, inv.getMedium(), inv.getAddress(), inv.getRoomId(), parameters);
IThreePidInviteReply reply = invMgr.storeInvite(invite); IThreePidInviteReply reply = invMgr.storeInvite(invite);
respondJson(exchange, new ThreePidInviteReplyIO(reply, keyMgr.getPublicKeyBase64(keyMgr.getCurrentIndex()), cfg.getPublicUrl())); // FIXME the key info must be set by the invitation manager in the reply object!
respondJson(exchange, new ThreePidInviteReplyIO(reply, keyMgr.getPublicKeyBase64(keyMgr.getServerSigningKey().getId()), cfg.getPublicUrl()));
} }
} }

View File

@@ -0,0 +1,103 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sarl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.http.undertow.handler.invite.v1;
import com.google.gson.JsonObject;
import io.kamax.matrix.MatrixID;
import io.kamax.matrix._MatrixID;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.dns.ClientDnsOverwrite;
import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.exception.NotAllowedException;
import io.kamax.mxisd.exception.RemoteHomeServerException;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.invitation.InvitationManager;
import io.undertow.server.HttpServerExchange;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URI;
import java.util.Optional;
public class RoomInviteHandler extends BasicHttpHandler {
public static final String Path = "/_matrix/client/r0/rooms/{roomId}/invite";
private static final Logger log = LoggerFactory.getLogger(RoomInviteHandler.class);
private final CloseableHttpClient client;
private final ClientDnsOverwrite dns;
private final InvitationManager invMgr;
public RoomInviteHandler(CloseableHttpClient client, ClientDnsOverwrite dns, InvitationManager invMgr) {
this.client = client;
this.dns = dns;
this.invMgr = invMgr;
}
@Override
public void handleRequest(HttpServerExchange exchange) {
String accessToken = getAccessToken(exchange);
String whoamiUri = dns.transform(URI.create(exchange.getRequestURL()).resolve(URI.create("/_matrix/client/r0/account/whoami"))).toString();
log.info("Who Am I URL: {}", whoamiUri);
HttpGet whoAmIReq = new HttpGet(whoamiUri);
whoAmIReq.addHeader("Authorization", "Bearer " + accessToken);
_MatrixID uId;
try (CloseableHttpResponse whoAmIRes = client.execute(whoAmIReq)) {
int sc = whoAmIRes.getStatusLine().getStatusCode();
String body = EntityUtils.toString(whoAmIRes.getEntity());
if (sc != 200) {
log.warn("Unable to get caller identity from Homeserver - Status code: {}", sc);
log.debug("Body: {}", body);
throw new RemoteHomeServerException(body);
}
JsonObject json = GsonUtil.parseObj(body);
Optional<String> uIdRaw = GsonUtil.findString(json, "user_id");
if (!uIdRaw.isPresent()) {
throw new RemoteHomeServerException("No User ID provided when checking identity");
}
uId = MatrixID.asAcceptable(uIdRaw.get());
} catch (IOException e) {
InternalServerError ex = new InternalServerError(e);
log.error("Ref {}: Unable to fetch caller identity from Homeserver", ex.getReference());
throw ex;
}
log.info("Processing room invite from {}", uId.getId());
JsonObject reqBody = parseJsonObject(exchange);
if (!invMgr.canInvite(uId, reqBody)) {
throw new NotAllowedException("Your account is not allowed to invite that address");
}
log.info("Invite was allowing, relaying to the Homeserver");
proxyPost(exchange, reqBody, client, dns);
}
}

View File

@@ -0,0 +1,77 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sarl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.http.undertow.handler.register.v1;
import com.google.gson.JsonObject;
import io.kamax.matrix.ThreePid;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.dns.ClientDnsOverwrite;
import io.kamax.mxisd.exception.NotAllowedException;
import io.kamax.mxisd.http.io.identity.SessionEmailTokenRequestJson;
import io.kamax.mxisd.http.io.identity.SessionPhoneTokenRequestJson;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.registration.RegistrationManager;
import io.undertow.server.HttpServerExchange;
import org.apache.http.impl.client.CloseableHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Register3pidRequestTokenHandler extends BasicHttpHandler {
public static final String Key = "medium";
public static final String Path = "/_matrix/client/r0/register/{" + Key + "}/requestToken";
private static final Logger log = LoggerFactory.getLogger(Register3pidRequestTokenHandler.class);
private final RegistrationManager mgr;
private final ClientDnsOverwrite dns;
private final CloseableHttpClient client;
public Register3pidRequestTokenHandler(RegistrationManager mgr, ClientDnsOverwrite dns, CloseableHttpClient client) {
this.mgr = mgr;
this.dns = dns; // FIXME this shouldn't be in here but in the manager
this.client = client; // FIXME this shouldn't be in here but in the manager
}
@Override
public void handleRequest(HttpServerExchange exchange) {
JsonObject body = parseJsonObject(exchange);
String medium = getPathVariable(exchange, Key);
String address = GsonUtil.findString(body, "address").orElse("");
if (ThreePidMedium.Email.is(medium)) {
address = GsonUtil.get().fromJson(body, SessionEmailTokenRequestJson.class).getValue();
} else if (ThreePidMedium.PhoneNumber.is(medium)) {
address = GsonUtil.get().fromJson(body, SessionPhoneTokenRequestJson.class).getValue();
} else {
log.warn("Unsupported 3PID medium. We attempted to extract the address but the call might fail");
}
ThreePid tpid = new ThreePid(medium, address);
if (!mgr.isAllowed(tpid)) {
throw new NotAllowedException("Your " + medium + " address cannot be used for registration");
}
proxyPost(exchange, body, client, dns);
}
}

View File

@@ -26,7 +26,7 @@ import io.undertow.server.HttpServerExchange;
public class StatusHandler extends BasicHttpHandler { public class StatusHandler extends BasicHttpHandler {
public static final String Path = "/_matrix/identity/status"; public static final String Path = "/status";
@Override @Override
public void handleRequest(HttpServerExchange exchange) { public void handleRequest(HttpServerExchange exchange) {

View File

@@ -0,0 +1,48 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sarl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.http.undertow.handler.status;
import com.google.gson.JsonObject;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.Mxisd;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.undertow.server.HttpServerExchange;
public class VersionHandler extends BasicHttpHandler {
public static final String Path = "/version";
private final String body;
public VersionHandler() {
JsonObject server = new JsonObject();
server.addProperty("name", Mxisd.Name);
server.addProperty("version", Mxisd.Version);
body = GsonUtil.getPrettyForLog(GsonUtil.makeObj("server", server));
}
@Override
public void handleRequest(HttpServerExchange exchange) {
respondJson(exchange, body);
}
}

View File

@@ -18,10 +18,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.as; package io.kamax.mxisd.invitation;
import io.kamax.matrix._MatrixID; import io.kamax.matrix._MatrixID;
import io.kamax.mxisd.invitation.IThreePidInvite;
public interface IMatrixIdInvite extends IThreePidInvite { public interface IMatrixIdInvite extends IThreePidInvite {

View File

@@ -20,6 +20,8 @@
package io.kamax.mxisd.invitation; package io.kamax.mxisd.invitation;
import java.util.List;
public interface IThreePidInviteReply { public interface IThreePidInviteReply {
String getId(); String getId();
@@ -30,4 +32,6 @@ public interface IThreePidInviteReply {
String getDisplayName(); String getDisplayName();
List<String> getPublicKeys();
} }

View File

@@ -23,21 +23,29 @@ package io.kamax.mxisd.invitation;
import com.google.gson.JsonArray; import com.google.gson.JsonArray;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import io.kamax.matrix.MatrixID; import io.kamax.matrix.MatrixID;
import io.kamax.matrix.crypto.SignatureManager; import io.kamax.matrix.ThreePid;
import io.kamax.matrix._MatrixID;
import io.kamax.matrix.json.GsonUtil; import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.config.InvitationConfig; import io.kamax.mxisd.config.InvitationConfig;
import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.crypto.*;
import io.kamax.mxisd.dns.FederationDnsOverwrite; import io.kamax.mxisd.dns.FederationDnsOverwrite;
import io.kamax.mxisd.exception.BadRequestException; import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.exception.ConfigurationException;
import io.kamax.mxisd.exception.MappingAlreadyExistsException; import io.kamax.mxisd.exception.MappingAlreadyExistsException;
import io.kamax.mxisd.exception.ObjectNotFoundException;
import io.kamax.mxisd.lookup.SingleLookupReply; import io.kamax.mxisd.lookup.SingleLookupReply;
import io.kamax.mxisd.lookup.ThreePidMapping; import io.kamax.mxisd.lookup.ThreePidMapping;
import io.kamax.mxisd.lookup.strategy.LookupStrategy; import io.kamax.mxisd.lookup.strategy.LookupStrategy;
import io.kamax.mxisd.notification.NotificationManager; import io.kamax.mxisd.notification.NotificationManager;
import io.kamax.mxisd.profile.ProfileManager;
import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.IStorage;
import io.kamax.mxisd.storage.ormlite.dao.ThreePidInviteIO; import io.kamax.mxisd.storage.ormlite.dao.ThreePidInviteIO;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.RandomStringUtils; import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.conn.ssl.NoopHostnameVerifier;
@@ -57,6 +65,8 @@ import java.io.IOException;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.DateTimeException;
import java.time.Instant;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ForkJoinPool;
@@ -64,14 +74,20 @@ import java.util.concurrent.TimeUnit;
public class InvitationManager { public class InvitationManager {
private transient final Logger log = LoggerFactory.getLogger(InvitationManager.class); private static final Logger log = LoggerFactory.getLogger(InvitationManager.class);
private static final String CreatedAtPropertyKey = "created_at";
private final String defaultCreateTs = Long.toString(Instant.now().toEpochMilli());
private InvitationConfig cfg; private InvitationConfig cfg;
private ServerConfig srvCfg;
private IStorage storage; private IStorage storage;
private LookupStrategy lookupMgr; private LookupStrategy lookupMgr;
private KeyManager keyMgr;
private SignatureManager signMgr; private SignatureManager signMgr;
private FederationDnsOverwrite dns; private FederationDnsOverwrite dns;
private NotificationManager notifMgr; private NotificationManager notifMgr;
private ProfileManager profileMgr;
private CloseableHttpClient client; private CloseableHttpClient client;
private Timer refreshTimer; private Timer refreshTimer;
@@ -79,23 +95,29 @@ public class InvitationManager {
private Map<String, IThreePidInviteReply> invitations = new ConcurrentHashMap<>(); private Map<String, IThreePidInviteReply> invitations = new ConcurrentHashMap<>();
public InvitationManager( public InvitationManager(
InvitationConfig cfg, MxisdConfig mxisdCfg,
IStorage storage, IStorage storage,
LookupStrategy lookupMgr, LookupStrategy lookupMgr,
KeyManager keyMgr,
SignatureManager signMgr, SignatureManager signMgr,
FederationDnsOverwrite dns, FederationDnsOverwrite dns,
NotificationManager notifMgr NotificationManager notifMgr,
ProfileManager profileMgr
) { ) {
this.cfg = cfg; this.cfg = requireValid(mxisdCfg);
this.srvCfg = mxisdCfg.getServer();
this.storage = storage; this.storage = storage;
this.lookupMgr = lookupMgr; this.lookupMgr = lookupMgr;
this.keyMgr = keyMgr;
this.signMgr = signMgr; this.signMgr = signMgr;
this.dns = dns; this.dns = dns;
this.notifMgr = notifMgr; this.notifMgr = notifMgr;
this.profileMgr = profileMgr;
log.info("Loading saved invites"); log.info("Loading saved invites");
Collection<ThreePidInviteIO> ioList = storage.getInvites(); Collection<ThreePidInviteIO> ioList = storage.getInvites();
ioList.forEach(io -> { ioList.forEach(io -> {
io.getProperties().putIfAbsent(CreatedAtPropertyKey, defaultCreateTs);
log.info("Processing invite {}", GsonUtil.get().toJson(io)); log.info("Processing invite {}", GsonUtil.get().toJson(io));
ThreePidInvite invite = new ThreePidInvite( ThreePidInvite invite = new ThreePidInvite(
MatrixID.asAcceptable(io.getSender()), MatrixID.asAcceptable(io.getSender()),
@@ -105,7 +127,7 @@ public class InvitationManager {
io.getProperties() io.getProperties()
); );
ThreePidInviteReply reply = new ThreePidInviteReply(getId(invite), invite, io.getToken(), ""); ThreePidInviteReply reply = new ThreePidInviteReply(io.getId(), invite, io.getToken(), "", Collections.emptyList());
invitations.put(reply.getId(), reply); invitations.put(reply.getId(), reply);
}); });
@@ -122,25 +144,69 @@ public class InvitationManager {
log.info("Setting up invitation mapping refresh timer"); log.info("Setting up invitation mapping refresh timer");
refreshTimer = new Timer(); refreshTimer = new Timer();
refreshTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
try {
lookupMappingsForInvites();
} catch (Throwable t) {
log.error("Error when running background mapping refresh", t);
}
}
}, 5000L, TimeUnit.MILLISECONDS.convert(cfg.getResolution().getTimer(), TimeUnit.MINUTES));
// We add a shutdown hook to cancel the hook and wait for pending resolutions
Runtime.getRuntime().addShutdownHook(new Thread(() -> { Runtime.getRuntime().addShutdownHook(new Thread(() -> {
refreshTimer.cancel(); refreshTimer.cancel();
ForkJoinPool.commonPool().awaitQuiescence(1, TimeUnit.MINUTES); ForkJoinPool.commonPool().awaitQuiescence(1, TimeUnit.MINUTES);
})); }));
// We set the refresh timer for background tasks
refreshTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
try {
doMaintenance();
} catch (Throwable t) {
log.error("Error when running background maintenance", t);
}
}
}, 5000L, TimeUnit.MILLISECONDS.convert(cfg.getResolution().getTimer(), TimeUnit.MINUTES));
} }
private String getId(IThreePidInvite invite) { private InvitationConfig requireValid(MxisdConfig cfg) {
return invite.getSender().getDomain().toLowerCase() + invite.getMedium().toLowerCase() + invite.getAddress().toLowerCase(); // This is not configured, we'll apply a default configuration
if (Objects.isNull(cfg.getInvite().getExpiration().isEnabled())) {
// We compute our own user, so it can be used if we bridge as well
String mxId = MatrixID.asAcceptable("_mxisd-expired_invite", cfg.getMatrix().getDomain()).getId();
// Enabled by default
cfg.getInvite().getExpiration().setEnabled(true);
// We'll resolve to our computed User ID
cfg.getInvite().getExpiration().setResolveTo(mxId);
// One calendar week (60min/1h * 24 = 1d * 7 = 1w)
cfg.getInvite().getExpiration().setAfter(60 * 24 * 7);
}
if (cfg.getInvite().getExpiration().isEnabled()) {
if (cfg.getInvite().getExpiration().getAfter() < 1) {
throw new ConfigurationException("Invitation expiration delay must be greater or equal to 1");
}
if (StringUtils.isBlank(cfg.getInvite().getExpiration().getResolveTo())) {
String localpart = cfg.getAppsvc().getUser().getInviteExpired();
if (StringUtils.isBlank(localpart)) {
throw new ConfigurationException("Could not compute the Invitation expiration resolution target from App service user: not set");
}
cfg.getInvite().getExpiration().setResolveTo(MatrixID.asAcceptable(localpart, cfg.getMatrix().getDomain()).getId());
}
try {
MatrixID.asAcceptable(cfg.getInvite().getExpiration().getResolveTo());
} catch (IllegalArgumentException e) {
throw new ConfigurationException("Invitation expiration resolution target is not a valid Matrix ID: " + e.getMessage());
}
}
return cfg.getInvite();
}
private String computeId(IThreePidInvite invite) {
String rawId = invite.getSender().getDomain().toLowerCase() + invite.getMedium().toLowerCase() + invite.getAddress().toLowerCase();
return Base64.encodeBase64URLSafeString(rawId.getBytes(StandardCharsets.UTF_8));
} }
private String getIdForLog(IThreePidInviteReply reply) { private String getIdForLog(IThreePidInviteReply reply) {
@@ -205,19 +271,56 @@ public class InvitationManager {
return lookupMgr.find(medium, address, cfg.getResolution().isRecursive()); return lookupMgr.find(medium, address, cfg.getResolution().isRecursive());
} }
public List<IThreePidInviteReply> listInvites() {
return new ArrayList<>(invitations.values());
}
public IThreePidInviteReply getInvite(String id) {
IThreePidInviteReply v = invitations.get(id);
if (Objects.isNull(v)) {
throw new ObjectNotFoundException("Invite", id);
}
return v;
}
public boolean canInvite(_MatrixID sender, JsonObject request) {
if (!request.has("medium")) {
log.info("Not a 3PID invite, allowing");
return true;
}
log.info("3PID invite detected, checking policies...");
List<String> allowedRoles = cfg.getPolicy().getIfSender().getHasRole();
if (Objects.isNull(allowedRoles)) {
log.info("No allowed role configured for sender, allowing");
return true;
}
List<String> userRoles = profileMgr.getRoles(sender);
if (Collections.disjoint(userRoles, allowedRoles)) {
log.info("Sender does not have any of the required roles, denying");
return false;
}
log.info("Sender has at least one of the required roles");
log.info("Sender pass all policies to invite, allowing");
return true;
}
public synchronized IThreePidInviteReply storeInvite(IThreePidInvite invitation) { // TODO better sync public synchronized IThreePidInviteReply storeInvite(IThreePidInvite invitation) { // TODO better sync
if (!notifMgr.isMediumSupported(invitation.getMedium())) { if (!notifMgr.isMediumSupported(invitation.getMedium())) {
throw new BadRequestException("Medium type " + invitation.getMedium() + " is not supported"); throw new BadRequestException("Medium type " + invitation.getMedium() + " is not supported");
} }
String invId = getId(invitation); String invId = computeId(invitation);
log.info("Handling invite for {}:{} from {} in room {}", invitation.getMedium(), invitation.getAddress(), invitation.getSender(), invitation.getRoomId()); log.info("Handling invite for {}:{} from {} in room {}", invitation.getMedium(), invitation.getAddress(), invitation.getSender(), invitation.getRoomId());
IThreePidInviteReply reply = invitations.get(invId); IThreePidInviteReply reply = invitations.get(invId);
if (reply != null) { if (reply != null) {
log.info("Invite is already pending for {}:{}, returning data", invitation.getMedium(), invitation.getAddress()); log.info("Invite is already pending for {}:{}, returning data", invitation.getMedium(), invitation.getAddress());
if (!StringUtils.equals(invitation.getRoomId(), reply.getInvite().getRoomId())) { if (!StringUtils.equals(invitation.getRoomId(), reply.getInvite().getRoomId())) {
log.info("Sending new notification as new invite room {} is different from the original {}", invitation.getRoomId(), reply.getInvite().getRoomId()); log.info("Sending new notification as new invite room {} is different from the original {}", invitation.getRoomId(), reply.getInvite().getRoomId());
notifMgr.sendForReply(new ThreePidInviteReply(reply.getId(), invitation, reply.getToken(), reply.getDisplayName())); notifMgr.sendForReply(new ThreePidInviteReply(reply.getId(), invitation, reply.getToken(), reply.getDisplayName(), reply.getPublicKeys()));
} else { } else {
// FIXME we should check attempt and send if bigger // FIXME we should check attempt and send if bigger
} }
@@ -232,8 +335,21 @@ public class InvitationManager {
String token = RandomStringUtils.randomAlphanumeric(64); String token = RandomStringUtils.randomAlphanumeric(64);
String displayName = invitation.getAddress().substring(0, 3) + "..."; String displayName = invitation.getAddress().substring(0, 3) + "...";
KeyIdentifier pKeyId = keyMgr.getServerSigningKey().getId();
KeyIdentifier eKeyId = keyMgr.generateKey(KeyType.Ephemeral);
reply = new ThreePidInviteReply(invId, invitation, token, displayName); String pPubKey = keyMgr.getPublicKeyBase64(pKeyId);
String ePubKey = keyMgr.getPublicKeyBase64(eKeyId);
invitation.getProperties().put(CreatedAtPropertyKey, Long.toString(Instant.now().toEpochMilli()));
invitation.getProperties().put("p_key_algo", pKeyId.getAlgorithm());
invitation.getProperties().put("p_key_serial", pKeyId.getSerial());
invitation.getProperties().put("p_key_public", pPubKey);
invitation.getProperties().put("e_key_algo", eKeyId.getAlgorithm());
invitation.getProperties().put("e_key_serial", eKeyId.getSerial());
invitation.getProperties().put("e_key_public", ePubKey);
reply = new ThreePidInviteReply(invId, invitation, token, displayName, Arrays.asList(pPubKey, ePubKey));
log.info("Performing invite to {}:{}", invitation.getMedium(), invitation.getAddress()); log.info("Performing invite to {}:{}", invitation.getMedium(), invitation.getAddress());
notifMgr.sendForReply(reply); notifMgr.sendForReply(reply);
@@ -246,6 +362,78 @@ public class InvitationManager {
return reply; return reply;
} }
public boolean hasInvite(ThreePid tpid) {
for (IThreePidInviteReply reply : invitations.values()) {
if (!StringUtils.equals(tpid.getMedium(), reply.getInvite().getMedium())) {
continue;
}
if (!StringUtils.equals(tpid.getAddress(), reply.getInvite().getAddress())) {
continue;
}
return true;
}
return false;
}
private void removeInvite(IThreePidInviteReply reply) {
invitations.remove(reply.getId());
storage.deleteInvite(reply.getId());
}
/**
* Trigger the periodic maintenance tasks
*/
public void doMaintenance() {
lookupMappingsForInvites();
expireInvites();
}
public void expireInvites() {
log.debug("Invite expiration: started");
if (!cfg.getExpiration().isEnabled()) {
log.debug("Invite expiration is disabled, skipping");
return;
}
if (invitations.isEmpty()) {
log.debug("No invite to expired, skipping");
return;
}
String targetMxid = cfg.getExpiration().getResolveTo();
for (IThreePidInviteReply reply : invitations.values()) {
log.debug("Processing invite {}", reply.getId());
String tsRaw = reply.getInvite().getProperties().computeIfAbsent(CreatedAtPropertyKey, k -> defaultCreateTs);
try {
Instant ts = Instant.ofEpochMilli(Long.parseLong(tsRaw));
Instant targetTs = ts.plusSeconds(cfg.getExpiration().getAfter() * 60);
Instant now = Instant.now();
log.debug("Invite {} - Created at {} - Expires at {} - Current time is {}", reply.getId(), ts, targetTs, now);
if (targetTs.isAfter(now)) {
log.debug("Invite {} has not expired yet, skipping", reply.getId());
continue;
}
log.info("Invite {} has expired at TS {} - Expiring and resolving to {}", targetTs, targetMxid);
publishMapping(reply, targetMxid);
} catch (NumberFormatException | DateTimeException e) {
log.warn("Invite {} has an invalid creation TS, setting to default value of {}", reply.getId(), defaultCreateTs);
reply.getInvite().getProperties().put(CreatedAtPropertyKey, defaultCreateTs);
}
}
log.debug("Invite expiration: finished");
}
public void expireInvite(String id) {
publishMapping(getInvite(id), cfg.getExpiration().getResolveTo());
}
public void lookupMappingsForInvites() { public void lookupMappingsForInvites() {
if (!invitations.isEmpty()) { if (!invitations.isEmpty()) {
log.info("Checking for existing mapping for pending invites"); log.info("Checking for existing mapping for pending invites");
@@ -266,6 +454,28 @@ public class InvitationManager {
} }
} }
public IThreePidInviteReply getInvite(String token, String privKey) {
for (IThreePidInviteReply reply : invitations.values()) {
if (StringUtils.equals(reply.getToken(), token)) {
String algo = reply.getInvite().getProperties().get("e_key_algo");
String serial = reply.getInvite().getProperties().get("e_key_serial");
if (StringUtils.isAnyBlank(algo, serial)) {
continue;
}
String storedPrivKey = keyMgr.getKey(new GenericKeyIdentifier(KeyType.Ephemeral, algo, serial)).getPrivateKeyBase64();
if (!StringUtils.equals(storedPrivKey, privKey)) {
continue;
}
return reply;
}
}
throw new ObjectNotFoundException("No invite with such token and/or private key");
}
private void publishMapping(IThreePidInviteReply reply, String mxid) { private void publishMapping(IThreePidInviteReply reply, String mxid) {
String medium = reply.getInvite().getMedium(); String medium = reply.getInvite().getMedium();
String address = reply.getInvite().getAddress(); String address = reply.getInvite().getAddress();
@@ -280,7 +490,7 @@ public class InvitationManager {
JsonObject obj = new JsonObject(); JsonObject obj = new JsonObject();
obj.addProperty("mxid", mxid); obj.addProperty("mxid", mxid);
obj.addProperty("token", reply.getToken()); obj.addProperty("token", reply.getToken());
obj.add("signatures", signMgr.signMessageGson(obj.toString())); obj.add("signatures", signMgr.signMessageGson(srvCfg.getName(), obj.toString()));
JsonObject objUp = new JsonObject(); JsonObject objUp = new JsonObject();
objUp.addProperty("mxid", mxid); objUp.addProperty("mxid", mxid);
@@ -298,30 +508,45 @@ public class InvitationManager {
content.addProperty("address", address); content.addProperty("address", address);
content.addProperty("mxid", mxid); content.addProperty("mxid", mxid);
content.add("signatures", signMgr.signMessageGson(content.toString())); content.add("signatures", signMgr.signMessageGson(srvCfg.getName(), content.toString()));
StringEntity entity = new StringEntity(content.toString(), StandardCharsets.UTF_8); StringEntity entity = new StringEntity(content.toString(), StandardCharsets.UTF_8);
entity.setContentType("application/json"); entity.setContentType("application/json");
req.setEntity(entity); req.setEntity(entity);
Instant resolvedAt = Instant.now();
boolean couldPublish = false;
boolean shouldArchive = true;
try { try {
log.info("Posting onBind event to {}", req.getURI()); log.info("Posting onBind event to {}", req.getURI());
CloseableHttpResponse response = client.execute(req); CloseableHttpResponse response = client.execute(req);
int statusCode = response.getStatusLine().getStatusCode(); int statusCode = response.getStatusLine().getStatusCode();
log.info("Answer code: {}", statusCode); log.info("Answer code: {}", statusCode);
if (statusCode >= 300 && statusCode != 403) { if (statusCode >= 300 && statusCode != 403) {
log.warn("Answer body: {}", IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8)); log.info("Answer body: {}", IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8));
} else { log.warn("HS returned an error.");
if (statusCode == 403) {
log.info("Invite was obsolete");
}
invitations.remove(getId(reply.getInvite())); shouldArchive = statusCode != 502;
storage.deleteInvite(reply.getId()); if (shouldArchive) {
log.info("Removed invite from internal store"); log.info("Invite can be found in historical storage for manual re-processing");
}
} else {
couldPublish = true;
if (statusCode == 403) {
log.info("Invite is obsolete or no longer under our control");
}
} }
response.close(); response.close();
} catch (IOException e) { } catch (IOException e) {
log.warn("Unable to tell HS {} about invite being mapped", domain, e); log.warn("Unable to tell HS {} about invite being mapped", domain, e);
} finally {
if (shouldArchive) {
synchronized (this) {
storage.insertHistoricalInvite(reply, mxid, resolvedAt, couldPublish);
removeInvite(reply);
log.info("Moved invite {} to historical table", reply.getId());
}
}
} }
}).start(); }).start();
} }
@@ -337,7 +562,7 @@ public class InvitationManager {
@Override @Override
public void run() { public void run() {
try { try {
log.info("Searching for mapping created since invite {} was created", getIdForLog(reply)); log.info("Searching for mapping created after invite {} was created", getIdForLog(reply));
Optional<SingleLookupReply> result = lookup3pid(reply.getInvite().getMedium(), reply.getInvite().getAddress()); Optional<SingleLookupReply> result = lookup3pid(reply.getInvite().getMedium(), reply.getInvite().getAddress());
if (result.isPresent()) { if (result.isPresent()) {
SingleLookupReply lookup = result.get(); SingleLookupReply lookup = result.get();

View File

@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.as; package io.kamax.mxisd.invitation;
import io.kamax.matrix._MatrixID; import io.kamax.matrix._MatrixID;

View File

@@ -20,18 +20,24 @@
package io.kamax.mxisd.invitation; package io.kamax.mxisd.invitation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ThreePidInviteReply implements IThreePidInviteReply { public class ThreePidInviteReply implements IThreePidInviteReply {
private String id; private String id;
private IThreePidInvite invite; private IThreePidInvite invite;
private String token; private String token;
private String displayName; private String displayName;
private List<String> publicKeys;
public ThreePidInviteReply(String id, IThreePidInvite invite, String token, String displayName) { public ThreePidInviteReply(String id, IThreePidInvite invite, String token, String displayName, List<String> publicKeys) {
this.id = id; this.id = id;
this.invite = invite; this.invite = invite;
this.token = token; this.token = token;
this.displayName = displayName; this.displayName = displayName;
this.publicKeys = Collections.unmodifiableList(new ArrayList<>(publicKeys));
} }
@Override @Override
@@ -54,4 +60,9 @@ public class ThreePidInviteReply implements IThreePidInviteReply {
return displayName; return displayName;
} }
@Override
public List<String> getPublicKeys() {
return publicKeys;
}
} }

View File

@@ -27,6 +27,8 @@ import io.kamax.matrix._MatrixID;
import io.kamax.mxisd.http.io.identity.SingeLookupReplyJson; import io.kamax.mxisd.http.io.identity.SingeLookupReplyJson;
import java.time.Instant; import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
public class SingleLookupReply { public class SingleLookupReply {
@@ -39,6 +41,7 @@ public class SingleLookupReply {
private Instant notBefore; private Instant notBefore;
private Instant notAfter; private Instant notAfter;
private Instant timestamp; private Instant timestamp;
private Map<String, Map<String, String>> signatures = new HashMap<>();
public static SingleLookupReply fromRecursive(SingleLookupRequest request, String body) { public static SingleLookupReply fromRecursive(SingleLookupRequest request, String body) {
SingleLookupReply reply = new SingleLookupReply(); SingleLookupReply reply = new SingleLookupReply();
@@ -52,6 +55,7 @@ public class SingleLookupReply {
reply.notAfter = Instant.ofEpochMilli(json.getNot_after()); reply.notAfter = Instant.ofEpochMilli(json.getNot_after());
reply.notBefore = Instant.ofEpochMilli(json.getNot_before()); reply.notBefore = Instant.ofEpochMilli(json.getNot_before());
reply.timestamp = Instant.ofEpochMilli(json.getTs()); reply.timestamp = Instant.ofEpochMilli(json.getTs());
reply.signatures = new HashMap<>(json.getSignatures());
} catch (JsonSyntaxException e) { } catch (JsonSyntaxException e) {
// stub - we only want to try, nothing more // stub - we only want to try, nothing more
} }
@@ -107,4 +111,12 @@ public class SingleLookupReply {
return timestamp; return timestamp;
} }
public Map<String, Map<String, String>> getSignatures() {
return signatures;
}
public Map<String, String> getSignature(String host) {
return signatures.computeIfAbsent(host, k -> new HashMap<>());
}
} }

View File

@@ -21,7 +21,7 @@
package io.kamax.mxisd.notification; package io.kamax.mxisd.notification;
import io.kamax.matrix.ThreePid; import io.kamax.matrix.ThreePid;
import io.kamax.mxisd.as.IMatrixIdInvite; import io.kamax.mxisd.invitation.IMatrixIdInvite;
import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.threepid.session.IThreePidSession; import io.kamax.mxisd.threepid.session.IThreePidSession;

View File

@@ -21,9 +21,9 @@
package io.kamax.mxisd.notification; package io.kamax.mxisd.notification;
import io.kamax.matrix.ThreePid; import io.kamax.matrix.ThreePid;
import io.kamax.mxisd.as.IMatrixIdInvite;
import io.kamax.mxisd.config.threepid.notification.NotificationConfig; import io.kamax.mxisd.config.threepid.notification.NotificationConfig;
import io.kamax.mxisd.exception.NotImplementedException; import io.kamax.mxisd.exception.NotImplementedException;
import io.kamax.mxisd.invitation.IMatrixIdInvite;
import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.threepid.session.IThreePidSession; import io.kamax.mxisd.threepid.session.IThreePidSession;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;

View File

@@ -37,10 +37,7 @@ import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.ArrayList; import java.util.*;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -113,4 +110,8 @@ public class ProfileManager {
} }
} }
public boolean hasAnyRole(_MatrixID user, List<String> requiredRoles) {
return !requiredRoles.isEmpty() || Collections.disjoint(getRoles(user), requiredRoles);
}
} }

View File

@@ -0,0 +1,142 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sarl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.registration;
import com.google.gson.JsonObject;
import io.kamax.matrix.ThreePid;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.config.RegisterConfig;
import io.kamax.mxisd.dns.ClientDnsOverwrite;
import io.kamax.mxisd.exception.NotImplementedException;
import io.kamax.mxisd.exception.RemoteHomeServerException;
import io.kamax.mxisd.invitation.InvitationManager;
import io.kamax.mxisd.util.RestClientUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URI;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class RegistrationManager {
private static final Logger log = LoggerFactory.getLogger(RegistrationManager.class);
private final RegisterConfig cfg;
private final CloseableHttpClient client;
private final ClientDnsOverwrite dns;
private final InvitationManager invMgr;
public RegistrationManager(RegisterConfig cfg, CloseableHttpClient client, ClientDnsOverwrite dns, InvitationManager invMgr) {
this.cfg = cfg;
this.client = client;
this.dns = dns;
this.invMgr = invMgr;
}
private String resolveProxyUrl(URI target) {
URIBuilder builder = dns.transform(target);
String urlToLogin = builder.toString();
log.info("Proxy resolution: {} to {}", target.toString(), urlToLogin);
return urlToLogin;
}
public RegistrationReply execute(URI target, JsonObject request) {
HttpPost registerProxyRq = RestClientUtils.post(resolveProxyUrl(target), GsonUtil.get(), request);
try (CloseableHttpResponse response = client.execute(registerProxyRq)) {
int status = response.getStatusLine().getStatusCode();
if (status == 200) {
// The user managed to register. We check if it had a session
String sessionId = GsonUtil.findObj(request, "auth").flatMap(auth -> GsonUtil.findString(auth, "session")).orElse("");
if (StringUtils.isEmpty(sessionId)) {
// No session ID was provided. This is an edge case we do not support for now as investigation is needed
// to ensure how and when this happens.
HttpPost newSessReq = RestClientUtils.post(resolveProxyUrl(target), GsonUtil.get(), new JsonObject());
try (CloseableHttpResponse newSessRes = client.execute(newSessReq)) {
RegistrationReply reply = new RegistrationReply();
reply.setStatus(newSessRes.getStatusLine().getStatusCode());
reply.setBody(GsonUtil.parseObj(EntityUtils.toString(newSessRes.getEntity())));
return reply;
}
}
}
throw new NotImplementedException("Registration");
} catch (IOException e) {
throw new RemoteHomeServerException(e.getMessage());
}
}
public boolean isAllowed(ThreePid tpid) {
// We check if the policy allows registration for invites, and if there is an invite for the 3PID
if (cfg.getPolicy().forInvite() && invMgr.hasInvite(tpid)) {
log.info("Registration allowed for pending invite");
return true;
}
// The following section deals with patterns which can either be built at startup time, or for each invite at runtime.
// Registration is a very rare occurrence relatively speaking, so we make the choice to build the patterns each time
// at runtime to save on RAM.
Object policy = cfg.getPolicy().getThreepid().get(tpid.getMedium());
if (Objects.nonNull(policy)) {
RegisterConfig.ThreepidPolicy tpidPolicy = GsonUtil.get().fromJson(GsonUtil.get().toJson(policy), RegisterConfig.ThreepidPolicy.class);
log.info("Found registration policy for {}", tpid.getMedium());
log.info("Processing pattern blacklist");
for (String pattern : tpidPolicy.getPattern().getBlacklist()) {
log.info("Processing pattern {}", pattern);
// We compile the pattern
Matcher m = Pattern.compile(pattern).matcher(tpid.getAddress());
if (m.matches()) { // We only care about those who match...
log.info("Found matching blacklist entry, denying registration");
return false; // ... and get denied as per blacklist
}
}
log.info("Processing pattern whitelist");
for (String pattern : tpidPolicy.getPattern().getWhitelist()) {
log.info("Processing pattern {}", pattern);
// We compile the pattern
Matcher m = Pattern.compile(pattern).matcher(tpid.getAddress());
if (m.matches()) { // We only care about those who match...
log.info("Found matching whitelist entry, allowing registration");
return true; // ... and get accepted as per whitelist
}
}
}
log.info("Returning default registration policy: {}", cfg.getPolicy().isAllowed());
return cfg.getPolicy().isAllowed();
}
}

View File

@@ -0,0 +1,46 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sarl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.registration;
import com.google.gson.JsonObject;
public class RegistrationReply {
private int status;
private JsonObject body;
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public JsonObject getBody() {
return body;
}
public void setBody(JsonObject body) {
this.body = body;
}
}

View File

@@ -38,6 +38,8 @@ public interface IStorage {
void deleteInvite(String id); void deleteInvite(String id);
void insertHistoricalInvite(IThreePidInviteReply data, String resolvedTo, Instant resolvedAt, boolean couldPublish);
Optional<IThreePidSessionDao> getThreePidSession(String sid); Optional<IThreePidSessionDao> getThreePidSession(String sid);
Optional<IThreePidSessionDao> findThreePidSession(ThreePid tpid, String secret); Optional<IThreePidSessionDao> findThreePidSession(ThreePid tpid, String secret);

View File

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

View File

@@ -0,0 +1,256 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sàrl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.storage.crypto;
import com.google.gson.JsonObject;
import io.kamax.matrix.crypto.KeyFileStore;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.crypto.*;
import io.kamax.mxisd.exception.ObjectNotFoundException;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
public class FileKeyStore implements KeyStore {
private static final Logger log = LoggerFactory.getLogger(FileKeyStore.class);
private final String currentFilename = "current";
private final String base;
public FileKeyStore(String path) {
base = new File(path).getAbsoluteFile().toString();
File f = new File(base);
if (!f.exists()) {
try {
FileUtils.forceMkdir(f);
} catch (IOException e) {
throw new RuntimeException("Unable to create key store");
}
} else {
if (f.isFile()) {
try {
log.info("Found old key store format at {}, migrating...", base);
File oldStorePath = new File(f.toString() + ".backup-before-migration");
FileUtils.moveFile(f, oldStorePath);
FileUtils.forceMkdir(f);
String privKey = new KeyFileStore(oldStorePath.toString()).load().orElse("");
if (StringUtils.isBlank(privKey)) {
log.info("Empty file, nothing to migrate");
} else {
// We ensure this is valid Base64 data before migrating
Base64.decodeBase64(privKey);
// We store the new key
add(new GenericKey(new GenericKeyIdentifier(KeyType.Regular, KeyAlgorithm.Ed25519, "0"), true, privKey));
log.info("Store migrated to new directory format");
}
} catch (IOException e) {
throw new RuntimeException("Unable to migrate store from old single file format to new directory format", e);
}
} else {
log.info("Key store is already in directory format");
}
}
if (!f.isDirectory()) {
throw new RuntimeException("Key store path is not a directory: " + f.toString());
}
}
private String toDirName(KeyType type) {
return type.name().toLowerCase();
}
private Path ensureDirExists(KeyIdentifier id) {
File b = Paths.get(base, toDirName(id.getType()), id.getAlgorithm()).toFile();
if (b.exists()) {
if (!b.isDirectory()) {
throw new RuntimeException("Key store path already exists but is not a directory: " + b.toString());
}
} else {
try {
FileUtils.forceMkdir(b);
} catch (IOException e) {
throw new RuntimeException("Unable to create key store path at " + b.toString(), e);
}
}
return b.toPath();
}
@Override
public boolean has(KeyIdentifier id) {
return Paths.get(base, toDirName(id.getType()), id.getAlgorithm(), id.getSerial()).toFile().isFile();
}
@Override
public List<KeyIdentifier> list() {
List<KeyIdentifier> keyIds = new ArrayList<>();
for (KeyType type : KeyType.values()) {
keyIds.addAll(list(type));
}
return keyIds;
}
@Override
public List<KeyIdentifier> list(KeyType type) {
List<KeyIdentifier> keyIds = new ArrayList<>();
File algoDir = Paths.get(base, toDirName(type)).toFile();
File[] algos = algoDir.listFiles();
if (Objects.isNull(algos)) {
return keyIds;
}
for (File algo : algos) {
File[] serials = algo.listFiles();
if (Objects.isNull(serials)) {
throw new IllegalStateException("Cannot list stored key serials: was expecting " + algo.toString() + " to be a directory");
}
for (File serial : serials) {
keyIds.add(new GenericKeyIdentifier(type, algo.getName(), serial.getName()));
}
}
return keyIds;
}
@Override
public Key get(KeyIdentifier id) throws ObjectNotFoundException {
File keyFile = ensureDirExists(id).resolve(id.getSerial()).toFile();
if (!keyFile.exists() || !keyFile.isFile()) {
throw new ObjectNotFoundException("Key", id.getId());
}
try (FileInputStream keyIs = new FileInputStream(keyFile)) {
FileKeyJson json = GsonUtil.get().fromJson(IOUtils.toString(keyIs, StandardCharsets.UTF_8), FileKeyJson.class);
return new GenericKey(id, json.isValid(), json.getKey());
} catch (IOException e) {
throw new RuntimeException("Unable to read key " + id.getId(), e);
}
}
@Override
public void add(Key key) throws IllegalStateException {
File keyFile = ensureDirExists(key.getId()).resolve(key.getId().getSerial()).toFile();
if (keyFile.exists()) {
throw new IllegalStateException("Key " + key.getId().getId() + " already exists");
}
FileKeyJson json = FileKeyJson.get(key);
try (FileOutputStream keyOs = new FileOutputStream(keyFile, false)) {
IOUtils.write(GsonUtil.get().toJson(json), keyOs, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException("Unable to create key " + key.getId().getId(), e);
}
}
@Override
public void update(Key key) throws ObjectNotFoundException {
File keyFile = ensureDirExists(key.getId()).resolve(key.getId().getSerial()).toFile();
if (!keyFile.exists() || !keyFile.isFile()) {
throw new ObjectNotFoundException("Key", key.getId().getId());
}
FileKeyJson json = FileKeyJson.get(key);
try (FileOutputStream keyOs = new FileOutputStream(keyFile, false)) {
IOUtils.write(GsonUtil.get().toJson(json), keyOs, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException("Unable to create key " + key.getId().getId(), e);
}
}
@Override
public void delete(KeyIdentifier id) throws ObjectNotFoundException {
File keyFile = ensureDirExists(id).resolve(id.getSerial()).toFile();
if (!keyFile.exists() || !keyFile.isFile()) {
throw new ObjectNotFoundException("Key", id.getId());
}
if (!keyFile.delete()) {
throw new RuntimeException("Unable to delete key " + id.getId());
}
}
@Override
public void setCurrentKey(KeyIdentifier id) throws IllegalArgumentException {
if (!has(id)) {
throw new IllegalArgumentException("Key " + id.getType() + ":" + id.getAlgorithm() + ":" + id.getSerial() + " is not known to the store");
}
JsonObject json = new JsonObject();
json.addProperty("type", id.getType().name());
json.addProperty("algo", id.getAlgorithm());
json.addProperty("serial", id.getSerial());
File f = Paths.get(base, currentFilename).toFile();
try (FileOutputStream keyOs = new FileOutputStream(f, false)) {
IOUtils.write(GsonUtil.get().toJson(json), keyOs, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException("Unable to write to " + f.toString(), e);
}
}
@Override
public Optional<KeyIdentifier> getCurrentKey() {
File f = Paths.get(base, currentFilename).toFile();
if (!f.exists()) {
return Optional.empty();
}
if (!f.isFile()) {
throw new IllegalStateException("Current key file is not a file: " + f.toString());
}
try (FileInputStream keyIs = new FileInputStream(f)) {
JsonObject json = GsonUtil.parseObj(IOUtils.toString(keyIs, StandardCharsets.UTF_8));
return Optional.of(new GenericKeyIdentifier(KeyType.valueOf(GsonUtil.getStringOrThrow(json, "type")), GsonUtil.getStringOrThrow(json, "algo"), GsonUtil.getStringOrThrow(json, "serial")));
} catch (IOException e) {
throw new RuntimeException("Unable to read " + f.toString(), e);
}
}
}

View File

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

View File

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

View File

@@ -34,24 +34,18 @@ import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.storage.IStorage; import io.kamax.mxisd.storage.IStorage;
import io.kamax.mxisd.storage.dao.IThreePidSessionDao; import io.kamax.mxisd.storage.dao.IThreePidSessionDao;
import io.kamax.mxisd.storage.ormlite.dao.ASTransactionDao; import io.kamax.mxisd.storage.ormlite.dao.ASTransactionDao;
import io.kamax.mxisd.storage.ormlite.dao.HistoricalThreePidInviteIO;
import io.kamax.mxisd.storage.ormlite.dao.ThreePidInviteIO; import io.kamax.mxisd.storage.ormlite.dao.ThreePidInviteIO;
import io.kamax.mxisd.storage.ormlite.dao.ThreePidSessionDao; import io.kamax.mxisd.storage.ormlite.dao.ThreePidSessionDao;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList; import java.util.*;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
public class OrmLiteSqlStorage implements IStorage { public class OrmLiteSqlStorage implements IStorage {
private transient final Logger log = LoggerFactory.getLogger(OrmLiteSqlStorage.class);
@FunctionalInterface @FunctionalInterface
private interface Getter<T> { private interface Getter<T> {
@@ -67,6 +61,7 @@ public class OrmLiteSqlStorage implements IStorage {
} }
private Dao<ThreePidInviteIO, String> invDao; private Dao<ThreePidInviteIO, String> invDao;
private Dao<HistoricalThreePidInviteIO, String> expInvDao;
private Dao<ThreePidSessionDao, String> sessionDao; private Dao<ThreePidSessionDao, String> sessionDao;
private Dao<ASTransactionDao, String> asTxnDao; private Dao<ASTransactionDao, String> asTxnDao;
@@ -86,6 +81,7 @@ public class OrmLiteSqlStorage implements IStorage {
withCatcher(() -> { withCatcher(() -> {
ConnectionSource connPool = new JdbcConnectionSource("jdbc:" + backend + ":" + path); ConnectionSource connPool = new JdbcConnectionSource("jdbc:" + backend + ":" + path);
invDao = createDaoAndTable(connPool, ThreePidInviteIO.class); invDao = createDaoAndTable(connPool, ThreePidInviteIO.class);
expInvDao = createDaoAndTable(connPool, HistoricalThreePidInviteIO.class);
sessionDao = createDaoAndTable(connPool, ThreePidSessionDao.class); sessionDao = createDaoAndTable(connPool, ThreePidSessionDao.class);
asTxnDao = createDaoAndTable(connPool, ASTransactionDao.class); asTxnDao = createDaoAndTable(connPool, ASTransactionDao.class);
}); });
@@ -150,6 +146,24 @@ public class OrmLiteSqlStorage implements IStorage {
}); });
} }
@Override
public void insertHistoricalInvite(IThreePidInviteReply data, String resolvedTo, Instant resolvedAt, boolean couldPublish) {
withCatcher(() -> {
HistoricalThreePidInviteIO io = new HistoricalThreePidInviteIO(data, resolvedTo, resolvedAt, couldPublish);
int updated = expInvDao.create(io);
if (updated != 1) {
throw new RuntimeException("Unexpected row count after DB action: " + updated);
}
// Ugly, but it avoids touching the structure of the historical parent class
// and avoid any possible regression at this point.
updated = expInvDao.updateId(io, UUID.randomUUID().toString().replace("-", ""));
if (updated != 1) {
throw new RuntimeException("Unexpected row count after DB action: " + updated);
}
});
}
@Override @Override
public Optional<IThreePidSessionDao> getThreePidSession(String sid) { public Optional<IThreePidSessionDao> getThreePidSession(String sid) {
return withCatcher(() -> Optional.ofNullable(sessionDao.queryForId(sid))); return withCatcher(() -> Optional.ofNullable(sessionDao.queryForId(sid)));

View File

@@ -0,0 +1,72 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sarl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.storage.ormlite.dao;
import com.j256.ormlite.field.DatabaseField;
import com.j256.ormlite.table.DatabaseTable;
import io.kamax.mxisd.invitation.IThreePidInviteReply;
import java.time.Instant;
@DatabaseTable(tableName = "invite_3pid_history")
public class HistoricalThreePidInviteIO extends ThreePidInviteIO {
@DatabaseField(canBeNull = false)
private String resolvedTo;
@DatabaseField(canBeNull = false)
private long resolvedAt;
@DatabaseField(canBeNull = false)
private boolean couldPublish;
@DatabaseField(canBeNull = false)
private long publishAttempts = 1; // Placeholder for retry mechanism, if ever implemented
public HistoricalThreePidInviteIO() {
// Needed for ORMLite
}
public HistoricalThreePidInviteIO(IThreePidInviteReply data, String resolvedTo, Instant resolvedAt, boolean couldPublish) {
super(data);
this.resolvedTo = resolvedTo;
this.resolvedAt = resolvedAt.toEpochMilli();
this.couldPublish = couldPublish;
}
public String getResolvedTo() {
return resolvedTo;
}
public Instant getResolvedAt() {
return Instant.ofEpochMilli(resolvedAt);
}
public boolean isCouldPublish() {
return couldPublish;
}
public long getPublishAttempts() {
return publishAttempts;
}
}

View File

@@ -22,6 +22,7 @@ package io.kamax.mxisd.threepid.connector.email;
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.Mxisd;
import io.kamax.mxisd.config.threepid.connector.EmailSmtpConfig; import io.kamax.mxisd.config.threepid.connector.EmailSmtpConfig;
import io.kamax.mxisd.exception.FeatureNotAvailable; import io.kamax.mxisd.exception.FeatureNotAvailable;
import io.kamax.mxisd.exception.InternalServerError; import io.kamax.mxisd.exception.InternalServerError;
@@ -92,7 +93,7 @@ public class EmailSmtpConnector implements EmailConnector {
try { try {
InternetAddress sender = new InternetAddress(senderAddress, senderName); InternetAddress sender = new InternetAddress(senderAddress, senderName);
MimeMessage msg = new MimeMessage(session, IOUtils.toInputStream(content, StandardCharsets.UTF_8)); MimeMessage msg = new MimeMessage(session, IOUtils.toInputStream(content, StandardCharsets.UTF_8));
msg.setHeader("X-Mailer", "mxisd"); // FIXME set version msg.setHeader("X-Mailer", Mxisd.Agent);
msg.setSentDate(new Date()); msg.setSentDate(new Date());
msg.setFrom(sender); msg.setFrom(sender);
msg.setRecipients(Message.RecipientType.TO, recipient); msg.setRecipients(Message.RecipientType.TO, recipient);

View File

@@ -21,11 +21,11 @@
package io.kamax.mxisd.threepid.generator; package io.kamax.mxisd.threepid.generator;
import io.kamax.matrix.ThreePid; import io.kamax.matrix.ThreePid;
import io.kamax.mxisd.as.IMatrixIdInvite;
import io.kamax.mxisd.config.MatrixConfig; import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.config.ServerConfig; import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.config.threepid.medium.GenericTemplateConfig; import io.kamax.mxisd.config.threepid.medium.GenericTemplateConfig;
import io.kamax.mxisd.exception.InternalServerError; import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.invitation.IMatrixIdInvite;
import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.threepid.session.IThreePidSession; import io.kamax.mxisd.threepid.session.IThreePidSession;
import io.kamax.mxisd.util.FileUtil; import io.kamax.mxisd.util.FileUtil;

View File

@@ -21,7 +21,7 @@
package io.kamax.mxisd.threepid.generator; package io.kamax.mxisd.threepid.generator;
import io.kamax.matrix.ThreePid; import io.kamax.matrix.ThreePid;
import io.kamax.mxisd.as.IMatrixIdInvite; import io.kamax.mxisd.invitation.IMatrixIdInvite;
import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.threepid.session.IThreePidSession; import io.kamax.mxisd.threepid.session.IThreePidSession;

View File

@@ -21,10 +21,10 @@
package io.kamax.mxisd.threepid.generator; package io.kamax.mxisd.threepid.generator;
import io.kamax.matrix.ThreePid; import io.kamax.matrix.ThreePid;
import io.kamax.mxisd.as.IMatrixIdInvite;
import io.kamax.mxisd.config.MatrixConfig; import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.config.ServerConfig; import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.http.IsAPIv1; import io.kamax.mxisd.http.IsAPIv1;
import io.kamax.mxisd.invitation.IMatrixIdInvite;
import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.threepid.session.IThreePidSession; import io.kamax.mxisd.threepid.session.IThreePidSession;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;

View File

@@ -21,8 +21,8 @@
package io.kamax.mxisd.threepid.notification; package io.kamax.mxisd.threepid.notification;
import io.kamax.matrix.ThreePid; import io.kamax.matrix.ThreePid;
import io.kamax.mxisd.as.IMatrixIdInvite;
import io.kamax.mxisd.exception.ConfigurationException; import io.kamax.mxisd.exception.ConfigurationException;
import io.kamax.mxisd.invitation.IMatrixIdInvite;
import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.notification.NotificationHandler; import io.kamax.mxisd.notification.NotificationHandler;
import io.kamax.mxisd.threepid.connector.ThreePidConnector; import io.kamax.mxisd.threepid.connector.ThreePidConnector;

View File

@@ -24,10 +24,10 @@ import com.sendgrid.SendGrid;
import com.sendgrid.SendGridException; import com.sendgrid.SendGridException;
import io.kamax.matrix.ThreePid; import io.kamax.matrix.ThreePid;
import io.kamax.matrix.ThreePidMedium; import io.kamax.matrix.ThreePidMedium;
import io.kamax.mxisd.as.IMatrixIdInvite;
import io.kamax.mxisd.config.MxisdConfig; import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.config.threepid.connector.EmailSendGridConfig; import io.kamax.mxisd.config.threepid.connector.EmailSendGridConfig;
import io.kamax.mxisd.exception.FeatureNotAvailable; import io.kamax.mxisd.exception.FeatureNotAvailable;
import io.kamax.mxisd.invitation.IMatrixIdInvite;
import io.kamax.mxisd.invitation.IThreePidInviteReply; import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.notification.NotificationHandler; import io.kamax.mxisd.notification.NotificationHandler;
import io.kamax.mxisd.threepid.generator.PlaceholderNotificationGenerator; import io.kamax.mxisd.threepid.generator.PlaceholderNotificationGenerator;

View File

@@ -0,0 +1,2 @@
org.slf4j.simpleLogger.logFile=System.out
org.slf4j.simpleLogger.log.org.xnio=warn

View File

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

View File

@@ -0,0 +1,109 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sàrl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.test.crypto;
import com.google.gson.JsonObject;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.matrix.json.MatrixJson;
import io.kamax.mxisd.crypto.Signature;
import io.kamax.mxisd.crypto.SignatureManager;
import io.kamax.mxisd.crypto.ed25519.Ed25519Key;
import io.kamax.mxisd.crypto.ed25519.Ed25519KeyManager;
import io.kamax.mxisd.crypto.ed25519.Ed25519RegularKeyIdentifier;
import io.kamax.mxisd.crypto.ed25519.Ed25519SignatureManager;
import io.kamax.mxisd.storage.crypto.KeyStore;
import io.kamax.mxisd.storage.crypto.MemoryKeyStore;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.assertThat;
public class SignatureManagerTest {
private static SignatureManager signMgr;
private static SignatureManager build(String keySeed) {
Ed25519Key key = new Ed25519Key(new Ed25519RegularKeyIdentifier("0"), keySeed);
KeyStore store = new MemoryKeyStore();
store.add(key);
return new Ed25519SignatureManager(new Ed25519KeyManager(store));
}
@BeforeClass
public static void beforeClass() {
signMgr = build(KeyTest.Private);
}
private void testSign(String value, String sign) {
assertThat(signMgr.sign(value).getSignature(), is(equalTo(sign)));
}
// As per https://matrix.org/docs/spec/appendices.html#json-signing
@Test
public void onEmptyObject() {
String value = "{}";
String sign = "K8280/U9SSy9IVtjBuVeLr+HpOB4BQFWbg+UZaADMtTdGYI7Geitb76LTrr5QV/7Xg4ahLwYGYZzuHGZKM5ZAQ";
testSign(value, sign);
}
// As per https://matrix.org/docs/spec/appendices.html#json-signing
@Test
public void onSimpleObject() {
JsonObject data = new JsonObject();
data.addProperty("one", 1);
data.addProperty("two", "Two");
String value = GsonUtil.get().toJson(data);
String sign = "KqmLSbO39/Bzb0QIYE82zqLwsA+PDzYIpIRA2sRQ4sL53+sN6/fpNSoqE7BP7vBZhG6kYdD13EIMJpvhJI+6Bw";
testSign(value, sign);
}
@Test
public void onFederationHeader() {
SignatureManager mgr = build("1QblgjFeL3IxoY4DKOR7p5mL5sQTC0ChmeMJlqb4d5M");
JsonObject o = new JsonObject();
o.addProperty("method", "GET");
o.addProperty("uri", "/_matrix/federation/v1/query/directory?room_alias=%23a%3Amxhsd.local.kamax.io%3A8447");
o.addProperty("origin", "synapse.local.kamax.io");
o.addProperty("destination", "mxhsd.local.kamax.io:8447");
String signExpected = "SEMGSOJEsoalrBfHqPO2QrSlbLaUYLHLk4e3q4IJ2JbgvCynT1onp7QF1U4Sl3G3NzybrgdnVvpqcaEgV0WPCw";
Signature signProduced = mgr.sign(o);
assertThat(signProduced.getSignature(), is(equalTo(signExpected)));
}
@Test
public void onIdentityLookup() {
String value = MatrixJson.encodeCanonical("{\n" + " \"address\": \"mxisd-federation-test@kamax.io\",\n"
+ " \"medium\": \"email\",\n" + " \"mxid\": \"@mxisd-lookup-test:kamax.io\",\n"
+ " \"not_after\": 253402300799000,\n" + " \"not_before\": 0,\n" + " \"ts\": 1523482030147\n" + "}");
String sign = "ObKA4PNQh2g6c7Yo2QcTcuDgIwhknG7ZfqmNYzbhrbLBOqZomU22xX9raufN2Y3ke1FXsDqsGs7WBDodmzZJCg";
testSign(value, sign);
}
}

View File

@@ -28,10 +28,10 @@ import io.kamax.matrix.ThreePidMedium;
import io.kamax.matrix._MatrixID; import io.kamax.matrix._MatrixID;
import io.kamax.matrix.json.GsonUtil; import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.Mxisd; import io.kamax.mxisd.Mxisd;
import io.kamax.mxisd.as.MatrixIdInvite;
import io.kamax.mxisd.config.MxisdConfig; import io.kamax.mxisd.config.MxisdConfig;
import io.kamax.mxisd.config.threepid.connector.EmailSmtpConfig; import io.kamax.mxisd.config.threepid.connector.EmailSmtpConfig;
import io.kamax.mxisd.config.threepid.medium.EmailConfig; import io.kamax.mxisd.config.threepid.medium.EmailConfig;
import io.kamax.mxisd.invitation.MatrixIdInvite;
import io.kamax.mxisd.threepid.connector.email.EmailSmtpConnector; import io.kamax.mxisd.threepid.connector.email.EmailSmtpConnector;
import io.kamax.mxisd.threepid.session.ThreePidSession; import io.kamax.mxisd.threepid.session.ThreePidSession;
import org.apache.commons.lang.RandomStringUtils; import org.apache.commons.lang.RandomStringUtils;

View File

@@ -0,0 +1,42 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sàrl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.test.storage.crypto;
import io.kamax.mxisd.storage.crypto.FileKeyStore;
import io.kamax.mxisd.storage.crypto.KeyStore;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
public class FileKeyStoreTest extends KeyStoreTest {
@Override
public KeyStore create() throws IOException {
String path = FileUtils.getTempDirectoryPath() +
"/mxisd-test-key-store-" +
UUID.randomUUID().toString().replace("-", "");
FileUtils.forceDeleteOnExit(new File(path));
return new FileKeyStore(path);
}
}

View File

@@ -0,0 +1,128 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2019 Kamax Sàrl
*
* https://www.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.test.storage.crypto;
import io.kamax.mxisd.crypto.*;
import io.kamax.mxisd.exception.ObjectNotFoundException;
import io.kamax.mxisd.storage.crypto.KeyStore;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.Before;
import org.junit.Test;
import java.util.Optional;
import static org.junit.Assert.*;
public abstract class KeyStoreTest {
private KeyStore store;
public abstract KeyStore create() throws Exception;
private Key generateRandomKey() {
KeyIdentifier keyId = new GenericKeyIdentifier(KeyType.Regular, "algo", RandomStringUtils.randomAlphanumeric(6));
return new GenericKey(keyId, true, RandomStringUtils.randomAlphanumeric(48));
}
@Before
public void before() throws Exception {
store = create();
}
@Test
public void isEmptyAfterCreate() {
assertTrue(store.list().isEmpty());
assertFalse(store.getCurrentKey().isPresent());
}
@Test
public void add() {
Key key = generateRandomKey();
KeyIdentifier keyId = key.getId();
store.add(key);
Key keyFromStore = store.get(keyId);
assertEquals(key.getId(), keyFromStore.getId());
assertEquals(key.getPrivateKeyBase64(), keyFromStore.getPrivateKeyBase64());
assertEquals(key.isValid(), keyFromStore.isValid());
assertTrue(store.list().contains(keyId));
assertTrue(store.list(keyId.getType()).contains(keyId));
}
@Test(expected = IllegalStateException.class)
public void addDuplicate() {
Key key = generateRandomKey();
store.add(key);
store.add(key);
}
@Test
public void update() {
Key key = generateRandomKey();
store.add(key);
Key keyUpdated = new GenericKey(key.getId(), !key.isValid(), key.getPrivateKeyBase64());
store.update(keyUpdated);
Key keyFromStore = store.get(key.getId());
assertEquals(key.getId(), keyFromStore.getId());
assertEquals(key.getPrivateKeyBase64(), keyFromStore.getPrivateKeyBase64());
assertEquals(key.isValid(), !keyFromStore.isValid());
}
@Test(expected = ObjectNotFoundException.class)
public void updateNonExisting() {
store.update(generateRandomKey());
}
@Test
public void delete() {
Key key = generateRandomKey();
store.add(key);
store.delete(key.getId());
assertFalse(store.list().contains(key.getId()));
assertFalse(store.list(key.getId().getType()).contains(key.getId()));
}
@Test(expected = ObjectNotFoundException.class)
public void deleteNonExisting() {
store.delete(generateRandomKey().getId());
}
@Test
public void setCurrentKey() {
Key key = generateRandomKey();
store.add(key);
store.setCurrentKey(key.getId());
Optional<KeyIdentifier> currentKey = store.getCurrentKey();
assertTrue(currentKey.isPresent());
assertEquals(currentKey.get(), key.getId());
}
@Test(expected = IllegalArgumentException.class)
public void setCurrentKeyNonExisting() {
store.setCurrentKey(generateRandomKey().getId());
}
}

View File

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